15
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[Golang] reflectパッケージの使い方を調べたら書く

Posted at

はじめに

最近reflectパッケージを使う機会があったので、その時どう書けばいいか調べたことをメモ書き。
気が向いたら更新してく。
面倒なのでimportとかは端折る。

reflect パッケージ

なにものなのかは godoc を見る。
DeepL翻訳様様で翻訳した内容↓

reflectパッケージは、プログラムが任意の型を持つオブジェクトを操作することを可能にする、ランタイムリフレクションを実装しています。典型的な使い方は,静的な型interface{}を持つ値を受け取り, TypeOfを呼んで動的な型情報を取り出し,それをTypeで返すというものです.

ValueOfを呼び出すと、実行時データを表すValueが返される。ZeroはTypeを取り、その型に対するゼロ値を表すValueを返す。

godocにも書かれているが https://go.dev/blog/laws-of-reflection も見てみると良さそう。

いろいろ使い方

Type/Valueを取得

interface{}を扱う場合の基本はここから始まる。

func a(v interface{}) {
	rt := reflect.TypeOf(v)  // return reflect.Type
	rv := reflect.ValueOf(v) // return reflect.Value
	// do something...
}

型を判定して分岐処理をする

reflectパッケージで定数化されている型はGitHubの reflect/type.go を参照。

func a(rt reflect.Type) {
	switch rt.Kind() {
	case reflect.String:
		// string型の場合の処理
	case reflect.Struct:
		// 構造体の場合の処理
	case reflect.Ptr:
		// ポインタの場合の処理
	default:
		// いずれの型でもない
	}
}

reflect.Valueでも同様にKind()を呼び出すことで型判定が可能。
(値がポインタでなければ基本的には問題なと思う。たぶん。)

func a(rv reflect.Kind) {
	switch rv.Kind() {
	case reflect.String:
		// string型の場合の処理
	default:
		// いずれの型でもない
	}
}

型を判定して分岐処理をする(ポインタを考慮)

reflect.TypeElem()を実行すれば良い。

Array, Chan, Map, Ptr, Slice以外の型に対してElem()を呼び出すとpanicが発生する。

func a(rt reflect.Type) {
	switch rt.Kind() {
	case reflect.String:
		fmt.Println("string")
	case reflect.Ptr:
		fmt.Println("pointer")
		a(rt.Elem())
	default:
		fmt.Printf("other(%s)", rt.Kind())
	}
}

reflect.Valueでも同様の処理を書くことは可能だがnil値の考慮が必要になる。

これはNGな例

func main() {
	var s *string
	a(reflect.ValueOf(s))
}

func a(rv reflect.Value) {
	switch rv.Kind() {
	case reflect.String:
		fmt.Println("string")
	case reflect.Ptr:
		fmt.Println("pointer")
		a(rv.Elem())
	default:
		fmt.Printf("other(%s)", rv.Kind())
	}
}

このコードを実行すると出力は以下のようになる。
nil値のポインタ型に対してElem()を実行した場合、Kind()メソッドが返す値はreflect.Invalidになるので、おそらく書いた本人が意図した挙動にはならない。

$ go run main.go
pointer!!
other(invalid)

reflect.Ptrの場合にIsNil()を呼び出すことでnil値か判定してあげれば同じようなことができないわけではない。

func a(rv reflect.Value) {
	switch rv.Kind() {
	case reflect.String:
		fmt.Println("string")
	case reflect.Ptr:
		fmt.Println("pointer")
		if rv.IsNil() {
			fmt.Println("nil!!")
			return
		}
		a(rv.Elem())
	default:
		fmt.Printf("other(%s)", rv.Kind())
	}
}

Array, Sliceの型を取得

例えばSliceに設定可能な型を取得したい場合、Elem().Kind()で取得できる。

var s []string

rt := reflect.TypeOf(s)
fmt.Println(rt.Elem().Kind()) // string

rv := reflect.ValueOf(s)
fmt.Println(rv.Type().Elem().Kind()) // string

reflect.Valueから値を取り出す

プリミティブ型

取得したい型に対応するメソッドが用意されているので実行すれば良い。

String()以外のメソッドは実際の値の型と異なるメソッドを呼び出すとpanicが発生する。

func a(rv reflect.Value) {
	switch rv.Kind() {
	case reflect.Bool:
		fmt.Println(rv.Bool())
	case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8:
		fmt.Println(rv.Int())
	case reflect.Uint, reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8:
		fmt.Println(rv.Uint())
	case reflect.Float32, reflect.Float64:
		fmt.Println(rv.Float())
	case reflect.String:
		fmt.Println(rv.String())
	default:
		fmt.Printf("other(%s)", rv.Kind())
	}
}

String()はpanicを発生させることはないが、値がstring以外の型の場合は <int Value> のような文字列が返ってくる。
設定された値を文字列として出力したい場合にString()を使用すると意図しない結果になるので注意が必要。

それぞれの型でstringに変換するような処理を実装してあげる。

