ludwig125のブログ

頑張りすぎずに頑張る父

go言語reflectメモ

reflect パッケージについてメモ

他記事:

ludwig125.hatenablog.com

reflectとは

  • reflectはGoでReflection機能を提供するためのパッケージ
  • このパッケージの関数を使うことで、引数の型や値を動的に取得できる

使い方1. (reflectで参照した変数の値を変更しないとき)

package main

import (
    "fmt"
    "reflect"
)

type Point struct {
    X int
    Y int
}

func main() {
    p := Point{10, 5} // 変数を定義
    rv := reflect.ValueOf(p)
    fmt.Printf("rv.Type = %v\n", rv.Type())  // 名前空間付きの型名
    fmt.Printf("rv.Kind= %v\n", rv.Kind()) // 格納リソース種別
    fmt.Printf("rv.Interface= %v\n", rv.Interface()) // interface{}としての実際の値

    xv := rv.Field(0) // rv内のX要素を取り出し
    fmt.Printf("xv: %d\n", xv.Int()) // intに変換して表示
}

実行結果

rv.Type = main.Point
rv.Kind= struct
rv.Interface= {10 5}
xv: 10

使い方2. (reflectで参照した変数の値を変更するとき)

  • 変数の値を変更するときは、SetInt(SetFloat, SetStringなど他にもいろいろ)を使う
  • ただし、変更するためには以下のようにValueOfにポインタを渡して、そのElemを取る必要がある
  • Elemはinterfaceの値もしくはポインタの参照先を返す

package main

import (
    "fmt"
    "reflect"
)

type Point struct {
    X int
    Y int
}

func main() {
    p := &Point{X: 10, Y: 5} // ポインタにすることによって編集可能にする
    rv := reflect.ValueOf(p).Elem() // Elemによってポインタが指している先の値を取得する
    fmt.Printf("rv.Type = %v\n", rv.Type())  // 名前空間付きの型名
    fmt.Printf("rv.Kind= %v\n", rv.Kind()) // 格納リソース種別
    fmt.Printf("rv.Interface= %v\n", rv.Interface()) // interface{}としての実際の値
    
    xv := rv.Field(0) // rv内のX要素を取り出し
    fmt.Printf("xv: %d\n", xv.Int()) // intに変換して表示    
    xv.SetInt(100) // intの100をX要素にセット
    fmt.Printf("after SetInt xv: %d\n", xv.Int()) // intに変換して表示
}

The Go Playground

実行結果

rv.Type = main.Point
rv.Kind= struct
rv.Interface= {10 5}
xv: 10
after SetInt xv: 100

【値を変更しようとしてpanicになる場合1】ポインタを渡さなかった場合

  • ポインタではなく値を変更しようとするとpanicになる
func main() {
    p := Point{X: 10, Y: 5}
    rv := reflect.ValueOf(p)
    xv := rv.Field(0)                             // rv内のX要素を取り出し
    fmt.Printf("xv: %d\n", xv.Int())              // intに変換して表示
    xv.SetInt(100)                               // intの100をX要素にセット => panic: reflect: reflect.Value.SetInt using unaddressable value
    fmt.Printf("after SetInt xv: %d\n", xv.Int()) // intに変換して表示
}

The Go Playground

実行結果

xv: 10
panic: reflect: reflect.Value.SetInt using unaddressable value

goroutine 1 [running]:
reflect.flag.mustBeAssignable(0x82, 0xf0a20)
    /usr/local/go/src/reflect/value.go:234 +0x180
reflect.Value.SetInt(0xf0460, 0x414020, 0x82, 0x7, 0x64, 0x0)
    /usr/local/go/src/reflect/value.go:1542 +0x40
main.main()
    /tmp/sandbox268672126/prog.go:18 +0x200

【値を変更しようとしてpanicになる場合2】Elemを使わなかった場合

  • Elemでreflect.Valueの指し示す先の値を取得していないとFieldメソッドを使った時にpanicになる
  • Field (再掲)
