From 6eb1756aa516e37ae0af2ab2b1d91e283fbc9b07 Mon Sep 17 00:00:00 2001 From: Drew Bednar Date: Sun, 13 Oct 2024 14:23:03 -0400 Subject: [PATCH] Adding reflection module --- learn_go_with_tests/reflection/go.mod | 3 + learn_go_with_tests/reflection/reflection.go | 99 ++++++++++ .../reflection/reflection_test.go | 177 ++++++++++++++++++ 3 files changed, 279 insertions(+) create mode 100644 learn_go_with_tests/reflection/go.mod create mode 100644 learn_go_with_tests/reflection/reflection.go create mode 100644 learn_go_with_tests/reflection/reflection_test.go diff --git a/learn_go_with_tests/reflection/go.mod b/learn_go_with_tests/reflection/go.mod new file mode 100644 index 0000000..fae5224 --- /dev/null +++ b/learn_go_with_tests/reflection/go.mod @@ -0,0 +1,3 @@ +module reflection + +go 1.23.1 diff --git a/learn_go_with_tests/reflection/reflection.go b/learn_go_with_tests/reflection/reflection.go new file mode 100644 index 0000000..daf122f --- /dev/null +++ b/learn_go_with_tests/reflection/reflection.go @@ -0,0 +1,99 @@ +package reflection + +import "reflect" + +// Go allows for `interface{}` which we can think as as just +// any type. (any is just an alias for interface{}) this allows for +// some metaprogramming features in the language reflection being one +// of them. Reflection allows us to examine structure at runtime. + +// The downside, you lose type safety and performance impact. + +// Only use reflection if you really need to. + +// See also https://blog.golang.org/laws-of-reflection + +func walk(x interface{}, fn func(input string)) { + // This code is very unsafe and very naive, + val := getValue(x) + + // if val.Kind() == reflect.Slice { + // for i := 0; i < val.Len(); i++ { + // walk(val.Index(i).Interface(), fn) + // } + // return + // } + + // // This would panic if there were no fields + // field := val.Field(0) + // // This would be wrong if the field had any value other than a string + // fn(field.String()) + + // // change for test table ( will fail case 2) + // for i := 0; i < val.NumField(); i++ { + // field := val.Field(i) + // fn(field.String()) + // } + + // THIS GOT messy + // for i := 0; i < val.NumField(); i++ { + // field := val.Field(i) + + // // if field.Kind() == reflect.String { + // // fn(field.String()) + // // } + + // // if field.Kind() == reflect.Struct { + // // // it got recursive + // // walk(field.Interface(), fn) + // // } + + // switch field.Kind() { + // case reflect.String: + // fn(field.String()) + // case reflect.Struct: + // walk(field.Interface(), fn) + // } + // } + + switch val.Kind() { + case reflect.Struct: + for i := 0; i < val.NumField(); i++ { + walk(val.Field(i).Interface(), fn) + } + case reflect.Slice, reflect.Array: + for i := 0; i < val.Len(); i++ { + walk(val.Index(i).Interface(), fn) + } + case reflect.Map: + for _, key := range val.MapKeys() { + walk(val.MapIndex(key).Interface(), fn) + } + + case reflect.Chan: + for { + if v, ok := val.Recv(); ok { + walk(v.Interface(), fn) + } else { + break + } + } + case reflect.Func: + valFnResult := val.Call(nil) + for _, res := range valFnResult { + walk(res.Interface(), fn) + } + case reflect.String: + fn(val.String()) + } +} + +func getValue(x interface{}) reflect.Value { + val := reflect.ValueOf(x) + + if val.Kind() == reflect.Pointer { + val = val.Elem() + } + + return val +} diff --git a/learn_go_with_tests/reflection/reflection_test.go b/learn_go_with_tests/reflection/reflection_test.go new file mode 100644 index 0000000..85dc83a --- /dev/null +++ b/learn_go_with_tests/reflection/reflection_test.go @@ -0,0 +1,177 @@ +package reflection + +import ( + "reflect" + "testing" +) + +type Person struct { + Name string + Profile Profile +} + +type Profile struct { + Age int + City string +} + +func assertContains(t testing.TB, haystack []string, needle string) { + t.Helper() + contains := false + for _, x := range haystack { + if x == needle { + contains = true + } + } + if !contains { + t.Errorf("expected %v to contain %q but it didn't", haystack, needle) + } +} + +func TestWalk(t *testing.T) { + + cases := []struct { + Name string + Input interface{} + ExpectedCalls []string + }{ + // case 0 + { + Name: "struct with on string field", + Input: struct{ Name string }{"Drew"}, + ExpectedCalls: []string{"Drew"}, + }, + // case 1 + { + "struct with two string fields", + struct { + Name string + City string + }{"Chris", "London"}, + []string{"Chris", "London"}, + }, + // case 2 + { + "struct with non string field", + struct { + Name string + Age int + }{"Chris", 33}, + []string{"Chris"}, + }, + // case 3 Nested Struct + { + "nested fields", + Person{ + "Chris", + Profile{33, "London"}, + }, + []string{"Chris", "London"}, + }, + // case 4 struct as pointer + { + "pointers to things", + &Person{ + "Chris", + Profile{33, "London"}, + }, + []string{"Chris", "London"}, + }, + // case 5 slice of profiles + { + "slices", + []Profile{ + {33, "London"}, + {34, "Reykjavík"}, + }, + []string{"London", "Reykjavík"}, + }, + // case 6 array + { + "arrays", + [2]Profile{ + {33, "London"}, + {34, "Reykjavík"}, + }, + []string{"London", "Reykjavík"}, + }, + // case 7 map THIS HAS A GOTCHA + // maps in go do not guarantee order. It will eventually fake + // moved to a separate test run to handle that case + // { + // "maps", + // map[string]string{ + // "Cow": "Moo", + // "Sheep": "Baa", + // }, + // []string{"Moo", "Baa"}, + // }, + } + + for _, test := range cases { + t.Run(test.Name, func(t *testing.T) { + var got []string + walk(test.Input, func(input string) { + got = append(got, input) + }) + + if !reflect.DeepEqual(got, test.ExpectedCalls) { + t.Errorf("get %v, want %v", got, test.ExpectedCalls) + } + }) + + } + + t.Run("with mas", func(t *testing.T) { + aMap := map[string]string{ + "Cow": "Moo", + "Sheep": "Baa", + } + + var got []string + walk(aMap, func(input string) { + got = append(got, input) + }) + + assertContains(t, got, "Moo") + assertContains(t, got, "Baa") + }) + + 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 []string + want := []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) + } + }) + + t.Run("with function", func(t *testing.T) { + aFunction := func() (Profile, Profile) { + return Profile{33, "Berlin"}, Profile{34, "Katowice"} + } + + var got []string + want := []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) + } + }) +}