func a(rv reflect.Value) {
	switch rv.Kind() {
	case reflect.Bool:
		if rv.Bool() {
			fmt.Println("true")	
		} else {
			fmt.Println("false")	
		}
	case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8:
		fmt.Println(strconv.FormatInt(rv.Int(), 10))
	case reflect.Uint, reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8:
		fmt.Println(strconv.FormatUint(rv.Uint(), 10))
	case reflect.Float32:
		fmt.Println(strconv.FormatFloat(rv.Float(), 'f', -1, 32))
	case reflect.Float64:
		fmt.Println(strconv.FormatFloat(rv.Float(), 'f', -1, 64))
	case reflect.String:
		fmt.Println(rv.String())
	default:
		fmt.Printf("other(%s)", rv.Kind())
	}
}

ただ文字列になればいいだけなら別にこれでも良いが。

func a(rv reflect.Value) {
	fmt.Printf("%v", rv.Interface())
}

Array、Slice

SliceとArrayの使い方は基本的に同じ。
Len()を呼び出すと長さが取得できるのでforで回しながらIndex()を呼び出して値を取得していく。
Index()reflect.Valueを返すので、型によって処理分岐したければKind()を使う。

ちなみにCap()を呼び出すとcapacityを取得できる。

Len()Cap()Array, Chan, Slice型以外の場合panicが発生する。
Index()Array, Slice, String型以外の場合panicが発生する。

func a(rv reflect.Value) {
	fmt.Println(rv.Len())
	fmt.Println(rv.Cap())

	for i := 0; i < rv.Len(); i++ {
		fmt.Println(rv.Index(i).String())
	}
}

Map

MapRange()を呼び出して、要素がなくなるまでforで回していく。
Mapのキーと値はそれぞれreflect.MapIterKey(), Value()で取得可能。
KeyとValuえはともにreflect.Valueを返すので、型によって処理分岐したければKind()を使う。

MapRange()Map型以外の場合panicが発生する。

func a(rv reflect.Value) {
	iter := rv.MapRange() // return *reflect.MapIter
	for iter.Next() {
		fmt.Printf("key: %v, value: %v", iter.Key(), iter.Value())
	}
}

構造体

NumField()で構造体が持つフィールド数を取得し、forで回していく。
フィールドの情報を取得するにはType()で一度reflect.Typeを取得してから、Field()を実行する必要がある。
フィールドの値を取得するにはFieldByName()を使用する(戻り値はreflect.Value)

NumField(), Field() FieldByName()Struct型以外の場合panicが発生する。

func a(rv reflect.Value) {
	for i := 0; i < rv.NumField(); i++ {
		f := rv.Type().Field(i)
		fv := rv.FieldByName(f.Name)
		fmt.Printf("field name: %s, value: %v", f.Name, fv)
	}
}

その他構造体のフィールドから情報を取るメソッド

タグを取得

TagフィールドのGet()を実行する。
タグは文字列で帰ってくるので、あとは自分が使いたいようにパースしたりして使う。

encoding/json の実装を参考にしてみると良いかも。

func main() {
	a(reflect.ValueOf(struct {
		a string `json:"abcde"`
	}{a: "aaaa"})) // tag: "abcde"
	a(reflect.ValueOf(struct{ a string }{a: "aaaa"})) // tag: ""
}

func a(rv reflect.Value) {
	for i := 0; i < rv.NumField(); i++ {
		f := rv.Type().Field(i)
		fmt.Printf("field name: %s, tag: %v\n", f.Name, f.Tag.Get("json"))
	}
}
フィールドがPublicかPrivateか判定

IsExported()を使う

encoding/json でも使っている。

func main() {
	a(reflect.ValueOf(struct{ A string }{A: "aaaa"})) // exported: true
	a(reflect.ValueOf(struct{ a string }{a: "aaaa"})) // exported: false
}

func a(rv reflect.Value) {
	for i := 0; i < rv.NumField(); i++ {
		f := rv.Type().Field(i)
		fmt.Printf("field name: %s, exported: %v", f.Name, f.IsExported())
	}
}

interface{}

interface{}を返すやつ
(TODO: 使ってないので雑)

func a(rv reflect.Value) {
	fmt.Println(rv.Interface())
}

reflect.Valueに値を設定する

reflectを使用して元の値を書き換えるためには https://go.dev/blog/laws-of-reflection第3法則 で説明されている内容が大事なポイント。

  1. To modify a reflection object, the value must be settable.

上記のように書かれています。
DeepL翻訳様様で翻訳した内容↓

  1. Reflectionオブジェクトを変更するには、その値が設定可能でなければなりません

詳細な内容は https://go.dev/blog/laws-of-reflection を見たほうが良い。

一応雑に書くとこんな感じ

以下のコードはxのコピーがreflect.ValueOfに渡るため
reflect.ValueOfの引数として生成されるインターフェース値はxそのものではない。

var x float64 = 3.4
v := reflect.ValueOf(x)

そのため以下のコードがもし成功したとしても、元の変数xの値は変わることはない。
そのような挙動は混乱を招き、役に立たないため違反であるため、panicが発生する。

v.SetFloat(7.1)