func main() {
    p := &Point{X: 10, Y: 5}
    rv := reflect.ValueOf(p)
    xv := rv.Field(0)                             // rv内のX要素を取り出し => panic: reflect: call of reflect.Value.Field on ptr Value
    fmt.Printf("xv: %d\n", xv.Int())              // intに変換して表示
    xv.SetInt(100)                                // intの100をX要素にセット
    fmt.Printf("after SetInt xv: %d\n", xv.Int()) // intに変換して表示
}

The Go Playground

実行結果

panic: reflect: call of reflect.Value.Field on ptr Value

goroutine 1 [running]:
reflect.Value.Field(0xe83c0, 0x414020, 0x16, 0x0, 0x40c120, 0x0, 0x90b80, 0x38f1)
    /usr/local/go/src/reflect/value.go:813 +0x160
main.main()
    /tmp/sandbox389676022/prog.go:16 +0xe0

値を変更できるか(SetXXXを使えるか)はCanSetを使うと判定できる

package main

import (
    "fmt"
    "reflect"
)

type Point struct {
    X int
    Y int
}

func main() {
    p := Point{X: 10, Y: 5}
    rv := reflect.ValueOf(p)
    if rv.CanSet() {
        fmt.Println("p CanSet: true")
        xv := rv.Field(0)                // rv内のX要素を取り出し
        fmt.Printf("xv: %d\n", xv.Int()) // intに変換して表示
        xv.SetInt(100)
        fmt.Printf("after SetInt xv: %d\n", xv.Int()) // intに変換して表示
    } else {
        fmt.Println("p CanSet: false")
    }
    pPtr := &Point{X: 10, Y: 5}
    rv = reflect.ValueOf(pPtr).Elem()
    if rv.CanSet() {
        fmt.Println("pPtr CanSet: true")
        xv := rv.Field(0)                // rv内のX要素を取り出し
        fmt.Printf("xv: %d\n", xv.Int()) // intに変換して表示
        xv.SetInt(100)
        fmt.Printf("after SetInt xv: %d\n", xv.Int()) // intに変換して表示
    } else {
        fmt.Println("pPtr CanSet: true")
    }
}

The Go Playground

実行結果

p CanSet: false
pPtr CanSet: true
xv: 10
after SetInt xv: 100

使えそうな使用例

1. 構造体のフィールドを順に出力

package main

import (
    "fmt"
    "reflect"
)

type Point struct {
    X int
    Y int
    Z int
}

func (p Point) PrintFiled() {
    v := reflect.ValueOf(p)
    t := v.Type()
    for i := 0; i < t.NumField(); i++ {
        // 構造体のフィールド名、フィールドの値、フィールドの型を出力
        fmt.Printf("%s %d %T\n", t.Field(i).Name, v.Field(i).Interface(), v.Field(i).Interface())
    }
}

func main() {
    p1 := Point{X: 10, Y: 5}
    p1.PrintFiled()

    fmt.Println("----")

    p2 := Point{X: 100, Y: 50, Z: 7}
    p2.PrintFiled()
}

The Go Playground

実行結果

X 10 int
Y 5 int
Z 0 int
----
X 100 int
Y 50 int
Z 7 int

2. 構造体の型が何であってもinterface型のSliceにまとめて返す

  • どんな型であってもinterfaceのスライスにしたい場面があったので
  • 以下のように、toInterfaceSlice関数を定義して、PointとUserのInterfaceSliceメソッドの中でこの関数を呼ぶだけにすることで共通化できた
  • toInterfaceSlice関数の引数はinterface{}なのでどの構造体でも受け付けられる
package main

import (
    "fmt"
    "reflect"
)

func toInterfaceSlice(v interface{}) []interface{} {
    var vs []interface{}
    rv := reflect.ValueOf(v)
    rt := rv.Type()
    for i := 0; i < rt.NumField(); i++ {
        vs = append(vs, rv.Field(i).Interface())
    }
    return vs
}

type Point struct {
    X int
    Y int
    Z int
}

func (p Point) InterfaceSlice() []interface{} {
    return toInterfaceSlice(p)
}

type User struct {
    ID     int
    Name   string
    Height float64
    Weight float64
}
func (u User) InterfaceSlice() []interface{} {
    return toInterfaceSlice(u)
}

