經過一段時間的學習與實踐,針對 Golang 語言基礎的 TDD 驅動開發測試訓練已經告一段落,接下來會在此基礎上繼續使用 TDD 來構建應用程式 。
博主前一部分的程式碼Github先貼下面?以供參考
https://github.com/slowlydance2me/My_Golang_Study.git
2023-03-29 今日更新
golang 挑戰:編寫函式
walk(x interface{}, fn func(string))
,引數為結構體x
,並對x
中的所有字串欄位呼叫fn
函式。難度級別:遞迴。
為此,我們需要使用 反射。
計算中的反射提供了程式檢查自身結構體的能力,特別是透過型別,這是超程式設計的一種形式。這也是造成困惑的一個重要原因。
什麼是 interface
?
string
,int
以及我們自己定義的型別,如 BankAccount
,我們享受到了 Go 為我們提供的型別安全。interface{}
來解決這個問題,你可以將其視為 任意 型別。walk(x interface{}, fn func(string))
的 x 引數可以接收任何的值。
那麼為什麼不透過將所有引數都定義為 interface
型別來得到真正靈活的函式呢?
-
作為函式的使用者,使用
interface
將失去對型別安全的檢查。如果你想傳入string
型別的Foo.bar
但是傳入的是int
型別的Foo.baz
,編譯器將無法通知你這個錯誤。你也搞不清楚函式允許傳遞什麼型別的引數。知道一個函式接收什麼型別,例如UserService
,是非常有用的。
- 作為這樣一個函式的作者,你必須檢查傳入的 所有 引數,並嘗試斷定引數型別以及如何處理它們。這是透過 反射 實現的。這種方式可能相當笨拙且難以閱讀,而且一般效能比較差(因為程式必須在執行時執行檢查)。
interface
型別,這裡容易讓人困惑)設計它,以便使用者可以用多種型別來呼叫你的函式,這些型別實現了函式工作所需要的任何方法。首先編寫測試
我們想用一個 struct
來呼叫我們的函式,這個 struct
中有一個字串欄位(x
),然後我們可以監視傳入的函式(fn
),看看它是否被呼叫。
func TestWalk(t *testing.T) { expected := "Chris" var got []string x := 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) } }
-
我們想儲存一個字串切片(
got
),字串透過walk
傳遞到fn
。在前面的章節中,通常我們會專門為函式或方法呼叫指定型別,但在這種情況下,我們可以傳遞一個匿名函式給fn
,它會隱藏got
。
-
我們使用帶有
string
型別的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 1
FAIL
編寫足夠的程式碼使測試透過
我們可以使用任意的字串呼叫 fn
函式來使測試透過。
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 '%s', want '%s'", 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
並嘗試檢視它的屬性。ValueOf
,該函式值返回一個給定變數的 Value
。這為我們提供了檢查值的方法,包括我們在下一行中使用的欄位。-
我們只看第一個也是唯一的欄位,可能根本就沒有欄位會引起
panic
- 然後我們呼叫
String()它以字串的形式返回底層值,但是我們知道,如果這個欄位不是字串,程式就會出錯。
重構
fn
呼叫的字串陣列。func TestWalk(t *testing.T) { cases := []struct{ Name string Input 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 []string walk(test.Input, func(input string) { got = append(got, input) }) if !reflect.DeepEqual(got, test.ExpectedCalls) { t.Errorf("got %v, want %v", got, test.ExpectedCalls) } }) } }
現在,我們可以很容易地新增一個場景,看看如果有多個字串欄位會發生什麼。
首先編寫測試
為測試用例新增以下場景。.
{ "Struct with two string fields", struct { Name string City 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()) } }
value
有一個方法 NumField
,它返回值中的欄位數。這讓我們遍歷欄位並呼叫 fn
透過我們的測試。
重構
walk
的另一個缺點是它假設每個欄位都是 string 讓我們為這個場景編寫一個測試。
首先編寫測試
新增一下測試用例
{ "Struct with non string field", struct { Name string Age 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
怎麼辦?換句話說,如果我們有一個包含巢狀欄位的 struct會發生什麼?
首先編寫測試
我們臨時使用匿名結構體語法為我們的測試宣告型別,所以我們可以繼續這樣做
{ "Nested fields", struct { Name string Profile struct { Age int City string } }{"Chris", struct { Age int City string }{33, "London"}}, []string{"Chris", "London"}, },
struct
的結構。type Person struct { Name string Profile Profile } type Profile struct { Age int City 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_things
panic: 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()
提取底層值
重構
讓我們封裝一個獲得 reflect.Value
的功能,將 interface{}
傳入函式並返回這個值
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 }
-
得到
x
的reflect.Value
,這樣我就可以檢查它,我不在乎怎麼做。
-
遍歷欄位,根據其型別執行任何需要執行的操作。
首先編寫測試
{ "Slices", []Profile { {33, "London"}, {34, "Reykjavík"}, }, []string{"London", "Reykjavík"}, },
嘗試執行測試
=== RUN TestWalk/Slices
panic: 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
-
結構體中的每個欄位
-
切片中的每一項
return
來停止執行剩餘的程式碼),如果不是,我們就假設它是
struct
。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()) } }
struct
或切片,我們會遍歷它的值,並對每個值呼叫 walk
函式。如果是 reflect.String
,我們就呼叫 fn
。walk的重複操作,但概念上它們是相同的。
func walk(x interface{}, fn func(input string)) { val := getValue(x) numberOfValues := 0 var getField func(int) reflect.Value switch val.Kind() { case reflect.String: fn(val.String()) case reflect.Struct: numberOfValues = val.NumField() getField = val.Field case reflect.Slice: numberOfValues = val.Len() getField = val.Index } for i:=0; i< numberOfValues; i++ { walk(getField(i).Interface(), fn) } }
value
是一個 reflect.String
,我們就像平常一樣呼叫 fn
。switch
將根據型別提取兩個內容-
有多少欄位
-
如何提取
Value
(Field
或Index
)
numberOfValues
,使用 getField
函式的結果呼叫 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 := 0 var getField func(int) reflect.Value switch val.Kind() { case reflect.String: fn(val.String()) case reflect.Struct: numberOfValues = val.NumField() getField = val.Field case 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 := 0 var getField func(int) reflect.Value switch val.Kind() { case reflect.String: fn(val.String()) case reflect.Struct: numberOfValues = val.NumField() getField = val.Field case reflect.Slice, reflect.Array: numberOfValues = val.Len() getField = val.Index case reflect.Map: for _, key := range val.MapKeys() { walk(val.MapIndex(key).Interface(), fn) } } for i:=0; i< numberOfValues; i++ { walk(getField(i).Interface(), fn) } }
然而透過設計,你無法透過索引從 map
中獲取值。它只能透過 鍵 來完成,這樣就打破了我們的抽象,該死。
重構
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)) } } }
我們已經介紹了 walkValue
,它依照「Don't repeat yourself」的原則在 switch
中呼叫 walk
函式,這樣它們就只需要從 val
中提取 reflect.Value
即可。
最後一個問題
map
不能保證順序一致。因此,你的測試有時會失敗,因為我們斷言對 fn
的呼叫是以特定的順序完成的。map
t.Run("with maps", func(t *testing.T) { aMap := map[string]string{ "Foo": "Bar", "Baz": "Boz", } var got []string walk(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) { contains := false for _, x := range haystack { if x == needle { contains = true } } if !contains { t.Errorf("expected %+v to contain '%s' but it didnt", haystack, needle) } }
大功告成!
總結
-
介紹了
reflect
包中的一些概念。
-
使用遞迴遍歷任意資料結構。
-
在回顧中做了一個糟糕的重構,但不用對此感到太沮喪。透過迭代地進行測試,這並不是什麼大問題。