這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。在 Go 中,當涉及到物件導向編程,會有許多的前期工作需要做,以至於許多剛從其它語言遷移到 Go 的程式員會將那些語言中的一些概念帶到 Go 中。物件建構器就是這麼一個存在於許多其它語言中而無法在 Go 中找到的概念。## 為什麼需要構造器在 Go 中,有些對象需要初始化,比如 channel 和 slice 這兩個很容易想到的例子。這個初始化的過程通過調用 `make` 函數來執行。> make 這個內建函數為且僅為 slice, map 以及 chan 類型的對象分配以及初始化。和 new 一樣,函數的第一個參數是一個類型而不是值。與 new 不同的是,make 函數返回的是與參數一樣的類型而不是參數類型的指標。當然我們還需要預設參數,所以在 Go 中分配本身並不是我們需要建構函式的唯一原因(事實上其它語言也一樣)。由於結構體的定義不允許設定預設值,因此我們需要一個能夠設定預設值的函數。## 原生的構造器提供構造器的一個通用的慣例是提供 `New` 或者 `NewStructName` 的函數,其中 StructName 是建構函式的傳回值。通常而言,`New` 函數用於比較小的能夠自予且只匯出一個結構體的包中,比如 [fatih/structs](https://godoc.org/github.com/fatih/structs#New)。如果你想在一個包中建立多個對象,那麼你最好去看看標準庫中的 `time` 包,比如 [time.NewTicker()](https://golang.org/pkg/time/#NewTicker) 以及 [time.NewTimer()](https://golang.org/pkg/time/#NewTicker)然而 `time` 包匯出了許多其它的結構體,為何卻沒有這樣的構造器?那些沒有分配符或者無需設定預設值的結構體提可以完全忽略構造器,這些結構體提供構造器僅僅是為了符合編碼的習慣,或者為一些準備長期使用的 API 預留功能以防將來需要用到構造器。當然,也有許多常用的構造器並沒有遵循這些方針,比如 `time.Now` 提供了預設值,但它的命名是描述性的,這樣當你閱讀代碼的時候你就知道你期望的是一個目前時間的時間戳記,而不是 Unix 紀元之類的值。## 值接收器以下就是我所建議的值接收器構造器的方式:```gotype Person struct {name string}func (Person) New(name string) *Person {return &Person{name}}```你可以通過調用 `Person{}.New("Tit Petric")` 的方式來使用這個構造器,並得到一個初始化後的對象。事實上,我們可以更好地理解我們正在編寫的代碼,因為我們可以使用一個 `Person` 對象(或者一個指向它的指標),因為這就是我們開始的內容。## 實際驗證我想說的是與使用普通的函數構造器相比,使用值接收器的構造器並不會產生效能或者記憶體的問題。你會相信我所說的嗎?那麼讓我用 benchmark 來去除你的舊觀念吧。```New 2000000 754 ns/op 0 B/op 0 allocs/opPerson.New 2000000 786 ns/op 0 B/op 0 allocs/op```你也可以自己運行[測試案例](https://play.golang.org/p/injCAoxZpVg)(將代碼拷貝到你本地的 `main.go` 檔案中並執行 `go run main.go` - 由於已耗用時間的限制,你無法在 playground 中運行這段代碼)。依然覺得證據不夠充分?讓我們以這個[小例子](https://play.golang.org/p/F4xsmeGwy5d)為例並匯出彙編代碼。將其儲存到 `main2.go` 中並執行 `go tool compile -S main2.go > main.s` 。查看新產生的 `main.s` 檔案中的彙編代碼。我們的構造器的調用主要在第 21 行 和第 25 行:```asm(main2.go:21) MOVQ AX, (SP)(main2.go:21) PCDATA $0, $0(main2.go:21) CALL runtime.newobject(SB)(main2.go:21) MOVQ 8(SP), AX(main2.go:21) MOVQ $10, 8(AX)(main2.go:21) MOVL runtime.writeBarrier(SB), CX(main2.go:21) TESTL CX, CX(main2.go:21) JNE 312(main2.go:21) LEAQ go.string."Tit Petric"(SB), CX(main2.go:21) MOVQ CX, (AX)```以及```asm(main2.go:25) MOVQ AX, (SP)(main2.go:25) PCDATA $0, $0(main2.go:25) CALL runtime.newobject(SB)(main2.go:25) MOVQ 8(SP), AX(main2.go:25) MOVQ $10, 8(AX)(main2.go:25) MOVL runtime.writeBarrier(SB), CX(main2.go:25) TESTL CX, CX(main2.go:25) JNE 279(main2.go:25) LEAQ go.string."Tit Petric"(SB), CX(main2.go:25) MOVQ CX, (AX)```兩段構造器的代碼完全一致,且在兩個例子中函數都是完全內聯的,所以無論你用哪種方式寫得到的彙編代碼都是一樣的。對於 "inlining" 不熟悉的同學:> 在電腦科學中,內嵌函式是指用於告訴編譯器它應該在一個特定的函數上執行線上擴充的一種程式設計語言概念。換句話說,編譯器會把函數的內容作為一個整體插入到每個調用該函數的地方。在我們的例子中,這意味著在彙編代碼只在構造器內部執行一次分配動作。值接收器並沒有申明一個變數來接收值,編譯器已經對其做了充分的最佳化。## 結論值接收器構造器已經很接近於其他語言中的構造器了,儘管它看起來有些笨拙。在 PHP 中執行 `new Person(...)` 將會調用構造器並返回一個該類型的對象,而 Go 可以使用 `Person{}.New()` 並實現更強大的功能。類似於這樣的構造器:1. 可以使用多個參數(與 PHP, Java 等相同),2. 可以返回多個值, 以便適應 Go 的錯誤處理方式包括標準庫在內的許多包已經提供 `New` 函數。我無法找出所有的提供該功能的包,但 `errors` 包是容易想到的其中之一。對於第二種形式的例子,Google Youtube APIs 提供了 `New(*http.Client) (*Service, error)` 給大家使用。這樣的例子比比皆是。當你在寫自己的應用時,公認的思想是,除非你有明確的理由,否則不應該建立子包。更加戲劇性的做法是根據你的每個結構體建立一個包,這樣就可以為其提供各自的 `New` 構造器了。在這個例子中第二種方法比較合適。我知道肯定會有其它的一些顧慮:* 一旦你選擇了這種構造器,那麼值接收器將會和 `Person` 的執行個體綁定,且你不會去定義全域的 `NewPerson`,* 在 PHP 中與此類似的是 `Person::New()` 這樣的東西,而不是 `__constructor`。沒有全域調用。* Go 編譯器完全最佳化了來自值接收器的隱含開銷/分配。事實上從編譯後的彙編代碼來看無論用哪種方式都沒有什麼區別,正如上面的例子所示。* 這違背了命令式編程或者過程式編程的思想。你會認為 `Person{}` 會執行一次分配動作,然後在執行值接收器時會丟棄該次分配並被記憶體回收行程回收。Go 編譯器很聰明,事實上它非常聰明,如上所示,它完全最佳化了這個過程。編譯器太過於聰明了。* 你可以同時使用值接收器構造器和在合適的場合使用構造器函數。這個[例子](https://github.com/titpetric/go-web-crontab/blob/master/crontab/crontab.go)同時提供了 `New()` 和一個 `Crontab{}.New()` 構造器,其它的結構體則提供值接收器構造器。* 無論你申明了多少結構體,你都可以給每個結構體建立一個 `New` 構造器。當然了,如果你有多種建立對象的方法,你就無需只使用一種構造器。比如 [context 包](https://golang.org/pkg/context/)提供了四種構造器。** `TODO` 和 `Background` 不會再每次調用時返回新的值。
via: https://scene-si.org/2018/03/08/an-argument-for-value-receiver-constructors/
作者:Tit Petric 譯者:killernova 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
335 次點擊