這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。當我接受了 Go 根本沒有 object 之後,我才開始更容易理解 Go 的 object 是什麼,其實就是一些可以操作共有狀態的函數集合,加了點文法糖的點綴。你可能心想“閉嘴吧,Go 當然有 object”,或者想“能操作共有狀態的函數集合就是 object 的定義啊”,好吧,也許你是對的。至少從我能想到的之前用過的 object 來看,我沒看出來一些操作相同狀態的相關函數的集合,和一個 object 有啥區別。再說 Go 的 object 模型不止是文法糖(我說的貌似有點極端哈:-))。不過 object 模型和經典的,比如 Java,C++ 和 Python(我目前就瞭解這麼多)的模型相比還是有很大不同的。在苦苦探索 Go 的 object 是如何工作的過程中,放棄傳統的 object 觀念,而只從函數方面考慮問題,這讓我受益良多。我要做的就是嘗試將 object 模型解構成函數然後重建,看 Go 是如何工作的,可以看出來,Go 更傾向於用 object 做輔助從而讓文法更簡單,而不是像物件導向的語言一樣,什麼都是 object。下面的解構看起來可能不太好,因為我不精通 Go,不過我還是強迫自己試了試,因為看起來還挺好玩的:-)。## 從函數開始好,我試著開始從函數上證實下,下面這個略蠢的例子是為了看看用函數能做什麼。我們來定義一個類型,實際上這是一個函數:```gotype Adder func(int, int) int```你可以把這當成一個 interface(不過它們不是一回事)。任何符合這個特徵的函數都可以被當作 **Adder** 類型:```go// Same type as Adderfunc add(a int, b int) int {return a + b}```一個不知道怎麼 add 的抽象 adder:```gofunc abstractedAdd(a Adder, b int, c int) int {return a(b, c)}```這真讓人想起可以用 interface 做類似的事。 **abstractedAdd** 不知道怎麼做 add,但是他可以接受任何一個遵循同樣協議的 Adder 的實現。下面給出最無用的也是最簡單的例子,全部代碼:```gopackage mainimport "fmt"type Adder func(int, int) int// Same type as Adderfunc add(a int, b int) int {return a + b}func abstractedAdd(a Adder, b int, c int) int {return a(b, c)}func main() {var a Adderfmt.Printf("Adder: %v\n", a)a = addfmt.Printf("Adder initialized: %v\n", a)fmt.Printf("%d + %d = %d\n", 1, 1, abstractedAdd(a, 1, 1))fmt.Printf("%d + %d = %d\n", 1, 1, abstractedAdd(add, 1, 1))}```從這個例子中我們探討下 Go 的 object。一個方法能符合 **Adder** 類型嗎?依你的經驗來看可能有點反直覺(就好象,你需要一個函數,而實際給了一個方法,這個意思),我們看看 adder object。```gotype ObjectAdder struct{}func (o *ObjectAdder) Add(a int, b int) int {return a + b}```看起來沒錯,加到我們的例子裡:```gopackage mainimport "fmt"type Adder func(int, int) intfunc add(a int, b int) int {return a + b}func abstractedAdd(a Adder, b int, c int) int {return a(b, c)}type ObjectAdder struct{}func (o *ObjectAdder) Add(a int, b int) int {return a + b}func main() {var a Adderfmt.Printf("Adder: %v\n", a)a = addfmt.Printf("Adder initialized: %v\n", a)fmt.Printf("func: %d + %d = %d\n", 1, 1, abstractedAdd(a, 1, 1))fmt.Printf("func: %d + %d = %d\n", 1, 1, abstractedAdd(add, 1, 1))var o *ObjectAdderfmt.Printf("object: %d + %d = %d\n", 1, 1, abstractedAdd(o.Add, 1, 1))}```結果輸出:```Adder: <nil>Adder initialized: 0x401000func: 1 + 1 = 2func: 1 + 1 = 2object: 1 + 1 = 2```哈,成功的。和介面不一樣,函數簽名不會匹配任何方法名稱,你可以像傳參數一樣傳方法,因為方法實際上就是函數,有點像下面這個:```govar o *ObjectAdderfmt.Printf("object: %d + %d = %d\n", 1, 1, abstractedAdd(o.Whatever, 1, 1))```應該能運行。沒看出來方法就是函數嗎?再看這個:```gofmt.Printf("func add: %T\n", add)fmt.Printf("object.Add: %T\n", o.Add)```結果輸出:```func add: func(int, int) intobject.Add: func(int, int) int```你看出來這個空函數和這個 object 方法的區別了嗎?沒有,因為沒區別。這就是為什麼傳參數可以運行。這也可以解釋代碼裡另一個容易讓學 Go 的新手(像我這樣的)困惑的問題。我們在例子中沒有完全初始化 ObjectAdder。我用指標是有目的的,你們可以看到指標也沒有初始化(nil 的),可是代碼卻能運行。在我所知道的其他的物件導向的語言裡,這不可能啟動並執行,但是在 Go 裡可以,為什麼呢?那是因為在 Go 裡,根本就沒有方法,沒有方法類型,方法實際上就是文法糖,用來在做函數調用的時候傳遞一個執行個體類型來作為第一個參數(就像在 C 語言裡習慣用的那樣)。在 Go 裡第一個參數類型通常被稱為方法接收者,不過這沒什麼特別的,就是傳遞給函數的一個參數。細化下我們的例子:```gofmt.Printf("ObjectAdder.Add: %T\n", (*ObjectAdder).Add)fmt.Printf("ObjectAdder.Add: %d + %d = %d\n", 1, 1, (*ObjectAdder).Add(nil, 1, 1))```我在這做了什麼呢?就是搞清楚你在做如下聲明的時候 Go 實際上做了什麼:```gotype ObjectAdder struct{}func (o *ObjectAdder) Add(a int, b int) int {}```在這給 ***ObjectAdder** 類型添加了一個函數。這個函數可以被訪問並且可以被當作任何值使用(被調用,作為參數傳遞等)。如果你覺得“嗨,ObjectAdder 類型可不是 \*ObjectAdder”,好吧,在 Go 裡指標類型確實是另一種類型,甚至和有函數組合的指標也不是一個類型。要加哪個類型的函數是由方法接收者決定的,在這個 case 裡就是(\*ObjectAdder)。這和 Go 的[方法集合](https://github.com/golang/go/wiki/MethodSets)的概念有關。總之,繼續往下看吧,輸出結果:```ObjectAdder.Add: func(*main.ObjectAdder, int, int) intObjectAdder.Add: 1 + 1 = 2```根本就沒有方法,就是函數。我們在 Go 裡看到的 object 就是一些關聯到某個類型的函數組合,加點文法糖,來把第一個參數傳給你。說實話就好像所有的物件導向中的 object 實際上實現了一樣。好處是在 Go 這是 100% 簡潔明確的,沒有魔術,就是文法糖。 Go 在簡潔這方面做的確實嚴謹。這樣很多事情就更簡單一致了,從例子裡可以看出來。傳函數或方法做參數沒有任何區別(我想不出來有區別的理由)。下面是例子中最終的所有代碼:```gopackage mainimport "fmt"type Adder func(int, int) int// Same type as Adderfunc add(a int, b int) int {return a + b}func abstractedAdd(a Adder, b int, c int) int {return a(b, c)}type ObjectAdder struct{}func (o *ObjectAdder) Add(a int, b int) int {return a + b}func main() {var a Adderfmt.Printf("Adder: %v\n", a)a = addfmt.Printf("Adder initialized: %v\n", a)fmt.Printf("func: %d + %d = %d\n", 1, 1, abstractedAdd(a, 1, 1))fmt.Printf("func: %d + %d = %d\n", 1, 1, abstractedAdd(add, 1, 1))var o *ObjectAdderfmt.Printf("func add: %T\n", add)fmt.Printf("object.Add: %T\n", o.Add)fmt.Printf("object: %d + %d = %d\n", 1, 1, abstractedAdd(o.Add, 1, 1))fmt.Printf("ObjectAdder.Add: %T\n", (*ObjectAdder).Add)fmt.Printf("ObjectAdder.Add: %d + %d = %d\n", 1, 1, (*ObjectAdder).Add(nil, 1, 1))}```這個例子是完全狀態無關的。Object 通常會有狀態和副作用,Go 的函數也有狀態和副作用嗎?## 函數和狀態 ##為了讓函數和 object 之間的差距更小一點,我們用個最原始的/最簡單的例子,一個 iterator:```gopackage mainimport "fmt"func iterator() func() int {a := 0return func() int {a++return a}}func main() {iter := iterator()fmt.Printf("iter 1: %d\n", iter())fmt.Printf("iter 2: %d\n", iter())fmt.Printf("iter 3: %d\n", iter())}```如果你運行一下,你就會看到這個 iterator 是有效。準確的講我們現在有什麼呢?我們有一個 **iterator** 函數,看起來像另一個函數的的建構函式,它會返回這個函數,這就是為什麼 **iterator** 返回的類型是:```gofunc() int```通常指的閉包是這樣的文法結構:```goa := 0return func() int {a++return a}```我們執行個體化的這個函數,用到了外部的一個變數,會將變數 **a** 和新建立的函數關聯起來,它包含一個 **a** 的引用而且可以操作(**a**)。如果你習慣用 object 作為一種狀態管理方式(實際上很多 C 編程者也覺得奇怪,因為在 C 語言裡函數是靜態構造的),這就比較燒腦了。在 Go 語言裡,函數可以隨時初始化,下面是這個例子的另一版,說明我們實際在初始化函數:```gopackage mainimport "fmt"func iterator() func() int {a := 0return func() int {a++return a}}func main() {itera := iterator()iterb := iterator()fmt.Printf("itera 1: %d\n", itera())fmt.Printf("itera 2: %d\n", itera())fmt.Printf("itera 3: %d\n", itera())fmt.Printf("iterb 1: %d\n", iterb())fmt.Printf("iterb 2: %d\n", iterb())fmt.Printf("iterb 3: %d\n", iterb())}```得到結果:```itera 1: 1itera 2: 2itera 3: 3iterb 1: 1iterb 2: 2iterb 3: 3```因此每個 iterator 都是互相獨立的,沒有辦法讓一個函數從另一個函數獲得狀態,除非在代碼裡明確的允許,或者你用不安全的包,做很糟糕的指標運算。這還挺有意思的,因為像 Lisp 這樣的語言最初就有閉包,提供了你可以想象的絕對最大程度的封裝。除了從函數中你沒有其他辦法直接擷取狀態。我們看一眼用 Go 的 object 的閉包是什麼樣的:```gopackage mainimport "fmt"type iterator struct {a int}func (i *iterator) iter() int {i.a++return i.a}func newIter() *iterator {return &iterator{a: 0,}}func main() {i := newIter()fmt.Printf("iter 1: %d\n", i.iter())fmt.Printf("iter 2: %d\n", i.iter())fmt.Printf("iter 3: %d\n", i.iter())}```可以看到,非常簡單的事情,用 object 的方式會顯得更笨拙一點,至少在我看來是這樣。我甚至用了一個同樣糟糕的名為 **a** 的 int 變數,實際上它表示狀態。現在我們建立一個 struct,儲存狀態,給函數添加類型,用這個函數控制狀態。 如果你覺得複雜,你也可以不用 struct:```gopackage mainimport "fmt"type iterator intfunc (i *iterator) iter() int {*i++return int(*i)}func main() {var i iteratorfmt.Printf("iter 1: %d\n", i.iter())fmt.Printf("iter 2: %d\n", i.iter())fmt.Printf("iter 3: %d\n", i.iter())}```這個函數也做了同樣的事情,用不同的方式。和 object 一樣也可以管理狀態,而且用範圍將狀態隔離,只有函數能修改這個狀態。為了完成這部分,我們寫幾個函數,操作一個共用狀態(這就是 object 所做的):```go package mainimport "fmt"type stateChanger func() intfunc new() (stateChanger, stateChanger) {a := 0return func() int {a++return a},func() int {a--return a}}func main() {inc, dec := new()fmt.Printf("inc 1: %d\n", inc())fmt.Printf("inc 2: %d\n", inc())fmt.Printf("inc 3: %d\n", inc())fmt.Printf("dec 1: %d\n", dec())fmt.Printf("dec 2: %d\n", dec())fmt.Printf("dec 3: %d\n", dec())}```輸出:```inc 1: 1inc 2: 2inc 3: 3dec 1: 2dec 2: 1dec 3: 0```可以清楚看到,兩個函數共用同一個狀態,都能管理狀態,就像你用含有兩個方法的 object 做的一樣。 當然我不是鼓勵大家隨便用帶一些變數的函數,struct 的存在就是為了給不同類型組合命名,賦予含義的。和函數使用一樣,只有一堆鬆散的函數在大部分情況下(比如帶資料的情況)是很糟糕的。因為 Go 的確把函數作為第一等公民,struct 中有很多含有函數的欄位,類比方法的行為,來代表一組操作共有狀態的函數。但是這樣很難用而且容易出錯,比如存在調用未初始化的欄位/方法的可能(凡是用 C 編碼的人都能明白這個問題,和將造成的後果)。一個日曆操作:```gopackage mainimport "fmt"type Calculator struct {Add func(int,int) intSub func(int,int) int}func newCalculator() Calculator {return Calculator{Add: func(a int, b int) int {return a + b},Sub: func(a int, b int) int {return a - b},}}func main() {calc := newCalculator()fmt.Println(calc.Add(3, 2))fmt.Println(calc.Sub(3, 2))}```嗯,你可以爭論代碼冗繁的問題,以你的經驗來看這種方式可能比 Go 使用方法更好。但是你不可否認這給犯錯誤留了很大空間。比如下面這個:```gopackage mainimport "fmt"type Calculator struct {Add func(int,int) intSub func(int,int) int}func newCalculator() Calculator {return Calculator{Add: func(a int, b int) int {return a + b},Sub: func(a int, b int) int {return a - b},}}func main() {var calc Calculatorfmt.Println(calc.Add(3, 2))fmt.Println(calc.Sub(3, 2))}```輸出結果是:```panic: runtime error: invalid memory address or nil pointer dereference[signal SIGSEGV: segmentation violation code=0xffffffff addr=0x0 pc=0xc64e8]goroutine 1 [running]:main.main()/tmp/sandbox772959961/main.go:23 +0x28```雖然使用方法也可能出現這個問題,但給類型添加函數比代表函數組合操作同一個類型要安全好多倍。至少調用方法永遠是安全的(當然你也可能遇到一個無效的方法接受者,使程式崩潰)。除了繁瑣和容易出錯,還有一個問題就是怎樣表達抽象概念,這比單獨的函數複雜多了。## 抽象我們目前的所有抽象方法都是由一個函數組成的,一個函數就可以表示,但是如果抽象需求多於一個函數怎麼辦呢?如果沒辦法表達,你只能在一個函數裡合并抽象操作,那也太恐怖了(想象一下 read/write 抽象模型就在同一個函數裡的情形)。上面 calculator 例子裡提供了一種類比方法的方式,達到這樣的程度,就是人們看怎麼使用 Calculator 的時候看不出來那些方法其實根本不是方法。但是有個重要的概念沒有了,一個在 Go 的方法裡很基礎的概念,你怎麼去表示你需要一組函數,不用定義誰去實現,或者怎樣實現?完整的說下,給出一個函數 X,需要一組函數 Y,你如何在文法上表達出一個 Z 類型實現了這組被需要的 Y 函數,因此可以作為 X 函數使用的可行方案?一個解決辦法是使用**安全**多態。我希望能對同一組無縫互動的函數有多種不同的實現。多態的重點在**安全**。我就 C 的多態分享下我的觀點,C 的多態是可行的,也啟動並執行不錯,但是絕對不安全。你可能會反對說沒有實現是絕對安全的,但是最起碼要比 C 安全,這也是大部分語言比如 Java 和 Python 在開發之初做到的。安全是很重要的,因為 calculator 的例子可能會用來實現這樣的形式。我們可以這樣做:```gotype Calculator struct {Add func(int,int) intSub func(int,int) int}func codeThatDependsOnCalculator(c Calculator) {// etc}```這將允許一個 **Calculator** 的 N 個不同的實現與依賴它的代碼整合,但是不安全。很簡單,只完成一半的實現就能瞞天過海了。所有接收 **Calculator** 的函數都要檢查 **Add** 和 **Sub** 不是 nil 的。這和 C 裡面實現的太像了,這個工作顯然是編譯器能幫你做的(在 C 裡面你可以用宏定義)。Go 的解決方案是用介面,在我看來這是 Go 裡最棒的特性。鑒於這篇博文已經很長了,關於介面的思想演變我將在後續博文中討論。祝探索 Go 的過程愉快;-)。## 致謝特別感謝:- [i4k](https://github.com/tiago4orion)- [vitorarins](https://github.com/vitorarins)- [cadicallegari](https://github.com/cadicallegari)感謝諸位花時間幫我 review 並且指出了一些低級的錯誤。
via: https://katcipis.github.io/blog/exploring-go-objects/
作者:TIAGO KATCIPIS 譯者:ArisAries 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
1423 次點擊