認真一點學 Go:15. 介面

老苗發表於2021-10-15

>> 原文地址

學到什麼

  1. 什麼是介面?

  2. 如何定義介面?

  3. 如何使用介面?

  4. 如何嵌入介面?

  5. 介面與介面之間如何賦值?

  6. 如何推斷介面的實際型別?

  7. 如何使用空介面?

介面是通過定義抽象方法來約定實現者的規則,概念和其它語言中有點類似,但對於 Go 語言中介面的實現與介面之間耦合性更低,靈活性更高。

為了通俗的理解介面的概念,我舉個例子,”假如你受女媧之託讓你進行造人的工作,但給你制定了造人的要求,要有吃喝的動作,但具體每個人怎麼吃怎麼喝那無所謂,只要有了這兩個動作就表示你造人成功”。

這個例子中女媧制定的要求就是介面,你按照造人的要求去做就稱作介面的實現。

也不知道講明白了沒,保佑你可以。

定義

現在定義一個 People 的介面,並定義吃喝兩個動作。


type People interface {

  // 可不寫引數名:Eat(string) error

    Eat(thing string) error

    Drink(thing string) error

}
  • EatDrink 方法不需要具體實現。

  • 方法前不需要 func 關鍵字。

  • 方法的引數名稱和返回名稱可以不寫。

定義後就可以直接宣告一個該介面型別變數。


var p People

p 變數沒有初始化,此時值為 nil

實現介面

介面實現的工作是交給自定義型別的,自定義型別實現了介面所有的方法,就實現介面了。


type LaoMiao struct {

    Name string

    Age  int

}

func (l LaoMiao) Eat(thing string) error {

    fmt.Println("在公司偷吃" + thing)

    return nil

}

func (l LaoMiao) Drink(thing string) error {

    fmt.Println("在公司偷喝" + thing)

    return nil

}
  • LaoMiao 實現了 People 介面中的所有方法,就說明實現了該介面。

  • 無需使用其它語言中 implements 關鍵字顯示的去實現。

  • 可同時實現多個介面。

再看張圖,可能就更清晰了,如下:

認真一點學 Go:15. 介面

圖中“實現者”都包含了 AB 介面的方法,那它都實現了這兩個介面。

介面的使用

實現了介面之後,就可以將該型別的例項化賦值給介面型別。


var p People = LaoMiao{}

p.Eat("桃子")

// 輸出

在公司偷吃桃子

p 為介面型別,實際的實現是 LaoMiao 型別。

你可能好奇,我為啥不直接呼叫呢,類似如下:


m := LaoMiao{}

m.Eat("桃子")

上面程式碼沒有使用 People 介面型別,但如果我再定義一個型別去實現 People 介面,好處就體現出來了。


type LaoSun struct {

    Name string

    Age  int

}

func (l LaoSun) Eat(thing string) error {

    fmt.Println("在車上吃" + thing)

    return nil

}

func (l LaoSun) Drink(thing string) error {

    fmt.Println("在車上喝" + thing)

    return nil

}

又增加了一個型別去實現,看清楚這個是 LaoSun ,上面的那個型別是 LaoMiao

現在開始想個問題,如果我想呼叫這兩個型別的方法,並且呼叫的程式碼只寫一遍,該如何做?喘口氣,我告訴你,自然是用介面。


// interface/main.go

// ...

func Run(p People) {

    thing1, thing2 := "桃子", "可樂"

    p.Eat(thing1)

    p.Drink(thing2)

}

func main() {

    Run(LaoMiao{})

    Run(LaoSun{})

}

// 輸出

在公司偷吃桃子

在公司偷喝可樂

在車上吃桃子

在車上喝可樂

該程式碼增加了一個 Run 函式,該函式接受的型別為介面型別,main 函式中將兩個實現介面的型別傳遞給函式。

接收者型別與介面

在上面的程式碼中所有實現介面的型別接收者都是值型別,例如:


func (l LaoSun) Eat(thing string) error {

    // ...

}

接收者l型別為 LaoSun ,如果是指標接收者應該為 *LaoSun

在使用介面型別呼叫時,介面接受的型別為值型別,例如:


Run(LaoSun{})

該函式的引數 LaoSun{} 為值型別,但其實也可以傳遞指標型別 Run(&LaoSun{}) ,編譯器會進行解引用。

如果接收者為指標型別時,那給介面傳值時必須使用指標型別,例如:


type GouDan struct {

   Name string

   Age  int

}

