理解go interface看這一篇(實踐檢驗真理)
前言
我想,對於各位使用面向物件程式設計的程式設計師來說,"介面"這個名詞一定不陌生,比如java中的介面以及c++中的虛基類都是介面的實現。但是
golang
中的介面概念確與其他語言不同,有它自己的特點,下面我們就來一起解密。
定義
Go 語言中的介面是一組方法的簽名,它是 Go 語言的重要組成部分。簡單的說,interface是一組method簽名的組合,我們通過interface來定義物件的一組行為。interface 是一種型別,定義如下:
type Person interface {
Eat(food string )
}
它的定義可以看出來用了 type 關鍵字,更準確的說 interface 是一種具有一組方法的型別,這些方法定義了 interface 的行為。golang
介面定義不能包含變數,但是允許不帶任何方法,這種型別的介面叫empty interface
。
如果一個型別實現了一個interface
中所有方法,我們就可以說該型別實現了該interface
,所以我們我們的所有型別都實現了empty interface
,因為任何一種型別至少實現了0個方法。並且go
中並不像java
中那樣需要顯式關鍵字來實現interface
,只需要實現interface
包含的方法即可。
實現介面
這裡先拿java
語言來舉例,在java
interface
需要這樣宣告:
public class MyWriter implments io.Writer{}
這就意味著對於介面的實現都需要顯示宣告,在程式碼編寫方面有依賴限制,同時需要處理包的依賴,而在Go
語言中實現介面就是隱式的,舉例說明:
type error interface {
Error() string
}
type RPCError struct {
Code int64
Message string
}
func (e *RPCError) Error() string {
return fmt.Sprintf("%s, code=%d" , e.Message, e.Code)
}
上面的程式碼,並沒有error
介面的影子,我們只需要實現Error() string
方法就實現了error
介面。在Go
中,實現介面的所有方法就隱式地實現了介面。我們使用上述 RPCError
結構體時並不關心它實現了哪些介面,Go 語言只會在傳遞引數、返回引數以及變數賦值時才會對某個型別是否實現介面進行檢查。
Go
語言的這種寫法很方便,不用引入包依賴。但是interface
底層實現的時候會動態檢測也會引入一些問題:
- 效能下降。使用interface作為函式引數,runtime 的時候會動態的確定行為。使用具體型別則會在編譯期就確定型別。
- 不能清楚的看出struct實現了哪些介面,需要藉助ide或其它工具。
兩種介面
這裡大多數剛入門的同學肯定會有疑問,怎麼會有兩種介面,因為Go
語言中介面會有兩種表現形式,使用runtime.iface
表示第一種介面,也就是我們上面實現的這種,介面中定義方法;使用runtime.eface
表示第二種不包含任何方法的介面,第二種在我們日常開發中經常使用到,所以在實現時使用了特殊的型別。從編譯角度來看,golang並不支援泛型程式設計。但還是可以用interface{}
來替換引數,而實現泛型。
interface內部結構
Go 語言根據介面型別是否包含一組方法將介面型別分成了兩類:
- 使用
runtime.iface
結構體表示包含方法的介面 - 使用
runtime.eface
結構體表示不包含任何方法的interface{}
型別;
runtime.iface
結構體在Go
語言中的定義是這樣的:
type eface struct { // 16 位元組
_type *_type
data unsafe.Pointer
}
這裡只包含指向底層資料和型別的兩個指標,從這個type
我們也可以推斷出Go語言的任意型別都可以轉換成interface
。
另一個用於表示介面的結構體是 runtime.iface
,這個結構體中有指向原始資料的指標 data
,不過更重要的是 runtime.itab
型別的 tab
欄位。
type iface struct { // 16 位元組
tab *itab
data unsafe.Pointer
}
下面我們一起看看interface
中這兩個型別:
runtime_type
runtime_type
是 Go 語言型別的執行時表示。下面是執行時包中的結構體,其中包含了很多型別的元資訊,例如:型別的大小、雜湊、對齊以及種類等。
type _type struct {
size uintptr
ptrdata uintptr
hash uint32
tflag tflag
align uint8
fieldAlign uint8
kind uint8
equal func(unsafe.Pointer, unsafe.Pointer) bool
gcdata *byte
str nameOff
ptrToThis typeOff
}
這裡我只對幾個比較重要的欄位進行講解:
-
size
欄位儲存了型別佔用的記憶體空間,為記憶體空間的分配提供資訊; -
hash
欄位能夠幫助我們快速確定型別是否相等; -
equal
欄位用於判斷當前型別的多個物件是否相等,該欄位是為了減少 Go 語言二進位制包大小從typeAlg
結構體中遷移過來的); -
runtime_itab
runtime.itab
結構體是介面型別的核心組成部分,每一個 runtime.itab
都佔 32 位元組,我們可以將其看成介面型別和具體型別的組合,它們分別用 inter
和 _type
兩個欄位表示:
type itab struct { // 32 位元組
inter *interfacetype
_type *_type
hash uint32
_ [4]byte
fun [1]uintptr
}
inter
和_type
是用於表示型別的欄位,hash
是對_type.hash
的拷貝,當我們想將 interface
型別轉換成具體型別時,可以使用該欄位快速判斷目標型別和具體型別 runtime._type
是否一致,fun
是一個動態大小的陣列,它是一個用於動態派發的虛擬函式表,儲存了一組函式指標。雖然該變數被宣告成大小固定的陣列,但是在使用時會通過原始指標獲取其中的資料,所以 fun
陣列中儲存的元素數量是不確定的;
內部結構就做一個簡單介紹吧,有興趣的同學可以自行深入學習。
空的interface(runtime.eface
)
前文已經介紹了什麼是空的interface
,下面我們來看一看空的interface
如何使用。定義函式入參如下:
func doSomething(v interface{}){
}
這個函式的入參是interface
型別,要注意的是,interface
型別不是任意型別,他與C語言中的void *
不同,如果我們將型別轉換成了 interface{}
型別,變數在執行期間的型別也會發生變化,獲取變數型別時會得到 interface{}
,之所以函式可以接受任何型別是在 go 執行時傳遞到函式的任何型別都被自動轉換成 interface{}
。
那麼我們可以才來一個猜想,既然空的 interface 可以接受任何型別的引數,那麼一個 interface{}
型別的 slice 是不是就可以接受任何型別的 slice ?下面我們就來嘗試一下:
import (
"fmt"
)
func printStr(str []interface{}) {
for _, val := range str {
fmt.Println(val)
}
}
func main(){
names := []string{"stanley", "david", "oscar"}
printStr(names)
}
執行上面程式碼,會出現如下錯誤:./main.go:15:10: cannot use names (type []string) as type []interface {} in argument to printStr
。
這裡我也是很疑惑,為什麼Go
沒有幫助我們自動把slice
轉換成interface
型別的slice
,之前做專案就想這麼用,結果失敗了。後來我終於找到了答案,有興趣的可以看看原文,這裡簡單總結一下:interface
會佔用兩個字長的儲存空間,一個是自身的 methods 資料,一個是指向其儲存值的指標,也就是 interface 變數儲存的值,因而 slice []interface{} 其長度是固定的N*2
,但是 []T 的長度是N*sizeof(T)
,兩種 slice 實際儲存值的大小是有區別的。
既然這種方法行不通,那可以怎樣解決呢?我們可以直接使用元素型別是interface的切片。
var dataSlice []int = foo()
var interfaceSlice []interface{} = make([]interface{}, len(dataSlice))
for i, d := range dataSlice {
interfaceSlice[i] = d
}
非空interface
Go
語言實現介面時,既可以結構體型別的方法也可以是使用指標型別的方法。Go
語言中並沒有嚴格規定實現者的方法是值型別還是指標,那我們猜想一下,如果同時使用值型別和指標型別方法實現介面,會有什麼問題嗎?
先看這樣一個例子:
package main
import (
"fmt"
)
type Person interface {
GetAge () int
SetAge (int)
}
type Man struct {
Name string
Age int
}
func(s Man) GetAge()int {
return s.Age
}
func(s *Man) SetAge(age int) {
s.Age = age
}
func f(p Person){
p.SetAge(10)
fmt.Println(p.GetAge())
}
func main() {
p := Man{}
f(&p)
}
看上面的程式碼,大家對f(&p)
這裡的入參是否會有疑問呢?如果不取地址,直接傳過去會怎麼樣?試了一下,編譯錯誤如下:./main.go:34:3: cannot use p (type Man) as type Person in argument to f: Man does not implement Person (SetAge method has pointer receiver)
。透過註釋我們可以看到,因為SetAge
方法的receiver
是指標型別,那麼傳遞給f
的是P
的一份拷貝,在進行p
的拷貝到person
的轉換時,p
的拷貝是不滿足SetAge
方法的receiver
是個指標型別,這也正說明一個問題go中函式都是按值傳遞。
上面的例子是因為發生了值傳遞才會導致出現這個問題。實際上不管接收者型別是值型別還是指標型別,都可以通過值型別或指標型別呼叫,這裡面實際上通過語法糖起作用的。實現了接收者是值型別的方法,相當於自動實現了接收者是指標型別的方法;而實現了接收者是指標型別的方法,不會自動生成對應接收者是值型別的方法。
舉個例子:
type Animal interface {
Walk()
Eat()
}
type Dog struct {
Name string
}
func (d *Dog)Walk() {
fmt.Println("go")
}
func (d *Dog)Eat() {
fmt.Println("eat shit")
}
func main() {
var d Animal = &Dog{"nene"}
d.Eat()
d.Walk()
}
上面定義了一個介面Animal
,介面定義了兩個函式:
Walk()
Eat()
接著定義了一個結構體Dog
,他實現了兩個方法,一個是值接受者,一個是指標接收者。我們通過介面型別的變數呼叫了定義的兩個函式是沒有問題的,如果我們改成這樣呢:
func main() {
var d Animal = Dog{"nene"}
d.Eat()
d.Walk()
}
這樣直接就會報錯,我們只改了一部分,第一次將&Dog{"nene"}
賦值給了d
;第二次則將Dog{"nene"}
賦值給了d
。第二次報錯是因為,d
沒有實現Animal
。這正解釋了上面的結論,所以,當實現了一個接收者是值型別的方法,就可以自動生成一個接收者是對應指標型別的方法,因為兩者都不會影響接收者。但是,當實現了一個接收者是指標型別的方法,如果此時自動生成一個接收者是值型別的方法,原本期望對接收者的改變(通過指標實現),現在無法實現,因為值型別會產生一個拷貝,不會真正影響呼叫者。
總結一句話就是:如果實現了接收者是值型別的方法,會隱含地也實現了接收者是指標型別的方法。
型別斷言
一個interface
被多種型別實現時,有時候我們需要區分interface
的變數究竟儲存哪種型別的值,go
可以使用comma,ok
的形式做區分 value, ok := em.(T)
:em 是 interface 型別的變數,T代表要斷言的型別,value 是 interface 變數儲存的值,ok 是 bool 型別表示是否為該斷言的型別 T。總結出來語法如下:
<目標型別的值>,<布林引數> := <表示式>.( 目標型別 ) // 安全型別斷言
<目標型別的值> := <表示式>.( 目標型別 ) //非安全型別斷言
看個簡單的例子:
type Dog struct {
Name string
}
func main() {
var d interface{} = new(Dog)
d1,ok := d.(Dog)
if !ok{
return
}
fmt.Println(d1)
}
這種就屬於安全型別斷言,更適合在線上程式碼使用,如果使用非安全型別斷言會怎麼樣呢?
type Dog struct {
Name string
}
func main() {
var d interface{} = new(Dog)
d1 := d.(Dog)
fmt.Println(d1)
}
這樣就會發生錯誤如下:
panic: interface conversion: interface {} is *main.Dog, not main.Dog
斷言失敗。這裡直接發生了 panic
,所以不建議線上程式碼使用。
看過fmt
原始碼包的同學應該知道,fmt.println
內部就是使用到了型別斷言,有興趣的同學可以自行學習。
問題
上面介紹了interface
的基本使用方法及可能會遇到的一些問題,下面出三個題,看看你們真的掌握了嗎?
問題一
下面程式碼,哪一行存在編譯錯誤?(多選)
type Student struct {
}
func Set(x interface{}) {
}
func Get(x *interface{}) {
}
func main() {
s := Student{}
p := &s
// A B C D
Set(s)
Get(s)
Set(p)
Get(p)
}
答案:B、D;解析:我們上文提到過,interface
是所有go
型別的父類,所以Get
方法只能介面*interface{}
型別的引數,其他任何型別都不可以。
問題二
這段程式碼的執行結果是什麼?
func PrintInterface(val interface{}) {
if val == nil {
fmt.Println("this is empty interface")
return
}
fmt.Println("this is non-empty interface")
}
func main() {
var pointer *string = nil
PrintInterface(pointer)
}
答案:this is non-empty interface
。解析:這裡的interface{}
是空介面型別,他的結構如下:
type eface struct { // 16 位元組
_type *_type
data unsafe.Pointer
}
所以在呼叫函式PrintInterface
時發生了隱式的型別轉換,除了向方法傳入引數之外,變數的賦值也會觸發隱式型別轉換。在型別轉換時,*string
型別會轉換成interface
型別,發生值拷貝,所以eface struct{}
是不為nil
,不過data
指標指向的poniter
為nil
。
問題三
這段程式碼的執行結果是什麼?
type Animal interface {
Walk()
}
type Dog struct{}
func (d *Dog) Walk() {
fmt.Println("walk")
}
func NewAnimal() Animal {
var d *Dog
return d
}
func main() {
if NewAnimal() == nil {
fmt.Println("this is empty interface")
} else {
fmt.Println("this is non-empty interface")
}
}
答案:this is non-empty interface
. 解析:這裡的interface
是非空介面iface
,他的結構如下:
type iface struct { // 16 位元組
tab *itab
data unsafe.Pointer
}
d
是一個指向nil的空指標,但是最後return d
會觸發匿名變數 Animal = p
值拷貝動作,所以最後NewAnimal()
返回給上層的是一個Animal interface{}
型別,也就是一個iface struct{}
型別。 p
為nil,只是iface
中的data 為nil而已。 但是iface struct{}
本身並不為nil.
總結
interface
在我們日常開發中使用還是比較多,所以學好它還是很必要,希望這篇文章能讓你對Go
語言的介面有一個新的認識,這一篇到這裡結束啦,我們下期見~~~。
素質三連(分享、點贊、在看)都是筆者持續創作更多優質內容的動力!
建了一個Golang交流群,歡迎大家的加入,第一時間觀看優質文章,不容錯過哦(公眾號獲取)
結尾給大家發一個小福利吧,最近我在看[微服務架構設計模式]這一本書,講的很好,自己也收集了一本PDF,有需要的小夥可以到自行下載。獲取方式:關注公眾號:[Golang夢工廠],後臺回覆:[微服務],即可獲取。
我翻譯了一份GIN中文文件,會定期進行維護,有需要的小夥伴後臺回覆[gin]即可下載。
翻譯了一份Machinery中文文件,會定期進行維護,有需要的小夥伴們後臺回覆[machinery]即可獲取。
我是asong,一名普普通通的程式猿,讓gi我一起慢慢變強吧。我自己建了一個golang
交流群,有需要的小夥伴加我vx
,我拉你入群。歡迎各位的關注,我們下期見~~~
推薦往期文章:
- machinery-go非同步任務佇列
- Leaf—Segment分散式ID生成系統(Golang實現版本)
- 十張動圖帶你搞懂排序演算法(附go實現程式碼)
- Go語言相關書籍推薦(從入門到放棄)
- go引數傳遞型別
- 手把手教姐姐寫訊息佇列
- 常見面試題之快取雪崩、快取穿透、快取擊穿
- 詳解Context包,看這一篇就夠了!!!
- go-ElasticSearch入門看這一篇就夠了(一)
- 面試官:go中for-range使用過嗎?這幾個問題你能解釋一下原因嗎
- 學會wire依賴注入、cron定時任務其實就這麼簡單!
- 聽說你還不會jwt和swagger-飯我都不吃了帶著實踐專案我就來了
- [掌握這些Go語言特性,你的水平將提高N個檔次(二)](