理解Go Interface
1 概述
Go語言中的接口很特別,而且提供了難以置信的一系列靈活性和抽象性。接口是一個自定義類型,它是一組方法的集合,要有方法為接口類型就被認為是該接口。從定義上來看,接口有兩個特點:
- 接口本質是一種自定義類型,因此不要將Go語言中的接口簡單理解為C++/Java中的接口,后者僅用于聲明方法簽名。
- 接口是一種特殊的自定義類型,其中沒有數據成員,只有方法(也可以為空)。
接口是完全抽象的,因此不能將其實例化。然而,可以創建一個其類型為接口的變量,它可以被賦值為任何滿足該接口類型的實際類型的值。接口的重要特性是:
- 只要某個類型實現了接口所有的方法,那么我們就說該類型實現了此接口。該類型的值可以賦給該接口的值。
- 作為1的推論,任何類型的值都可以賦值給空接口interface{}。
接口的特性是Go語言支持鴨子類型的基礎,即“如果它走起來像鴨子,叫起來像鴨子(實現了接口要的方法),它就是一只鴨子(可以被賦值給接口的值)”。憑借接口機制和鴨子類型,Go語言提供了一種有利于類、繼承、模板之外的更加靈活強大的選擇。只要類型T的公開方法完全滿足接口I的要求,就可以把類型T的對象用在需要接口I的地方。這種做法的學名叫做”Structural Typing“。
2 方法
Go語言中同時有函數和方法。一個方法就是一個包含了接受者的函數,接受者可以是命名類型或者結構體類型的一個值或者是一個指針。所有給定類型的方法屬于該類型的方法集。
type User struct {Name stringEmail string
}func (u User) Notify() error// User 類型的值可以調用接受者是值的方法
damon := User{"AriesDevil", "ariesdevil@xxoo.com"}
damon.Notify()// User 類型的指針同樣可以調用接受者是值的方法
alimon := &User{"A-limon", "alimon@ooxx.com"}
alimon.Notify()
User
的結構體類型,定義了一個該類型的方法叫做Notify
,該方法的接受者是一個User
類型的值。要調用Notify
方法我們需要一個?User
類型的值或者指針。Go調用和解引用指針使得調用可以被執行。注意,當接受者不是一個指針時,該方法操作對應接受者的值的副本(意思就是即使你使用了指針調用函數,但是函數的接受者是值類型,所以函數內部操作還是對副本的操作,而不是指針操作。
我們可以修改Notify
方法,讓它的接受者使用指針類型:
func (u *User) Notify() error
再來一次之前的調用(注意:當接受者是指針時,即使用值類型調用那么函數內部也是對指針的操作。
總結:
- 一個結構體的方法的接收者可能是類型值或指針
- 如果接收者是值,無論調用者是類型值還是類型指針,修改都是值的副本
- 如果接收者是指針,則調用者修改的是指針指向的值本身。
3 接口實現
type Notifier interface {Notify() error
}func SendNotification(notify Notifier) error {return notify.Notify()
}unc (u *User) Notify() error {log.Printf("User: Sending User Email To %s<%s>\n",u.Name,u.Email)return nil
}func main() {user := User{Name: "AriesDevil",Email: "ariesdevil@xxoo.com",}SendNotification(user)
}// Output:
cannot use user (type User) as type Notifier in function argument:
User does not implement Notifier (Notify method has pointer receiver)
上述代碼是編譯不過的,見Output,編譯錯誤關鍵信息Notify method has pointer receiver
。 編譯器不考慮我們的值是實現該接口的類型,接口的調用規則是建立在這些方法的接受者和接口如何被調用的基礎上。下面的是語言規范里定義的規則,這些規則用來說明是否我們一個類型的值或者指針實現了該接口:
- 類型?
*T
?的可調用方法集包含接受者為?*T
?或?T
?的所有方法集 - 類型?
T
?的可調用方法集包含接受者為?T
?的所有方法 - 類型?
T
?的可調用方法集不包含接受者為?*T
?的方法
也就是說:
- 接收者是指針?
*T
?時,接口的實例必須是指針 - 接收者是值?
T
?時,接口的實例可以是指針也可以是值
4 空接口與nil
空接口(interface{}
)不包含任何的method,正因為如此,所有的類型都實現了interface{}
。interface{}
對于描述起不到任何的作用(因為它不包含任何的method),但是interface{}
在我們需要存儲任意類型的數值的時候相當有用,因為它可以存儲任意類型的數值。它有點類似于C語言的void*
類型。
Go語言中的nil在概念上和其它語言的null、None、nil、NULL一樣,都指代零值或空值。nil是預先說明的標識符,也即通常意義上的關鍵字。nil只能賦值給指針、channel、func、interface、map或slice類型的變量。如果未遵循這個規則,則會引發panic。
在底層,interface作為兩個成員來實現,一個類型(type)和一個值(data)。參考官方文檔翻譯Go中error類型的nil值和nil。
import ("fmt""reflect"
)func main() {var val interface{} = int64(58)fmt.Println(reflect.TypeOf(val))val = 50fmt.Println(reflect.TypeOf(val))
}
type用于存儲變量的動態類型,data用于存儲變量的具體數據。在上面的例子中,第一條打印語句輸出的是:int64。這是因為已經顯示的將類型為int64的數據58賦值給了interface類型的變量val,所以val的底層結構應該是:(int64, 58)。我們暫且用這種二元組的方式來描述,二元組的第一個成員為type,第二個成員為data。第二條打印語句輸出的是:int。這是因為字面量的整數在golang中默認的類型是int,所以這個時候val的底層結構就變成了:(int, 50)。
func main() {var val interface{} = nilif val == nil {fmt.Println("val is nil")} else {fmt.Println("val is not nil")}
}
變量val是interface類型,它的底層結構必然是(type, data)。由于nil是untyped(無類型),而又將nil賦值給了變量val,所以val實際上存儲的是(nil, nil)。因此很容易就知道val和nil的相等比較是為true的。
進一步驗證:
func main() {var val interface{} = (*interface{})(nil)if val == nil {fmt.Println("val is nil")} else {fmt.Println("val is not nil")}
}
(*interface{})(nil)
是將nil轉成interface類型的指針,其實得到的結果僅僅是空接口類型指針并且它指向無效的地址。也就是空接口類型指針而不是空指針,這兩者的區別蠻大的。
對于(*int)(nil)
、(*byte)(nil)
等等來說是一樣的。上面的代碼定義了接口指針類型變量val,它指向無效的地址(0x0),因此val持有無效的數據。但它是有類型的(*interface{})
。所以val的底層結構應該是:(*interface{}, nil)
。
有時候您會看到(*interface{})(nil)
的應用,比如var ptrIface = (*interface{})(nil)
,如果您接下來將ptrIface指向其它類型的指針,將通不過編譯。或者您這樣賦值:*ptrIface = 123
,那樣的話編譯是通過了,但在運行時還是會panic的,這是因為ptrIface指向的是無效的內存地址。其實聲明類似ptrIface這樣的變量,是因為使用者只是關心指針的類型,而忽略它存儲的值是什么。
小結: 無論該指針的值是什么:(*interface{}, nil)
,這樣的接口值總是非nil的,即使在該指針的內部為nil。
5 接口變量存儲的類型
接口的變量里面可以存儲任意類型的數值(該類型實現了某interface)。那么我們怎么反向知道這個變量里面實際保存了的是哪個類型的對象呢?目前常用的有兩種方法:
comma-ok斷言
value, ok = element.(T),這里value就是變量的值,ok是一個bool類型,element是interface變量,T是斷言的類型。如果element里面確實存儲了T類型的數值,那么ok返回true,否則返回false。
switch測試
switch value := element.(type) {case int:fmt.Printf("list[%d] is an int and its value is %d\n", index, value)case string:fmt.Printf("list[%d] is a string and its value is %s\n", index, value)...
element.(type)語法不能在switch外的任何邏輯里面使用,如果你要在switch外面判斷一個類型就使用comma-ok。
6 接口與反射
反射是程序運行時檢查其所擁有的結構,尤其是類型的一種能力。Go語言也提供對反射的支持。
在前面的interface{}與nil
的底層實現已提到,在reflect
包中有兩個類型需要了解:Type
和Value
。這兩個類型使得可以訪問接口變量的內容,還有兩個簡單的函數,reflect.TypeOf
和reflect.ValueOf
,從接口值中分別獲取reflect.Type
?和reflect.Value
。
如同物理中的反射,在Go語言中的反射也存在它自己的鏡像。從reflect.Value
可以使用Interface
方法還原接口值:
var x float64 = 3.4
v := reflect.ValueOf(x)// Interface 以 interface{} 返回 v 的值。
// func (v Value) Interface() interface{}// y 將為類型 float64
y := v.Interface().(float64)
fmt.Println(y)
聲明:本文是收集網上一些關于Go語言中接口(interface)的說明,是一篇學習筆記,文中多處引用,參考文章列表在最后,可直接訪問了解詳情。
參考:
[1]?Go 語言中的方法,接口和嵌入類型
[2]?詳解interface和nil
[3]?Go語言interface詳解