func (l *GouDan) Eat(thing string) error {

    fmt.Println("你管我吃" + thing)

    return nil

}

func (l *GouDan) Drink(thing string) error {

    fmt.Println("你管我喝" + thing)

    return nil

}

重新定義了一個型別去實現 People 介面並且方法的接收者為指標型別,如果給介面傳遞值型別時,編譯器會報錯。


Run(GouDan{})

// 輸出

cannot use GouDan{} (type GouDan) as type People in argument to Run:

    GouDan does not implement People (Drink method has pointer receiver)

正確的是,只能傳遞指標型別。


Run(&GouDan{})var sun *LaoSun = &LaoSun{}

Run(sun)

介面嵌入

一個介面可以包含另外一個介面,例如:


type Student interface {

   People

   Study()

}
  • 定義了一個 Student 介面,對於學生介面自然也會有吃喝的動作,因此無需重複定義,只需要將 People 介面嵌入就可以。

  • 如果自定義型別想實現 Student 介面,需要將嵌入介面定義的方法和自己所定義的方法都需要實現。

  • 可嵌入多個介面。

介面與介面賦值

在上面的程式碼中 People 介面被嵌入到了 Student 介面 ,那這個時候,Student 介面型別變數就可以賦值給 People 型別變數。

認真一點學 Go:15. 介面

例:


var stu Student

var pl People = stu

如果把 People 型別不被嵌入,而只是讓 Student 介面包含其方法,那上面介面與介面賦值也是允許的。


type Student interface {

    Eat(thing string) error

    Drink(thing string) error

    Study()

}

總結:大介面包含了小介面的方法,那大介面就可以賦值給小介面。

空介面

空介面表示沒有定義任何抽象方法,如下:


type Empty interface {}

Empty 型別現在就是空的介面,它可以接受任意型別。


var str Empty = "字串"

var num Empty = 222

平常專案中使用時,為了更簡單,其實無需定義空介面,直接使用 interface{} 作為型別。


var str interface{} = "字串"

var num interface{} = 222

總結一句:所有型別都實現了空介面,即空介面可以接受任意型別變數

型別推斷

在一個介面變數中,如果想知道該介面變數的具體實現型別是誰就需要使用型別推斷。

1. 介面轉實現者


v := var1.(T)
  • T 表示你需要推斷的型別。

  • v 為轉化後型別為 T 的變數。

  • var1 可以為空介面。

例:


var people People

// 將 People 型別轉化為 LaoMiao 值型別

people = LaoMiao{}

val := people.(LaoMiao)

// 將 People 型別轉化為 LaoMiao 指標型別

people = &LaoMiao{}

peo := people.(*LaoMiao)

從例子中可以看出,介面變數中儲存的型別和推斷時的型別必須相同,如果不相同時,編譯器會報錯。


people = LaoMiao{}

// 報錯

val := people.(*LaoMiao)

// 正確

val := people.(LaoMiao)

2. 是否可推斷

如果介面變數中儲存的實際型別不確定,那就必須進行判斷,如果不判斷時出現無法推斷的情況,編譯器就會報錯。


v, ok := var1.(T)

判斷其實就是在推斷型別時多增加了一個返回值,如果可推斷 ok 值為 true,否則為 false


people = LaoMiao{}

val1, ok := people.(*LaoMiao)

fmt.Println(ok)

val2, ok := people.(LaoMiao)

fmt.Println(ok)

// 輸出

false

true

3. type-switch

這個知識點其實在很早之前的《流程控制》一篇中講過了,我再講一遍並補充。


var data interface{}

data = "111"

// data 是介面型別, .(type) 獲取實際型別

// 將實際型別的值賦給 d 變數

switch d := data.(type) {

case string:

    // 進入分支後,d 是 string 型別

    fmt.Println(d + "str")

case int:

    // 進入分支後, d 是 int 型別

    fmt.Println(d + 1)

}

// 輸出

111str
  • 通過 .(type) 獲取介面的實際型別,記住這種方式只能用於 switch 語句中,這也是我為什麼單獨在這塊講解。

  • 不能使用 fallthrough 關鍵字。

  • 如果只是判斷型別,則無需使用 d 變數接受。

總結

Go 語言的介面知識點就講完了,很重要,務必要掌握清楚。那現在問問你知道介面的實現精髓是啥嗎?

我總結下,只要實現了其方法就實現了介面,實現的方法如果滿足了多個介面,那就都實現。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章