Twitter
golangチャレンジ:構造体
x
を受け取り、内部にあるすべての文字列フィールドに対してfn
を呼び出す関数walk(x interface{}, fn func(string))
を記述します。難易度:再帰的に。
これを行うには、リフレクション(reflection)を使用する必要があります。
コンピューティングにおけるリフレクションは、プログラムが、特にタイプを通じて、独自の構造を調べる能力です。それは一種のメタプログラミングです。また、混乱の元にもなります。
Goでは、string
、int
などの既知の型や、BankAccount
などの独自の型で機能する関数の点で、タイプの安全性を提供してきました。
つまり、自由な値(ドキュメント)を取得し、間違った型を関数に渡そうとするとコンパイラーが文句を言います。
コンパイル時に型がわからない関数を書きたいというシナリオに出くわすかもしれません。
Goでは、これを any 型と考えることができる型interface{}
で回避できます。
したがって、walk(x interface{}, fn func(string))
は、x
の任意の値を受け入れます。
「インターフェースinterface
」をとる関数のユーザーとして、タイプの安全性を失います。タイプstring
のFoo.bar
を関数に渡すつもりでしたが、代わりにint
であるFoo.baz
を渡した場合はどうなりますか?コンパイラーは間違いを通知できません。また、関数に渡すことが許可されている what もわかりません。たとえば関数が UserService
をとることを知ることは非常に便利です。
そのような関数の書き方として、渡された anything を検査して、型が何であり、それで何ができるのかを理解する必要があります。これは、リフレクション(reflection)を使用して行われます。これは非常に不格好で読みにくい場合があり、実行時にチェックを行う必要があるため一般的にパフォーマンスが低下します。
つまり、本当に必要な場合にのみ、リフレクションを使用してください。
ポリモーフィック関数が必要な場合は、ユーザーが関数を機能させるために必要なメソッドを実装している場合に、ユーザーが複数の型で関数を使用できるように、(interface
ではなく混乱を防ぐために)の周囲で設計できるかどうか検討してください。
私たちの機能は、さまざまなことを処理できる必要があります。いつものように、サポートしたい新しいものごとにテストを作成し、完了するまでリファクタリングを繰り返すというアプローチをとります。
文字列フィールドが(x
)に含まれている構造体で関数を呼び出す必要があります。 次に、渡された関数(fn
)をスパイして、呼び出されているかどうかを確認できます。
func TestWalk(t *testing.T) {expected := "Chris"var got []stringx := struct {Name string}{expected}walk(x, func(input string) {got = append(got, input)})if len(got) != 1 {t.Errorf("wrong number of function calls, got %d want %d", len(got), 1)}}
どの文字列がwalk
によってfn
に渡されたかを格納する文字列のスライス(got
)を格納したいと思います。多くの場合、前の章では、関数/メソッドの呼び出しをスパイするために専用の型を作成しましたが、この場合は、got
を閉じるfn
の匿名関数を渡すだけです。
最も単純な「ハッピー」パスを取得するために、文字列型のName
フィールドを持つ匿名のstruct
を使用します。
最後に、walk
をx
とスパイで呼び出し、今のところgot
の長さを確認するだけです。非常に基本的な動作が得られたら、アサーションでより具体的になります。
./reflection_test.go:21:2: undefined: walk
walk
を定義する必要があります
func walk(x interface{}, fn func(input string)) {}
テストを再試行してください
=== RUN TestWalk--- FAIL: TestWalk (0.00s)reflection_test.go:19: wrong number of function calls, got 0 want 1FAIL
このパスを作成するために、任意の文字列でスパイを呼び出すことができます。
func walk(x interface{}, fn func(input string)) {fn("I still can't believe South Korea beat Germany 2-0 to put them last in their group")}
これでテストに合格するはずです。 次に行う必要があるのは、fn
の呼び出し対象をより具体的にアサートすることです。
次のコードを既存のテストに追加して、fn
に渡された文字列が正しいことを確認します
if got[0] != expected {t.Errorf("got %q, want %q", got[0], expected)}
=== RUN TestWalk--- FAIL: TestWalk (0.00s)reflection_test.go:23: got 'I still can't believe South Korea beat Germany 2-0 to put them last in their group', want 'Chris'FAIL
func walk(x interface{}, fn func(input string)) {val := reflect.ValueOf(x)field := val.Field(0)fn(field.String())}
このコードは 非常に安全でナイーブ ですが、「赤」(テスト失敗)にあるときの目標は、可能な限り最小限のコードを記述することです。次に、懸念に対処するためのテストをさらに記述します。
リフレクションを使用してx
を確認し、そのプロパティを確認する必要があります。
reflect packageには、指定された変数のValue
を返す関数ValueOf
があります。これには、次の行で使用するフィールドなど、値を検査する方法があります。
次に、渡された値について非常に楽観的な仮定を行います。
最初で唯一のフィールドを見て、パニックを引き起こすフィールドがまったくない場合があります。
次に、基になる値を文字列として返すString()
を呼び出しますが、フィールドが文字列以外の場合は間違っていることがわかります。
私たちのコードは単純なケースに合格していますが、コードには多くの欠点があることを知っています。
さまざまな値を渡し、fn
が呼び出された文字列の配列をチェックするいくつかのテストを作成します。
新しいシナリオのテストを続行しやすくするために、テストをテーブルベースのテストにリファクタリングする必要があります。
func TestWalk(t *testing.T) {cases := []struct{Name stringInput interface{}ExpectedCalls []string} {{"Struct with one string field",struct {Name string}{ "Chris"},[]string{"Chris"},},}for _, test := range cases {t.Run(test.Name, func(t *testing.T) {var got []stringwalk(test.Input, func(input string) {got = append(got, input)})if !reflect.DeepEqual(got, test.ExpectedCalls) {t.Errorf("got %v, want %v", got, test.ExpectedCalls)}})}}
これで、シナリオを簡単に追加して、複数の文字列フィールドがある場合にどうなるかを確認できます。
次のシナリオをcases
に追加します。
{"Struct with two string fields",struct {Name stringCity string}{"Chris", "London"},[]string{"Chris", "London"},}
=== RUN TestWalk/Struct_with_two_string_fields--- FAIL: TestWalk/Struct_with_two_string_fields (0.00s)reflection_test.go:40: got [Chris], want [Chris London]
func walk(x interface{}, fn func(input string)) {val := reflect.ValueOf(x)for i:=0; i<val.NumField(); i++ {field := val.Field(i)fn(field.String())}}
val
には、値のフィールド数を返すメソッドNumField
があります。 これにより、フィールドを反復処理し、テストに合格したfn
を呼び出すことができます。
ここにコードを改善する明白なリファクターがあるようには見えないので、続けましょう。
walk
の次の欠点は、すべてのフィールドがstring
であると想定していることです。 このシナリオのテストを書いてみましょう。
次のケースを追加
{"Struct with non string field",struct {Name stringAge int}{"Chris", 33},[]string{"Chris"},},
=== RUN TestWalk/Struct_with_non_string_field--- FAIL: TestWalk/Struct_with_non_string_field (0.00s)reflection_test.go:46: got [Chris <int Value>], want [Chris]
フィールドのタイプがstring
であることを確認する必要があります。
func walk(x interface{}, fn func(input string)) {val := reflect.ValueOf(x)for i := 0; i < val.NumField(); i++ {field := val.Field(i)if field.Kind() == reflect.String {fn(field.String())}}}
そのKind
をチェックすることでそれを行うことができます。
繰り返しになりますが、コードは今のところ十分に妥当です。
次のシナリオは、「フラット」な「構造体」でない場合はどうなるのでしょうか。 言い換えると、いくつかのネストされたフィールドを持つstruct
があるとどうなりますか?
私たちは匿名構造体構文を使用して、テストのためにアドホックに型を宣言しているので、そのように続けることができます
{"Nested fields",struct {Name stringProfile struct {Age intCity string}}{"Chris", struct {Age intCity string}{33, "London"}},[]string{"Chris", "London"},},
しかし、内部の匿名構造体を取得すると、構文が少し乱雑になることがわかります。構文を改善するために作成する提案があります。
このシナリオの既知のタイプを作成してこれをリファクタリングし、テストで参照してみましょう。 私たちのテストのコードの一部がテストの外にあるという点で少し間接的ですが、読者は初期化を見て、struct
の構造を推測できるはずです。
次の型宣言をテストファイルのどこかに追加します。
type Person struct {Name stringProfile Profile}type Profile struct {Age intCity string}
これをケースに追加して、以前よりもはるかに明確に読み取ることができます。
{"Nested fields",Person{"Chris",Profile{33, "London"},},[]string{"Chris", "London"},},
=== RUN TestWalk/Nested_fields--- FAIL: TestWalk/Nested_fields (0.00s)reflection_test.go:54: got [Chris], want [Chris London]
問題は、型の階層の最初のレベルのフィールドでのみ反復していることです。
func walk(x interface{}, fn func(input string)) {val := reflect.ValueOf(x)for i := 0; i < val.NumField(); i++ {field := val.Field(i)if field.Kind() == reflect.String {fn(field.String())}if field.Kind() == reflect.Struct {walk(field.Interface(), fn)}}}
解決策は非常に簡単です。そのKind
をもう一度調べ、それが「構造体struct
」である場合は、その内部の「構造体struct
」でもう一度walk
を呼び出すだけです。
func walk(x interface{}, fn func(input string)) {val := reflect.ValueOf(x)for i := 0; i < val.NumField(); i++ {field := val.Field(i)switch field.Kind() {case reflect.String:fn(field.String())case reflect.Struct:walk(field.Interface(), fn)}}}
同じ値の比較を複数回行う場合、「一般的に」switch
にリファクタリングすると、読みやすさが向上し、コードの拡張が容易になります。
渡された構造体の値がポインターの場合はどうなりますか?
このケースを追加
{"Pointers to things",&Person{"Chris",Profile{33, "London"},},[]string{"Chris", "London"},},
=== RUN TestWalk/Pointers_to_thingspanic: reflect: call of reflect.Value.NumField on ptr Value [recovered]panic: reflect: call of reflect.Value.NumField on ptr Value
func walk(x interface{}, fn func(input string)) {val := reflect.ValueOf(x)if val.Kind() == reflect.Ptr {val = val.Elem()}for i := 0; i < val.NumField(); i++ {field := val.Field(i)switch field.Kind() {case reflect.String:fn(field.String())case reflect.Struct:walk(field.Interface(), fn)}}}
ポインターValue
でNumField
を使用することはできません。Elem()
を使用する前に、基になる値を抽出する必要があります。
与えられた interface{}
から reflect.Value
を関数に抽出する責任をカプセル化しましょう。
func walk(x interface{}, fn func(input string)) {val := getValue(x)for i := 0; i < val.NumField(); i++ {field := val.Field(i)switch field.Kind() {case reflect.String:fn(field.String())case reflect.Struct:walk(field.Interface(), fn)}}}func getValue(x interface{}) reflect.Value {val := reflect.ValueOf(x)if val.Kind() == reflect.Ptr {val = val.Elem()}return val}
これは実際には more コードを追加しますが、抽象化レベルは適切だと思います。
検査できるように、x
のreflect.Value
を取得します。方法は気にしません。
フィールドを反復処理し、そのタイプに応じて必要なことをすべて実行します。
次に、スライスをカバーする必要があります。
{"Slices",[]Profile {{33, "London"},{34, "Reykjavík"},},[]string{"London", "Reykjavík"},},
=== RUN TestWalk/Slicespanic: reflect: call of reflect.Value.NumField on slice Value [recovered]panic: reflect: call of reflect.Value.NumField on slice Value
これは以前のポインターシナリオに似ています。reflect.Value
で NumField
を呼び出そうとしていますが、構造体ではないため、これはありません。
func walk(x interface{}, fn func(input string)) {val := getValue(x)if val.Kind() == reflect.Slice {for i:=0; i< val.Len(); i++ {walk(val.Index(i).Interface(), fn)}return}for i := 0; i < val.NumField(); i++ {field := val.Field(i)switch field.Kind() {case reflect.String:fn(field.String())case reflect.Struct:walk(field.Interface(), fn)}}}
これは機能しますが、不幸です。心配はいりません。 テストに裏打ちされた実際のコードがあるので、好きなように自由に変更することができます。
少し抽象的に考えると、どちらかでwalk
と呼びたい
構造体の各フィールド
スライス内の各 thing
現時点でのコードはこれを実行しますが、十分に反映していません。 最初に、それがスライス(残りのコードの実行を停止するための return
付き)であるかどうかを確認し、そうでない場合は構造体であると想定します。
コードを書き直して、代わりにタイプ first を確認してから作業を行います。
func walk(x interface{}, fn func(input string)) {val := getValue(x)switch val.Kind() {case reflect.Struct:for i:=0; i<val.NumField(); i++ {walk(val.Field(i).Interface(), fn)}case reflect.Slice:for i:=0; i<val.Len(); i++ {walk(val.Index(i).Interface(), fn)}case reflect.String:fn(val.String())}}
格好良く!
構造体またはスライスの場合は、それぞれの値に対してwalk
を呼び出してその値を繰り返し処理します。 それ以外の場合、reflect.String
であれば、fn
を呼び出すことができます。
それでも、私にはそれがより良いものになり得るような気がします。フィールド/値を反復してwalk
を呼び出すという操作の繰り返しがありますが、概念的には同じです。
func walk(x interface{}, fn func(input string)) {val := getValue(x)numberOfValues := 0var getField func(int) reflect.Valueswitch val.Kind() {case reflect.String:fn(val.String())case reflect.Struct:numberOfValues = val.NumField()getField = val.Fieldcase reflect.Slice:numberOfValues = val.Len()getField = val.Index}for i:=0; i< numberOfValues; i++ {walk(getField(i).Interface(), fn)}}
value
がreflect.String
の場合、通常のようにfn
を呼び出すだけです。
それ以外の場合、switch
はタイプに応じて2つのものを抽出します
いくつのフィールドがありますか
Value
を抽出する方法(Field
またはIndex
)
それらを決定したら、getField
関数の結果を使用して、numberOfValues
が walk
を呼び出して反復できるようにします。
これで完了です。配列の処理は簡単です。
ケースに追加
{"Arrays",[2]Profile {{33, "London"},{34, "Reykjavík"},},[]string{"London", "Reykjavík"},},
=== RUN TestWalk/Arrays--- FAIL: TestWalk/Arrays (0.00s)reflection_test.go:78: got [], want [London Reykjavík]
配列はスライスと同じように処理できるため、コンマを使用してケースに追加するだけです。
func walk(x interface{}, fn func(input string)) {val := getValue(x)numberOfValues := 0var getField func(int) reflect.Valueswitch val.Kind() {case reflect.String:fn(val.String())case reflect.Struct:numberOfValues = val.NumField()getField = val.Fieldcase reflect.Slice, reflect.Array:numberOfValues = val.Len()getField = val.Index}for i:=0; i< numberOfValues; i++ {walk(getField(i).Interface(), fn)}}
次に処理するタイプは map
です。
{"Maps",map[string]string{"Foo": "Bar","Baz": "Boz",},[]string{"Bar", "Boz"},},
=== RUN TestWalk/Maps--- FAIL: TestWalk/Maps (0.00s)reflection_test.go:86: got [], want [Bar Boz]
ここでも少し抽象的に考えると、map
はstruct
に非常に似ていることがわかります。これは、コンパイル時にキーが不明であるということだけです。
func walk(x interface{}, fn func(input string)) {val := getValue(x)numberOfValues := 0var getField func(int) reflect.Valueswitch val.Kind() {case reflect.String:fn(val.String())case reflect.Struct:numberOfValues = val.NumField()getField = val.Fieldcase reflect.Slice, reflect.Array:numberOfValues = val.Len()getField = val.Indexcase reflect.Map:for _, key := range val.MapKeys() {walk(val.MapIndex(key).Interface(), fn)}}for i:=0; i< numberOfValues; i++ {walk(getField(i).Interface(), fn)}}
ただし、設計により、インデックスからマップから値を取得することはできません。これは key によってのみ行われるため、抽象化を壊します。
今の気分はどうですか?
当初は素晴らしい抽象化のように思われたかもしれませんが、コードは少し不安定に感じられます。
これで問題ありません! リファクタリングは道のりであり、間違いを犯すこともあります。 TDDの主要なポイントは、これらのことを試す自由を私たちに与えることです。
テストに裏打ちされた小さなステップを踏むことによって、これは決して不可逆的な状況ではありません。 リファクタリング前の状態に戻しましょう。
func walk(x interface{}, fn func(input string)) {val := getValue(x)walkValue := func(value reflect.Value) {walk(value.Interface(), fn)}switch val.Kind() {case reflect.String:fn(val.String())case reflect.Struct:for i := 0; i< val.NumField(); i++ {walkValue(val.Field(i))}case reflect.Slice, reflect.Array:for i:= 0; i<val.Len(); i++ {walkValue(val.Index(i))}case reflect.Map:for _, key := range val.MapKeys() {walkValue(val.MapIndex(key))}}}
walk
を導入しました。これは、val
からreflect.Value
を抽出するだけでよいように、switch
内のwalk
への呼び出しを乾燥させます。
Goのマップは順序を保証するものではないことに注意してください。したがって、fn
の呼び出しは特定の順序で行われると断言するため、テストが失敗することがあります。
これを修正するには、マップを含むアサーションを、順序を気にしない新しいテストに移動する必要があります。
t.Run("with maps", func(t *testing.T) {aMap := map[string]string{"Foo": "Bar","Baz": "Boz",}var got []stringwalk(aMap, func(input string) {got = append(got, input)})assertContains(t, got, "Bar")assertContains(t, got, "Boz")})
assertContains
の定義方法は次のとおりです
func assertContains(t *testing.T, haystack []string, needle string) {t.Helper()contains := falsefor _, x := range haystack {if x == needle {contains = true}}if !contains {t.Errorf("expected %+v to contain %q but it didn't", haystack, needle)}}
次に処理したい型はchan
です。
t.Run("with channels", func(t *testing.T) {aChannel := make(chan Profile)go func() {aChannel <- Profile{33, "Berlin"}aChannel <- Profile{34, "Katowice"}close(aChannel)}()var got []stringwant := []string{"Berlin", "Katowice"}walk(aChannel, func(input string) {got = append(got, input)})if !reflect.DeepEqual(got, want) {t.Errorf("got %v, want %v", got, want)}})
--- FAIL: TestWalk (0.00s)--- FAIL: TestWalk/with_channels (0.00s)reflection_test.go:115: got [], want [Berlin Katowice]
Recv()
で閉じられるまで、チャネルを通じて送信されたすべての値を反復処理できます
func walk(x interface{}, fn func(input string)) {val := getValue(x)var getField func(int) reflect.Valueswitch val.Kind() {case reflect.String:fn(val.String())case reflect.Struct:numberOfValues = val.NumField()getField = val.Fieldcase reflect.Slice, reflect.Array:numberOfValues = val.Len()getField = val.Indexcase reflect.Map:for _, key := range val.MapKeys() {walk(val.MapIndex(key).Interface(), fn)}case reflect.Chan:for v, ok := val.Recv(); ok; v, ok = val.Recv() {walk(v.Interface(), fn)}}}
次に処理するタイプはfunc
です。
t.Run("with function", func(t *testing.T) {aFunction := func() (Profile, Profile) {return Profile{33, "Berlin"}, Profile{34, "Katowice"}}var got []stringwant := []string{"Berlin", "Katowice"}walk(aFunction, func(input string) {got = append(got, input)})if !reflect.DeepEqual(got, want) {t.Errorf("got %v, want %v", got, want)}})
--- FAIL: TestWalk (0.00s)--- FAIL: TestWalk/with_function (0.00s)reflection_test.go:132: got [], want [Berlin Katowice]
このシナリオでは、引数のない関数はあまり意味がありません。ただし、任意の戻り値を許可する必要があります。
func walk(x interface{}, fn func(input string)) {val := getValue(x)var getField func(int) reflect.Valueswitch val.Kind() {case reflect.String:fn(val.String())case reflect.Struct:numberOfValues = val.NumField()getField = val.Fieldcase reflect.Slice, reflect.Array:numberOfValues = val.Len()getField = val.Indexcase reflect.Map:for _, key := range val.MapKeys() {walk(val.MapIndex(key).Interface(), fn)}case reflect.Chan:for v, ok := val.Recv(); ok; v, ok = val.Recv() {walk(v.Interface(), fn)}case reflect.Func:valFnResult := val.Call(nil)for _, res := range valFnResult {walk(res.Interface(), fn)}}}
reflect
パッケージのいくつかの概念を導入しました。
任意のデータ構造をたどるために再帰を使用しました。
振り返ってみると、悪いリファクタリングをしましたが、それについてはあまり動揺はありません。テストを反復的に行うことで、それほど大したことではありません。
これは、リフレクションの小さな側面だけをカバーしています。GOブログでは、詳細を網羅した優れた記事を掲載しています。
リフレクションについて理解したので、使用しないように最善を尽くしてください。