値が設定可能であるかを判定するには CanSet() メソッドを使用すれば良い。

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println(v.CanSet()) // false

値が設定可能であるとするためには、ValueOf()に変数xのポインタを渡す必要がある。

var x float64 = 3.4
v := reflect.ValueOf(&x) // --> set pointer
fmt.Println(v.CanSet()) // false

さらにポインタの参照先のValueを取得するためにElem()を使用することで、値が設定可能になります。

var x float64 = 3.4
v := reflect.ValueOf(&x)
fmt.Println(v.Elem().CanSet()) // true

Set

型を意識せずに値を設定したいだけならSet()を呼び出せば良い。

func a(rv reflect.Value, v interface{}) {
	src := reflect.ValueOf(v)
	rv.Elem().Set(src)
}

SetXXX

Set()ではなく、型に対応したメソッドを使うという方法もある。
設定したい型に対応するメソッドが用意されているので実行すれば良い。

func a(dist reflect.Value, v interface{}) {
	rv := reflect.ValueOf(v)

	switch rv.Kind() {
	case reflect.Bool:
		dist.SetBool(rv.Bool())
	case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8:
		dist.SetInt(rv.Int())
	case reflect.Uint, reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8:
		dist.SetUint(rv.Uint())
	case reflect.Float32, reflect.Float64:
		dist.SetFloat(rv.Float())
	case reflect.String:
		dist.SetString(rv.String())
	default:
		fmt.Printf("other(%s)", rv.Kind())
	}
}

Arrayの各indexに値を設定

Index().Set()を使う。

当然だがlen(dist) < len(src)の場合はpanic(reflect: array index out of range)が発生するので
ちゃんとやるなら双方のLen()のチェックを行うべきである。

func main() {
	var s [3]string
	rv := reflect.ValueOf(&s)

	fmt.Println(s) // ["", "", ""]
	a(rv, []string{"a", "b"})
	fmt.Println(s) // ["a", "b", ""]
}

func a(rv reflect.Value, src []string) {
	dist := rv.Elem()

	for i, s := range src {
		dist.Index(i).Set(reflect.ValueOf(s))
	}
}

Sliceに値を追加

reflect.AppendSlice()を使用し、戻り値をSet()に渡す。

reflect.AppendSlice()はSlice同士の結合を行うため、第2引数にはSliceをreflect.ValueOf()した結果を渡す必要がある。

func main() {
	var s []string
	rv := reflect.ValueOf(&s)

	fmt.Println(s) // []
	a(rv, []string{"a", "b", "c"})
	fmt.Println(s) // ["a", "b", "c"]
	a(rv, []string{"1", "2", "3"})
	fmt.Println(s) // ["a", "b", "c", "1", "2", "3"]
}

func a(rv reflect.Value, src []string) {
	dist := rv.Elem()
	dist.Set(reflect.AppendSlice(dist, reflect.ValueOf(src)))
}

Mapのキーを指定して値を設定

SetMapIndex()にキーと値のValueを設定する。

func main() {
	s := map[string]string{}
	rv := reflect.ValueOf(&s)

	fmt.Println(s) // map[]
	a(rv, map[string]string{"a": "hoge", "b": "fuga"})
	fmt.Println(s) // map[a:hoge b:fuga]
	a(rv, map[string]string{"b": "mage", "c": "age"})
	fmt.Println(s) // map[a:hoge b:mage c:age]
}

func a(rv reflect.Value, src map[string]string) {
	dist := rv.Elem()

	for key, val := range src {
		dist.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(val))
	}
}

値を初期化する

Arrayを作る

reflect.ArrayOf()を使う。
例えば[5]stringを作りたい場合は以下のようになる。

reflect.New()reflect.Ptr型のreflect.Valueを返すので、Elem()を使用しする必要がある。

rv := reflect.New(reflect.ArrayOf(5, reflect.TypeOf("")))
fmt.Printf("%#v\n", rv)        // &[5]string{"", "", "", "", ""}
fmt.Printf("%#v\n", rv.Elem()) // [5]string{"", "", "", "", ""}

Sliceを作る

reflect.MakeSlice()を使う。
make()と同様にlen, capを指定して使う。

s := []string{}

rv := reflect.MakeSlice(reflect.TypeOf(s), 5, 10)
fmt.Printf("%#v\n", rv)           // []string{"", "", "", "", ""}
fmt.Printf("len: %d\n", rv.Len()) // len: 5
fmt.Printf("cap: %d\n", rv.Cap()) // cap: 10
}

Mapを作る

reflect.MakeMapを使う。

s := map[string]string{}

rv := reflect.MakeMap(reflect.TypeOf(s))
fmt.Printf("%#v\n", rv)           // map[string]string{}

構造体を作る

reflect.New()に構造体のTypeを渡す。

s := struct {
	A string
	B int
}{}

rv := reflect.New(reflect.TypeOf(s))
fmt.Printf("%#v\n", rv)        // &struct { A string; B int }{A:"", B:0}
fmt.Printf("%#v\n", rv.Elem()) // struct { A string; B int }{A:"", B:0}
}
15
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?