學會了 Method 的基本定義和使用後,我們讨論一點更加複雜的話題。
1. 問題引入
type Person struct {
Name string
Age int
}
// 定義 Person 的一個方法 hello
func (p Person) hello() {
fmt.Printf("Hello, my name is %s. I'm %d years old\n", p.Name, p.Age)
}
func (p Person) grow() {
p.Age += 1
我們繼續使用上一篇的結構體定義。不過這裡新增加了一個新的方法 grow,它的目的是讓 Person 對象的年齡增加 1 歲。
好了,接下來寫一個測試程式:
allen := Person{"allen", 19}
allen.hello() // Output: Hello, my name is allen. I'm 19 years old
allen.grow()
allen.hello() // Output: Hello, my name is allen. I'm 19 years old
很遺憾,調用 grow 方法後,我們發現 allen 的年齡并沒有變為 20 歲。
2. 指針接收器
前面問題的主要原因在于 Golang 中的函數傳參都是值拷貝。當你調用
allen.grow()
時,實際上是把 allen 對象複制了一份,将其『副本』傳遞給了 grow 方法的接收器,是以在 grow 内部修改的對象也是『副本』。
要想真正達到修改原對象的目的,接收器類型必須為該類型的指針。
我們稍加修改 grow 方法:
// 接收器類型是 Person 指針類型
func (p *Person) grow() {
p.Age += 1
如何調用呢?你可以這樣:
allen.hello() // Output: Hello, my name is allen. I'm 19 years old
(&allen).grow()
allen.hello() // Output: Hello, my name is allen. I'm 20 years old
不過上面的寫法太笨拙,go 語言允許更加簡潔的寫法:
allen.hello() // Output: Hello, my name is allen. I'm 19 years old
allen.grow() // 直接使用 allen 對象調用指針接收器的方法
allen.hello() // Output: Hello, my name is allen. I'm 20 years old
Go 會自動的将其轉換成
(&allen).grow()
,而不需要你顯式的這樣去寫。
3. 類型和指針類型混用
接收器的實參是類型 T 和 *T,而接收器參數的類型也可以是 T 和 *T,這一組合,會出現 4 種情況
3.1 普通對象調用普通接收器方法
這種情況是我們所熟知的。
- 簡記為
(說明:左邊的 T 是接收器實參類型,右邊括号裡的 T 是接收器參數類型)T -> (T)
allen := Person{"allen", 19}
allen.hello()
3.2 對象指針調用指針接收器方法
- 簡記為
*T -> (*T)
allenptr := new(Person) // 等價于:allenptr := &Person{}
3.3 普通對象調用指針接收器方法
這種形式第 2 節已經講過了。
- 簡記為
T -> (*T)
allen := Person{"allen", 19}
(&allen).grow() // OK,這相當于 3.2 節
allen.grow() // OK,Go 會幫我們引式轉換成上面的形式
3.4 指針對象調用普通接收器方法
- 簡記為
*T -> (T)
allenptr := &Person{"allen", 19}
(*allenptr).hello() // OK,這相當于 3.1 節,使用 T -> (T)
allenptr.hello() // OK,Go 會幫我們引式轉換成上面的形式
3.5 小結
看這到裡是不是感覺有點繞?不用擔心,我簡單總結一下:
- 不用再關心接收器的實參類型了
- 隻關心接收器形參類型
- 實參無論是普通對象還是對象指針,都可以直接調用任何方法,Go 會自動幫你識别
這樣一來你會輕松很多。唯一需要注意就是,形參為指針類型的方法,操作的是原始對象。
4. 幾個特殊的例子
type Second int
func (s *Second) double() {
(*s) *= 2 // 直接修改值也是可以的
}
func (s Second) show() {
fmt.Printf("I'm %d\n", s)
}
s := Second(2)
s.show() // Output: I'm 2
s.double()
s.show() // Output: I'm 4
Second(5).show() // Output: I'm 5
Second(10).double() // Not OK! 編譯錯誤。無法擷取字面量位址。
(&Second(10)).double() // Not OK! 編譯錯誤。無法擷取字面量位址。
注意上面定義了一個底層類型為 int 的新類型 Second. 使用字面量可以調用普通類型接收器的方法 show,但是不可以調用指針接收器類型的方法 double,因為你無法擷取字面量的位址。字面量是一個臨時變量。
但是對于 Person 類型來說,下面的調用是合法的:
(&Person{"allen", 19}).grow() // OK,等價于 allen := new(Person); allen.grow()
因為 golang 對于結構體類型使用
&T{}
這種操作來說,相當于調用
new(T)
. (很遺憾的是使用 new 沒辦法為結構體類型賦初始,new 隻能賦予零值。是以使用
&T{...}
文法更加常見)下面的調用是非法的:
(Person{"allen", 19}).grow() // Not OK! 無法擷取字段量位址
5. nil 指針作為接收器參數
type LinkList struct {
value int
next *LinkList
}
// 計算連結清單長度
func (l *LinkList) Size() int {
// 先判斷接收器是否為 nil
if l == nil {
return 0
}
size := 0
for l != nil {
size += 1
l = l.next
}
return
list := LinkList{1, &LinkList{2, &LinkList{3, &LinkList{4, nil}}}}
fmt.Printf("list size: %d\n", list.Size()) // Output: list size: 4
var emptyList *LinkList
fmt.Printf("emptyList size: %d\n", emptyList.Size()) // Output: emptyList size: 0
6. 總結
- 掌握指針接收器方法
- 了解指針接收器方法和普通類型接收器方法的差別
- 知道如何處理接收器為 nil 的特殊情況