我最近在瀏覽 Hacker News 時看到一篇吸引我眼球的文章《[Python中的Lambdas和函數](http://www.thepythoncorner.com/2018/05/lambdas-and-functions-in-python.html?m=1)》,這篇文章 —— 我推薦你自己閱讀一下 —— 詳細講解了如何運用 Python 的 匿名函式,並舉了一個例子展示如何使用 Lambda 函數實現乾淨,[DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) 風格的代碼。讀這篇文章,我大腦中喜歡設計模式的部分對文章裡精巧的設計模式興奮不已,然而同時,我大腦中討厭動態語言的部分說,“呃~”。一點簡短的題外話來表達一下我對動態語言的厭惡(如果你沒有同感,請略過):我曾經是一個動態語言的狂熱粉絲(對某些任務我仍然喜歡動態語言並且幾乎每天都會使用到它)。Python 是我大學一直選擇的語言,我用它做科學計算並且做小的,概念驗證的項目(我的個人網站曾經使用Flask)。但是當我在現實世界([Qadium](https://www.qadium.com/))中開始為我的第一個大型 Python 項目做貢獻時,一切都變了。這些項目包含了收集,處理並且增強各種定義好的資料類型的系統職責。最開始我們選擇 Python 基於兩個原因:1)早期的員工都習慣使用 2)它是一門快速開發語言。當我開始項目時,我們剛啟動了我們最早的 B2B 產品,並且有幾個很早就使用 Python 的開發人員參與進來。留下來的代碼有幾個問題:1)代碼很難讀 2)代碼很難調試 3)代碼在不破壞些東西的前提下幾乎無法改變/重構。而且,代碼只經過了非常少的測試。這些就是快速搭建原型系統來驗證我們第一個產品價值的代價了。上述提到的問題太嚴重了,以至於後來大部分的開發時間都用來定位解決問題,很少有寶貴的時間來開發新功能或者修改系統來滿足我們不斷增長的收集和處理資料的慾望和需要。為瞭解決這些問題,我和另外一些工程師開始緩慢的,用一個靜態類型語言重新架構和重寫系統(對我當時來說,整體的體驗就像是一邊開著一輛著火的車,一邊還要再建造另一輛新車)。對處理系統,我們選擇了 Java 語言,對Tlog,我們選擇了 Go 語言。兩年後,我可以誠實的說,使用靜態語言,比如 Go( Go 依然保留了很多動態語言的感覺,比如 Python )。現在,我想肯定有不少讀者在嘟囔著“像 Python 這樣的動態語言是好的,只要把程式碼群組織好並且測試好”這類的話了吧,我並不想較真這個觀點,不過我要說的是,靜態語言在解決我們系統的問題中幫了大忙,並且更適合我們的系統。當支援和修複好這邊的麻煩事後,我們自己的基於 Python 的產生系統也做好了,我可以說我短期內都不想用動態語言來做任何的大項目了。那麼言歸正傳,當初我看到這篇文章時,我看到裡面有一些很棒的設計模式,我想試試看能否將它輕鬆的複製到 Go 中。如果你還沒有讀完[上述提及的文章](http://www.thepythoncorner.com/2018/05/lambdas-and-functions-in-python.html?m=1),我將它用 lambda /匿名函數解決的問題引述如下:> 假設你的一個客戶要求你寫一個程式來類比“逆波蘭運算式計算機”,他們會將這個程式安裝到他們全體員工的電腦上。你接受了這個任務,並且獲得了這個程式的需求說明:>> 程式能做所有的基礎運算(加減乘除),能求平方根和平方運算。很明顯,你應該能清空計算機的所有堆棧或者只刪除最後一個入棧的數值。如果你對逆波蘭運算式(RPN)不是很熟悉,可以在維基上或者找找它最開始的論文。現在開始解決問題,之前的文章作者提供一個可用但是極度冗餘的代碼。把它移植到 Go 中,就是這樣的```gopackage mainimport ("fmt""math")func main() {engine := NewRPMEngine()engine.Push(2)engine.Push(3)engine.Compute("+")engine.Compute("^2")engine.Compute("SQRT")fmt.Println("Result", engine.Pop())}// RPMEngine 是一個 RPN 計算引擎type RPMEngine struct {stack stack}// NewRPMEngine 返回一個 RPMEnginefunc NewRPMEngine() *RPMEngine {return &RPMEngine{stack: make(stack, 0),}}// 把一個值壓入內部堆棧func (e *RPMEngine) Push(v int) {e.stack = e.stack.Push(v)}// 把一個值從內部堆棧中取出func (e *RPMEngine) Pop() int {var v inte.stack, v = e.stack.Pop()return v}// 計算一個運算// 如果這個運算返回一個值,把這個值壓棧func (e *RPMEngine) Compute(operation string) error {switch operation {case "+":e.addTwoNumbers()case "-":e.subtractTwoNumbers()case "*":e.multiplyTwoNumbers()case "/":e.divideTwoNumbers()case "^2":e.pow2ANumber()case "SQRT":e.sqrtANumber()case "C":e.Pop()case "AC":e.stack = make(stack, 0)default:return fmt.Errorf("Operation %s not supported", operation)}return nil}func (e *RPMEngine) addTwoNumbers() {op2 := e.Pop()op1 := e.Pop()e.Push(op1 + op2)}func (e *RPMEngine) subtractTwoNumbers() {op2 := e.Pop()op1 := e.Pop()e.Push(op1 - op2)}func (e *RPMEngine) multiplyTwoNumbers() {op2 := e.Pop()op1 := e.Pop()e.Push(op1 * op2)}func (e *RPMEngine) divideTwoNumbers() {op2 := e.Pop()op1 := e.Pop()e.Push(op1 * op2)}func (e *RPMEngine) pow2ANumber() {op1 := e.Pop()e.Push(op1 * op1)}func (e *RPMEngine) sqrtANumber() {op1 := e.Pop()e.Push(int(math.Sqrt(float64(op1))))}```> rpn_calc_solution1.go 由 GitHub 託管,[查看源檔案](https://gist.github.com/jholliman/3f2461466ca1bc8e6b2d5c497de6c198/raw/179966bf7309e625ae304151937eedc9d3f2d067/rpn_calc_solution1.go)註:Go 並沒有一個內建的堆棧,所以,我自己建立了一個。```gopackage maintype stack []intfunc (s stack) Push(v int) stack {return append(s, v)}func (s stack) Pop() (stack, int) {l := len(s)return s[:l-1], s[l-1]}```> simple_stack.go 由 GitHub 託管,[查看源檔案](https://gist.github.com/jholliman/f1c8ce62ce2fbeb5ec4bc48f9326266b/raw/08e0c5df28eb0c527e0d4f0b2e85c1d381f7bc7c/simple_stack.go)(另外,這個堆棧不是安全執行緒的,並且對空堆棧進行 `Pop` 操作會引發 panic,除此之外,這個堆棧工作的很好)以上的方案是可以工作的,但是有大堆的代碼重複 —— 特別是擷取提供給運算子的參數/操作的代碼。Python-lambda 文章對這個方案做了一個改進,將運算函數寫為 lambda 運算式並且放入一個字典中,這樣它們可以通過名稱來引用,在運行期尋找一個運算所需要操作的數值,並用普通的代碼將這些運算元提供給運算函數。最終的python代碼如下:```python"""Engine class of the RPN Calculator"""import mathfrom inspect import signatureclass rpn_engine: def __init__(self): """ Constructor """ self.stack = [] self.catalog = self.get_functions_catalog() def get_functions_catalog(self): """ Returns the catalog of all the functions supported by the calculator """ return {"+": lambda x, y: x + y, "-": lambda x, y: x - y, "*": lambda x, y: x * y, "/": lambda x, y: x / y, "^2": lambda x: x * x, "SQRT": lambda x: math.sqrt(x), "C": lambda: self.stack.pop(), "AC": lambda: self.stack.clear()} def push(self, number): """ push a value to the internal stack """ self.stack.append(number) def pop(self): """ pop a value from the stack """ try: return self.stack.pop() except IndexError: pass # do not notify any error if the stack is empty... def compute(self, operation): """ compute an operation """ function_requested = self.catalog[operation] number_of_operands = 0 function_signature = signature(function_requested) number_of_operands = len(function_signature.parameters) if number_of_operands == 2: self.compute_operation_with_two_operands(self.catalog[operation]) if number_of_operands == 1: self.compute_operation_with_one_operand(self.catalog[operation]) if number_of_operands == 0: self.compute_operation_with_no_operands(self.catalog[operation]) def compute_operation_with_two_operands(self, operation): """ exec operations with two operands """ try: if len(self.stack) < 2: raise BaseException("Not enough operands on the stack") op2 = self.stack.pop() op1 = self.stack.pop() result = operation(op1, op2) self.push(result) except BaseException as error: print(error) def compute_operation_with_one_operand(self, operation): """ exec operations with one operand """ try: op1 = self.stack.pop() result = operation(op1) self.push(result) except BaseException as error: print(error) def compute_operation_with_no_operands(self, operation): """ exec operations with no operands """ try: operation() except BaseException as error: print(error)```> engine_peter_rel5.py 由GitHub託管 [查看源檔案](https://gist.github.com/mastro35/66044197fa886bf842213ace58457687/raw/a84776f1a93919fe83ec6719954384a4965f3788/engine_peter_rel5.py)這個方案比原來的方案只增加了一點點複雜度,但是現在增加一個新的運算子簡直就像增加一條線一樣簡單!我看到這個的第一個想法就是:我怎麼在 Go 中實現?我知道在 Go 中有[函數字面量](https://golang.org/ref/spec#Function_literals),它是一個很簡單的東西,就像在 Python 的方案中,建立一個運算子的名字與運算子操作的 map。它可以這麼被實現:```gopackage mainimport "math"func main() {catalog := map[string]interface{}{"+": func(x, y int) int { return x + y },"-": func(x, y int) int { return x - y },"*": func(x, y int) int { return x * y },"/": func(x, y int) int { return x / y },"^2": func(x int) int { return x * x },"SQRT": func(x int) int { return int(math.Sqrt(float64(x))) },"C": func() { /* TODO: need engine object */ },"AC": func() { /* TODO: need engine object */ },}}view rawrpn_operations_map.go hosted with by GitHub```> rpn_operations_map.go 由gitHub託管 [查看源檔案](https://gist.github.com/jholliman/9108b105be6ab136c2f163834b9e5e32/raw/bcd7634a1336b464a9957ea5f32a4868071bef6e/rpn_operations_map.go)注意:在 Go 語言中,為了將我們所有的匿名函數儲存在同一個 map 中,我們需要使用空介面類型,`interfa{}`。在 Go 中所有類型都實現了空介面(它是一個沒有任何方法的介面;所有類型都至少有 0 個函數)。在底層,Go 用兩個指標來表示一個介面:一個指向值,另一個指向類型。識別介面實際儲存的類型的一個方法是用 `.(type)` 來做斷言,比如:```gopackage mainimport ("fmt""math")func main() {catalog := map[string]interface{} {"+": func(x, y int) int { return x + y },"-": func(x, y int) int { return x - y },"*": func(x, y int) int { return x * y },"/": func(x, y int) int { return x / y },"^2": func(x int) int { return x * x },"SQRT": func(x int) int { return int(math.Sqrt(float64(x))) },"C": func() { /* TODO: need engine object */ },"AC": func() { /* TODO: need engine object */ },}for k, v := range catalog {switch v.(type) {case func(int, int) int:fmt.Printf("%s takes two operands\n", k)case func(int) int:fmt.Printf("%s takes one operands\n", k)case func():fmt.Printf("%s takes zero operands\n", k)}}}```> rpn_operations_map2.go 由GitHub託管 [查看源檔案](https://gist.github.com/jholliman/497733a937fa5949148a7160473b7742/raw/7ec842fe18026bfc1c4d801eca566b4c0541008b/rpn_operations_map2.go)這段代碼會產生如下輸出(請原諒文法上的瑕疵):```SQRT takes one operandsAC takes zero operands+ takes two operands/ takes two operands^2 takes one operands- takes two operands* takes two operandsC takes zero operands```這就揭示了一種方法,可以獲得一個運算子需要多少個運算元,以及如何複製 Python 的解決方案。但是我們如何能做到更好?我們能否為提取運算子所需參數抽象出一個更通用的邏輯?我們能否在不用 `if` 或者 `switch` 語句的情況下,尋找一個函數所需要的運算元的個數並且調用它?實際上通過 Go 中的 `relect` 包提供的反射功能,我們是可以做到的。對於 Go 中的反射,一個簡要的說明如下:在 Go 中,通常來講,如果你需要一個變數,類型或者函數,你可以定義它然後使用它。然而,如果你發現你是在運行時需要它們,或者你在設計一個系統需要使用多種不同類型(比如,實現運算子的函數 —— 它們接受不同數量的變數,因此是不同的類型),那麼你可以就需要使用反射。反射給你在運行時檢查,建立和修改不同類型的能力。如果需要更詳盡的 Go 的反射說明以及一些使用 `reflect` 包的基礎知識,請參閱[反射的規則](https://blog.golang.org/laws-of-reflection)這篇部落格。下列代碼示範了另一種解決方案,通過反射來實現尋找我們匿名函數需要的運算元的個數:```gopackage mainimport ("fmt""math""reflect")func main() {catalog := map[string]interface{}{"+": func(x, y int) int { return x + y },"-": func(x, y int) int { return x - y },"*": func(x, y int) int { return x * y },"/": func(x, y int) int { return x / y },"^2": func(x int) int { return x * x },"SQRT": func(x int) int { return int(math.Sqrt(float64(x))) },"C": func() { /* TODO: need engine object */ },"AC": func() { /* TODO: need engine object */ },}for k, v := range catalog {method := reflect.ValueOf(v)numOperands := method.Type().NumIn()fmt.Printf("%s has %d operands\n", k, numOperands)}}```> rpn_operations_map3.go 由GitHub託管 [查看源檔案](https://gist.github.com/jholliman/e3d7abd71b9bf6cb71eb55d49c40b145/raw/ed64e1d3f718e4219c68d189a8149d8179cdc90c/rpn_operations_map3.go)類似與用 `.(type)` 來切換的方法,代碼輸出如下:```^2 has 1 operandsSQRT has 1 operandsAC has 0 operands* has 2 operands/ has 2 operandsC has 0 operands+ has 2 operands- has 2 operands```現在我不再需要根據函數的簽名來寫入程式碼參數的數量了!注意:如果值的種類([種類(Kind)](https://golang.org/pkg/reflect/#Kind) 不要與類型弄混了))不是 `Func`,調用 `toNumIn` 會觸發 `panic`,所以小心使用,因為 panic 只有在運行時才會發生。通過檢查 Go 的 `reflect` 包,我們知道,如果一個值的種類(Kind)是 Func 的話,我們是可以通過調用 [`Call`](https://golang.org/pkg/reflect/#Value.Call) 方法,並且傳給它一個 值對象的切片來調用這個函數。比如,我們可以這麼做:```gopackage mainimport ("fmt""math""reflect")func main() {catalog := map[string]interface{}{"+": func(x, y int) int { return x + y },"-": func(x, y int) int { return x - y },"*": func(x, y int) int { return x * y },"/": func(x, y int) int { return x / y },"^2": func(x int) int { return x * x },"SQRT": func(x int) int { return int(math.Sqrt(float64(x))) },"C": func() { /* TODO: need engine object */ },"AC": func() { /* TODO: need engine object */ },}method := reflect.ValueOf(catalog["+"])operands := []reflect.Value{reflect.ValueOf(3),reflect.ValueOf(2),}results := method.Call(operands)fmt.Println("The result is ", int(results[0].Int()))}```> rpn_operations_map4.go 由GitHub託管 [查看源檔案](https://gist.github.com/jholliman/96bccb0c73c75a165211892da87cd676/raw/e4b420f77350e3f7e081dddb20c0d2a7232cc071/rpn_operations_map4.go)就像我們期待的那樣,這段代碼會輸出:```The result is 5```酷!現在我們可以寫出我們的終極解決方案了:```gopackage mainimport ("fmt""math""reflect")func main() {engine := NewRPMEngine()engine.Push(2)engine.Push(3)engine.Compute("+")engine.Compute("^2")engine.Compute("SQRT")fmt.Println("Result", engine.Pop())}// RPMEngine 是一個 RPN 計算引擎type RPMEngine struct {stack stackcatalog map[string]interface{}}// NewRPMEngine 返回 一個 帶有 預設功能目錄的 RPMEnginefunc NewRPMEngine() *RPMEngine {engine := &RPMEngine{stack: make(stack, 0),}engine.catalog = map[string]interface{}{"+": func(x, y int) int { return x + y },"-": func(x, y int) int { return x - y },"*": func(x, y int) int { return x * y },"/": func(x, y int) int { return x / y },"^2": func(x int) int { return x * x },"SQRT": func(x int) int { return int(math.Sqrt(float64(x))) },"C": func() { _ = engine.Pop() },"AC": func() { engine.stack = make(stack, 0) },}return engine}// 將一個值壓入內部堆棧func (e *RPMEngine) Push(v int) {e.stack = e.stack.Push(v)}// 從內部堆棧取出一個值func (e *RPMEngine) Pop() int {var v inte.stack, v = e.stack.Pop()return v}// 計算一個運算// 如果這個運算返回一個值,把這個值壓棧func (e *RPMEngine) Compute(operation string) error {opFunc, ok := e.catalog[operation]if !ok {return fmt.Errorf("Operation %s not supported", operation)}method := reflect.ValueOf(opFunc)numOperands := method.Type().NumIn()if len(e.stack) < numOperands {return fmt.Errorf("Too few operands for requested operation %s", operation)}operands := make([]reflect.Value, numOperands)for i := 0; i < numOperands; i++ {operands[numOperands-i-1] = reflect.ValueOf(e.Pop())}results := method.Call(operands)// If the operation returned a result, put it on the stackif len(results) == 1 {result := results[0].Int()e.Push(int(result))}return nil}```> rpn_calc_solution2.go 由 GitHub託管 [查看源檔案](https://gist.github.com/jholliman/c636340cac9253da98efdbfcfde56282/raw/b5f80bb79164b5250b088c6df094ca4ec9840009/rpn_calc_solution2.go)我們確定運算元的個數(第 64 行),從堆棧中獲得運算元(第 69-72 行),然後調用需要的運算函數,而且對不同參數個數的運算函數的調用都是一樣的(第 74 行)。而且與 Python 的解決方案一樣,增加新的運算函數,只需要往 map 中增加一個匿名函數的條目就可以了(第 30 行)。總結一下,我們已經知道如果使用匿名函數和反射將一個 Python 中有趣的設計模式複製到 Go 語言中來。對反射的過度使用我會保持警惕,反射增加了代碼的複雜度,而且我經常看到反射用於繞過一些壞的設計。另外,它會將一些本該在編譯期發生的錯誤變成運行期錯誤,而且它還會顯著拖慢程式(即將推出:兩種方案的基準檢查) —— 通過[檢查源碼](https://golang.org/src/reflect/value.go?s=9676:9715#L295),看起來 `reflect.Value.Call` 執行了很多的準備工作,並且對每一次調用 都為它的 `[]reflect.Value` 返回結果參數分配了新的切片。也就是說,如果效能不需要關注 —— 在 Python 中通常都不怎麼關注效能;),有足夠的測試,而且我們的目標是最佳化代碼的長度,並且想讓它易於加入新的運算,那麼反射是一個值得推薦的方法。
via: https://medium.com/@jhh3/anonymous-functions-and-reflection-in-go-71274dd9e83a
作者:John Holliman 譯者:MoodWu 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
146 次點擊