1. 程式人生 > 其它 >理解go interface看這一篇(實踐檢驗真理)

理解go interface看這一篇(實踐檢驗真理)

技術標籤:Golang夢工廠go語言go介面

前言

我想,對於各位使用面向物件程式設計的程式設計師來說,"介面"這個名詞一定不陌生,比如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指標指向的poniternil

問題三

這段程式碼的執行結果是什麼?


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,我拉你入群。歡迎各位的關注,我們下期見~~~

推薦往期文章: