golang的介面有啥用

2023-01-04 22:01:03

在golang中,介面是一種型別,是用來將對方法進行一個收束,其作用是:1、作為方法的收束器,進行物件導向設計;2、作為各種資料的承載者,可以用來接收函數引數等。介面的定義語法「type 介面型別名 interface{方法名( 參數列1 ) 返回值列表}」;當方法名首字母是大寫且這個介面型別名首字母也是大寫時,這個方法可以被介面所在的包(package)之外的程式碼存取。

本教學操作環境:windows7系統、GO 1.18版本、Dell G3電腦。

一、介面(interface)是什麼

interface是一組method簽名的組合,我們通過interface來定義物件的一組行為。

(注意method 和普通func的區別)

Interface是一種型別,和往常語言的介面不一樣,它只是用來將對方法進行一個收束。然而正是這種收束,使GO語言擁有了基於功能的物件導向。

介面的主要功能:

1.作為方法的收束器,進行物件導向設計。

2.作為各種資料的承載者,可以用來接收函數引數等。

這也是,GO語言提倡面向介面程式設計

二、介面的定義使用

2.1定義

類似結構體

type 介面型別名 interface{
    方法名1( 參數列1 ) 返回值列表1
    方法名2( 參數列2 ) 返回值列表2
    …
}
登入後複製

當然這只是有方法的介面定義,面向資料的介面不用。

  • 介面名:使用type將介面定義為自定義的型別名。Go語言的介面在命名時,一般會在單詞後面新增er,如有寫操作的介面叫Writer,有字串功能的介面叫Stringer等。介面名最好要能突出該介面的型別含義。

  • 方法名:當方法名首字母是大寫且這個介面型別名首字母也是大寫時,這個方法可以被介面所在的包(package)之外的程式碼存取。

  • 參數列、返回值列表:參數列和返回值列表中的引數變數名可以省略

2.2使用

一個物件只要全部實現了介面中的方法,那麼就實現了這個介面。換句話說,介面就是一個需要實現的方法列表

//定義介面
type FastfoodStore interface{
    MakeHamberger()
    MakeFriedChips()
    MakeSoftDrink()
}
//定義結構體
type KFC struct{}
type HambergerKing struct{}

//實現了介面中所有的方法
func (kfc KFC) MakeHamberger(){
    fmt.println("肯德基的漢堡")
}
func (kfc KFC) MakeFriedChips(){
    fmt.println("肯德基的薯條")
}
func (kfc KFC) MakeSoftDrink(){
    fmt.println("肯德基的飲料")
}

func (K *HambergerKing) MakeHameberger(){
    fmt.println("漢堡王的漢堡")
}
func (K *HambergerKing) MakeFriedChips(){
    fmt.println("漢堡王的薯條")
}
func (K *HambergerKing) MakeSoftDrink(){
    fmt.println("漢堡王的飲料")
}
登入後複製

我們可以看到不同於Java的介面顯式實現,Go的語言是隱式實現的。

  • 在 Java 中:實現介面需要顯式地宣告介面並實現所有方法;
  • 在 Go 中:實現介面的所有方法就隱式地實現了介面;

那麼GO語言是如何檢查該型別是否是介面呢?

答:Go 語言只會在傳遞引數、返回引數以及變數賦值時才會對某個型別是否實現介面進行檢查。從型別檢查的過程來看,編譯器僅在需要時才檢查型別,型別實現介面時只需要實現介面中的全部方法,不需要像 Java 等程式語言中一樣顯式宣告。

我們可以看到在上面實現介面的時候,KFC是用結構體物件實現的,而Hamberger king是通過指標實現的兩者有什麼不同呢?

答:區別在於我們初始化介面的時候

//結構體初始化和指標初始化
var f faststore = KFC{}             //可以通過編譯
var f faststore = &KFC{}            //可以通過編譯

var f faststore = HambergerKing{}    //無法通過編譯
var f faststore = &HambergerKing{}    //可以通過編譯
登入後複製

所以在我們使用指標進行實現,結構體初始化時,為啥不行呢?

答:Go 語言在傳遞引數時都是傳值的。

1.png

如上圖所示,無論上述程式碼中初始化的變數指標還是結構體,使用 呼叫方法時都會發生值拷貝:

如上圖左側,對於 &HambergerKing{} 來說,這意味著拷貝一個新的 &HambergerKing{} 指標,這個指標與原來的指標指向一個相同並且唯一的結構體,所以編譯器可以隱式的對變數解除參照(dereference)獲取指標指向的結構體;
如上圖右側,對於 HambergerKing{} 來說,這意味著方法會接受一個全新的 HambergerKing{},因為方法的引數是*HambergerKing,編譯器不會無中生有建立一個新的指標;即使編譯器可以建立新指標,這個指標指向的也不是最初呼叫該方法的結構體;
上面的分析解釋了指標型別的現象,當我們使用指標實現介面時,只有指標型別的變數才會實現該介面;當我們使用結構體實現介面時,指標型別和結構體型別都會實現該介面。當然這並不意味著我們應該一律使用結構體實現介面,這個問題在實際工程中也沒那麼重要,在這裡我們只想解釋現象背後的原因。

在上面我們說過,interface有兩種用法,現在介紹了其中一種就是作為方法的收束器。那麼第二種就是作為資料的承載者

2.3 資料承載者

作為資料容器時,介面就是一個「空」介面,這個空來形容沒有Method。空interface(interface{})不包含任何的method,正因為如此,所有的型別都實現了空interface。空interface對於描述起不到任何的作用(因為它不包含任何的method),但是空interface在我們需要儲存任意型別的數值的時候相當有用,因為它可以儲存任意型別的數值。它有點類似於C語言的void*型別。

需要注意的是,與 C 語言中的 void * 不同,interface{} 型別不是任意型別。如果我們將型別轉換成了 interface{} 型別,變數在執行期間的型別也會發生變化,獲取變數型別時會得到 interface{}。

我們嘗試從底層實現來解釋兩種用法的不同,你會好理解一些。Go 語言使用 runtime.iface 表示第一種介面,使用 runtime.eface 表示第二種不包含任何方法的介面 interface{},兩種介面雖然都使用 interface 宣告,但是由於後者在 Go 語言中很常見,所以在實現時使用了特殊的型別。

2.png

空介面作為函數的引數

使用空介面實現可以接收任意型別的函數引數。

// 空介面作為函數引數
func show(a interface{}) {
    fmt.Printf("type:%T value:%v\n", a, a)
}
登入後複製

空介面作為map的值

使用空介面實現可以儲存任意值的字典。

// 空介面作為map值
    var studentInfo = make(map[string]interface{})
    studentInfo["name"] = "Wilen"
    studentInfo["age"] = 18
    studentInfo["married"] = false
    fmt.Println(studentInfo)
//gin框架的gin.H{}
登入後複製

三、關於介面型別轉換

interface 可以儲存所有的值,那麼自然會涉及到型別轉換這個話題。與此同時,我們也將在這節細說型別轉換中,因為結構體實現和結構體指標實現的介面的異同。

3.1結構體指標實現介面

//我們仍然運用上面快餐店的例子
type Store interface{
    MakeHamberger()
}
type KFC struct{
    name string
}
func (k *KFC) MakeHamberger(){
    fmt.println(k.name+"製作了一個漢堡")
}
func main(){
    var s store = &KFC{name:"東街店"}
    store.MakeHamberger()
}
登入後複製

這裡將上述程式碼生成的組合指令拆分成三部分分析:

1.結構體 KFC 的初始化;

KFC的初始化又可以分為下面幾步:

  • 獲取 KFC 結構體型別指標並將其作為引數放到棧上;

  • 通過 CALL 指定呼叫 runtime.newobject函數,這個函數會以 KFC 結構體型別指標作為入參,分配一片新的記憶體空間並將指向這片記憶體空間的指標返回到 SP+8 上;

  • SP+8 現在儲存了一個指向 KFC 結構體的指標,我們將棧上的指標拷貝到暫存器 DI 上方便操作;

  • 由於 Cat 中只包含一個字串型別的 Name 變數,所以在這裡會分別將字串地址 &"東街店" 和字串長度 6 設定到結構體上。

3.png