func main() {
    p := Point{X: 10, Y: 5, Z: 1}
    for _, v := range p.InterfaceSlice() {
        fmt.Printf("%v %T\n", v, v)
    }

    fmt.Println("----")

    u := User{ID: 1001, Name: "hoge", Height: 170.1, Weight: 60.45}
    for _, v := range u.InterfaceSlice() {
        fmt.Printf("%v %T\n", v, v)
    }
}

The Go Playground

実行結果

10 int
5 int
1 int
----
1001 int
hoge string
170.1 float64
60.45 float64

2-2. 関数に渡すパラメータが構造体のポインタの場合でも対応できるようにする

  • 上のtoInterfaceSlice関数で引数が構造体のポインタの場合には以下のようにElemを使って実体を取得するようにすれば、 引数が構造体の場合も、構造体のポインタの場合も両方対応できる
func toInterfaceSlice(v interface{}) []interface{} {
    var vs []interface{}

    rv := reflect.ValueOf(v)

    fmt.Printf("rv.Kind %v\n", rv.Kind())
    // パラメータvが構造体のポインタのときはElemでポインタの指している先の値を取得する
    if rv.Kind() == reflect.Ptr {
        // vが構造体のポインタの時はここを通る
        rv = reflect.ValueOf(v).Elem()
    }
    rt := rv.Type()
    for i := 0; i < rt.NumField(); i++ {
        vs = append(vs, rv.Field(i).Interface())
    }
    return vs
}

type Point struct {
    X int
    Y int
    Z int
}

func (p Point) InterfaceSlice() []interface{} {
    // toInterfaceSliceには構造体を渡す
    return toInterfaceSlice(p)
}

type User struct {
    ID     int
    Name   string
    Height float64
    Weight float64
}

func (u *User) InterfaceSlice() []interface{} {
    // toInterfaceSliceには構造体のポインタを渡す
    return toInterfaceSlice(u)
}

全体のコードは以下

The Go Playground

実行結果

rv.Kind struct
10 int
5 int
1 int
----
rv.Kind ptr
1001 int
hoge string
170.1 float64
60.45 float64

2-3. 構造体のフィールドに構造体を含んでいる場合にも対応できるようにする

  • 以下のように、構造体のフィールドに構造体を持つ場合(ネストしている場合)にも対応できるようにしたい
type GamePlayerInfo struct {
    User
    Point
}
  • 以下のように条件分岐して再帰を使ってみる
func toInterfaceSlice(v interface{}) []interface{} {
    var vs []interface{}

    rv := reflect.ValueOf(v)
    if rv.Kind() == reflect.Ptr {
        rv = reflect.ValueOf(v).Elem()
    }
    rt := rv.Type()
    for i := 0; i < rt.NumField(); i++ {
        if rv.Field(i).Kind() == reflect.Struct {
            // フィールドがstructの場合は再帰でinterfaceのSliceを取得して後ろにつなげる
            sl := toInterfaceSlice(rv.Field(i).Interface())
            vs = append(vs, sl...)
        } else {
            vs = append(vs, rv.Field(i).Interface())
        }
    }
    return vs
}

全体のコードは以下

The Go Playground

実行結果

10 int
5 int
1 int
----
1001 int
hoge string
170.1 float64
60.45 float64
----
1001 int
hoge string
170.1 float64
60.45 float64
10 int
5 int
1 int

2-4. 構造体のフィールドで〇〇メソッドを持つ場合はそれを使うようにする(おまけ)

  • 以下のように、Attribute変数(Enum)のように、Stringメソッドを持つ場合はそれで出力させる
  • MethodByName を使う
type Attribute int

const (
    Fire Attribute = iota
    Water
    Earth
    Air
)

// constのString変換メソッド
func (a Attribute) String() string {
    return []string{"Fire", "Water", "Earth", "Air"}[a]
}

type SpecialGamePlayerInfo struct {
    User
    Point
    Attribute
}

The Go Playground

参考

Go 言語 reflect チートシート - Qiita

Go言語の型とreflect - Qiita

Go言語におけるinterface{}とリフレクションを使ったパターン - Qiita

reflect-examples/README.md at master · a8m/reflect-examples · GitHub

Go言語におけるinterface{}とリフレクションを使ったパターン - Qiita

Learning to Use Go Reflection – Capital One Tech – Medium