學到什麼
什麼是介面?
如何定義介面?
如何使用介面?
如何嵌入介面?
介面與介面之間如何賦值?
如何推斷介面的實際型別?
如何使用空介面?
介面是通過定義抽象方法來約定實現者的規則,概念和其它語言中有點類似,但對於 Go 語言中介面的實現與介面之間耦合性更低,靈活性更高。
為了通俗的理解介面的概念,我舉個例子,”假如你受女媧之託讓你進行造人的工作,但給你制定了造人的要求,要有吃喝的動作,但具體每個人怎麼吃怎麼喝那無所謂,只要有了這兩個動作就表示你造人成功”。
這個例子中女媧制定的要求就是介面,你按照造人的要求去做就稱作介面的實現。
也不知道講明白了沒,保佑你可以。
定義
現在定義一個 People
的介面,並定義吃喝兩個動作。
type People interface {
// 可不寫引數名:Eat(string) error
Eat(thing string) error
Drink(thing string) error
}
Eat
和Drink
方法不需要具體實現。方法前不需要
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
關鍵字顯示的去實現。可同時實現多個介面。
再看張圖,可能就更清晰了,如下:
圖中“實現者”都包含了 A
和 B
介面的方法,那它都實現了這兩個介面。
介面的使用
實現了介面之後,就可以將該型別的例項化賦值給介面型別。
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
型別變數。
例:
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 協議》,轉載必須註明作者和本文連結