2.賦值觸發的型別轉換過程;

因為 KFC 結構體的定義中只包含一個字串,而字串在 Go 語言中總共佔 16 位元組,所以每一個 KFC 結構體的大小都是 16 位元組。初始化 KFC 結構體之後就進入了將 *KFC 轉換成 Store 型別的過程了:

型別轉換的過程比較簡單,Store 作為一個包含方法的介面,它在底層使用 [runtime.iface] 結構體表示。runtime.iface 結構體包含兩個欄位,其中一個是指向資料的指標,另一個是表示介面和結構體關係的 tab 欄位,我們已經通過上一段程式碼 SP+8 初始化了 KFC 結構體指標,這段程式碼只是將編譯期間生成的 runtime.itab 結構體指標複製到 SP 上:

4.png

到這裡,我們會發現 SP ~ SP+16 共同組成了 runtime.iface 結構體。

3.呼叫介面的方法 Quack();

棧上的這個 runtime.iface 也是 MakeHamberger() 方法的第一個入參。通過CALL()完成方法的呼叫。

3.2 結構體實現介面

//我們仍然運用上面快餐店的例子
type Store interface{
    MakeHamberger()
}
type KFC struct{
    name string
}
func (k KFC) MakeHamberger(){
    fmt.println(k.name+"製作了一個漢堡")
}
func main(){
    var s store = KFC{name:"東街店"}
    store.MakeHamberger()
}
登入後複製

如果我們在初始化變數時使用指標型別 &KFC{Name: "東街店"} 也能夠通過編譯,不過生成的組合程式碼和上一節中的幾乎完全相同,所以這裡也就不分析這個情況了。

初始化 KFC 結構體;

在棧上初始化 KFC 結構體,而上一節的程式碼在堆上申請了 16 位元組的記憶體空間,棧上只有一個指向 KFC 的指標。

完成從 KFC 到 Store 介面的型別轉換;

初始化結構體後會進入型別轉換的階段,編譯器會將 go.itab."".KFC,"".Store 的地址和指向 KFC 結構體的指標作為引數一併傳入 runtime.convT2I 函數:這個函數會獲取 runtime.itab 中儲存的型別,根據型別的大小申請一片記憶體空間並將 elem 指標中的內容拷貝到目標的記憶體中:

func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
    t := tab._type
    x := mallocgc(t.size, t, true)
    typedmemmove(t, x, elem)
    i.tab = tab
    i.data = x
    return
}
登入後複製

runtime.convT2I 會返回一個 runtime.iface,其中包含 runtime.itab 指標和 KFC 變數。當前函數返回之後,main 函數的棧上會包含以下資料:

5.png

SP 和 SP+8 中儲存的 runtime.itab 和 KFC 指標是 runtime.convT2I 函數的入參,這個函數的返回值位於 SP+16,是一個佔 16 位元組記憶體空間的 runtime.iface 結構體,SP+32 儲存的是在棧上的 KFC 結構體,它會在 runtime.convT2I 執行的過程中拷貝到堆上。

3.3型別斷言

如何將一個介面型別轉換成具體型別?

x.(T)

非空介面

func main() {
    var c Store = &KFC{Name: "東街店"}
    switch c.(type) {
    case *KFC:
        kfc := c.(*KFC)
        kfc.MakeHamberger()
    }
}
登入後複製

因為 Go 語言的編譯器做了一些優化,所以程式碼中沒有runtime.iface 的構建過程,不過對於這一節要介紹的型別斷言和轉換沒有太多的影響。

switch語句生成的組合指令會將目標型別的 hash 與介面變數中的 itab.hash 進行比較

空介面

func main() {
    var c interface{} = &KFC{Name: "東街店"}
    switch c.(type) {
    case *KFC:
        kfc := c.(*KFC)
        kfc.MakeHamberger()
    }
}
登入後複製

上述程式碼會在型別斷言時就不是直接獲取變數中具體型別的 runtime._type,而是從 eface._type 中獲取,組合指令仍然會使用目標型別的 hash 與變數的型別比較.

【相關推薦:Go視訊教學、】

以上就是golang的介面有啥用的詳細內容,更多請關注TW511.COM其它相關文章!