和多數程式語言一樣,在 Go 語言中,我們需要考慮如何設計方法。由於在 Go 語言中,方法本質上就是函數,所以我們之前講解的關於函數設計的內容同樣適用於方法,例如錯誤處理設計、針對異常的處理策略、使用 defer 提升簡潔性,等等。
Receiver(Receiver)也是設計方法時需要考量的一點。Receiver定義了一個Method所屬的類型,有點類似於其他語言中的 class 或 object 的概念,如下所示:
1 | func (t T) M1() {} |
上面範例中的 t 就是Receiver,而 M1 和 M2 是Receiver定義的Method。M1 方法代表Receiver類型為 T 的Method,而 M2 方法則代表Receiver類型為 *T 的Method。下面我們來看看不同的Receiver類型對 M1 和 M2 的影響。
當Receiver類型為 T 時:
代表 T 類型Object的Receiver參數以pass by value傳遞到 M1 方法中,實際上是 T 類型Object的副本,因此 M1 方法中對副本的任何修改操作,都不會影響原 T 類型Object。
當Receiver類型為 *T 時:
代表 *T 類型實例的Receiver參數以pass by reference傳遞到 M2 方法中,實際上是 T 類型Object的地址,因此 M2 方法可以通過該address對原 T 類型Object進行修改操作。
我們來看看一個更直觀的範例,來證明上述分析結果,並觀察 Go 方法選擇不同的Receiver類型對原類型Object的影響:
1 | package main |
在這個範例中,我們為類型 T 定義了兩個Method - M1 和 M2,其中 M1 的Receiver類型為 T,而 M2 的Receiver類型為 *T。M1 和 M2 方法都通過Receiver參數 t 修改了 t 的attribute - a。
了解了不同Receiver類型對 Go 方法的影響後,我們可以總結一下,日常寫程式中選擇Receiver參數類型時可以參考的原則:
- 如果 Go 方法需要修改Receiver代表的類型Object,並將修改反映到原類型Object上,應選擇
*T作為Receiver參數類型。 - 如果Receiver的 size 太大,應選擇
*T作為Receiver參數類型,以避免複製大Object。 - 如果需要縮小外部接觸面,盡量少暴露可以修改內部型態的方法,可以考慮使用
T作為Receiver。 - T 類型是否需要實作某個interface:
如果T類型需要實作某個interface,那我們就要使用T作為Receiver參數類型,以滿足interface類型方法集合中的所有Method。如果T不需要實作某個interface,但*T需要實作該interface,則*T的方法集合包含T的方法集合。因此,我們在確定 Go 方法的Receiver類型時,可以參考上述原則。
這裡可以解釋一下什麼是方法集合,我們先通過一個範例來直觀了解為什麼要有方法集合,以及它主要用來解決什麼問題:
1 | type Interface interface { |
運行這個範例程式,我們在 i = t 這一行會得到 Go 編譯器的錯誤提示,Go 編譯器提示我們:T 沒有實作 Interface 類型方法列表中的 M2,因此類型 T 的Object t 不能賦值給 Interface 變量。
Interface類型相對特殊,它只會列出代表interface的方法列表,不會具體定義某個方法,類似C++的virtual和Java的interface,其方法集合就是它的方法列表中的所有方法,我們可以一目了然地看到。因此,我們下面重點講解的是非interface類型的方法集合。
為了方便查看一個非interface類型的方法集合,我這裡提供了一個函數 dumpMethodSet,用於輸出一個非interface類型的方法集合:
1 | func dumpMethodSet(i interface{}) { |
下面我們利用這個函數,試著輸出一下 Go 原生類型以及自定義類型的方法集合:
1 | type T struct{} |
我們得到如下結果:
1 | int's method set is empty! |
我們看到以 int 和 *int 為代表的 Go 原生類型,由於沒有定義Method,所以它們的方法集合都是空的。自定義類型 T 定義了方法 M1 和 M2,因此其方法集合包含了 M1 和 M2,也符合我們的預期。然而,*T 的方法集合中除了預期的 M3 和 M4 外,還包含了類型 T 的方法 M1 和 M2!
這是因為,Go 語言規定,***T 類型的方法集合包含所有以 *T 為Receiver參數類型的方法,以及所有以 T 為Receiver參數類型的方法**。這就是為何 *T 類型的方法集合包含四個方法的原因,以及第一個範例會報錯的原因。
總結
- Receiver類型為
T時:方法Receiver是一個值,任何修改都不會影響原Object。 - Receiver類型為
*T時:方法Receiver是一個pointer,可以修改原Object。 - 選擇Receiver類型的原則:
- 如果需要修改原Object,使用
*T。 - 如果Receiver的 size 太大,使用
*T。 - 如果希望縮小外部接觸面,減少Method, Attribute暴露,使用
T。 - 根據是否需要實作Interface來決定使用
T還是*T。
- 如果需要修改原Object,使用
這些原則可以幫助你在 Go 程式設計中做出更好的Method設計決策。