1. 程式人生 > >【Go語言入門系列】(九)寫這些就是為了搞懂怎麼用介面

【Go語言入門系列】(九)寫這些就是為了搞懂怎麼用介面

[【Go語言入門系列】](https://mp.weixin.qq.com/mp/appmsgalbum?action=getalbum&album_id=1441283546689404928)前面的文章: - [【Go語言入門系列】(六)再探函式](https://mp.weixin.qq.com/s/EgBGvrFmnn_oLanWIAnDgQ) - [【Go語言入門系列】(七)如何使用Go的方法?](https://mp.weixin.qq.com/s/dMcXqJ76e0xAbArn_Tb0og) - [【Go語言入門系列】(八)Go語言是不是面嚮物件語言?](https://mp.weixin.qq.com/s/Yau8Y1jM8dgLJwyuAM432A) # 1. 引入例子 如果你使用過Java等面嚮物件語言,那麼肯定對介面這個概念並不陌生。簡單地來說,介面就是規範,如果你的類實現了介面,那麼該類就必須具有介面所要求的一切功能、行為。介面中通常定義的都是方法。 > 就像玩具工廠要生產玩具,生產前肯定要先拿到一個生產規範,該規範要求了玩具的顏色、尺寸和功能,工人就按照這個規範來生產玩具,如果有一項要求沒完成,那就是不合格的玩具。 如果你之前還沒用過面嚮物件語言,那也沒關係,因為Go的介面和Java的介面有區別。直接看下面一個例項程式碼,來感受什麼是Go的介面,後面也圍繞該例程式碼來介紹。 ```go package main import "fmt" type people struct { name string age int } type student struct { people //"繼承"people subject string school string } type programmer struct { people //"繼承"people language string company string } type human interface { //定義human介面 say() eat() } type adult interface { //定義adult介面 say() eat() drink() work() } type teenager interface { //定義teenager介面 say() eat() learn() } func (p people) say() { //people實現say()方法 fmt.Printf("我是%s,今年%d。\n", p.name, p.age) } func (p people) eat() { //people實現eat()方法 fmt.Printf("我是%s,在吃飯。\n", p.name) } func (s student) learn() { //student實現learn()方法 fmt.Printf("我在%s學習%s。\n", s.school, s.subject) } func (s student) eat() { //student重寫eat()方法 fmt.Printf("我是%s,在%s學校食堂吃飯。\n", s.name, s.school) } func (pr programmer) work() { //programmer實現work()方法 fmt.Printf("我在%s用%s工作。\n", pr.company, pr.language) } func (pr programmer) drink() {//programmer實現drink()方法 fmt.Printf("我是成年人了,能大口喝酒。\n") } func (pr programmer) eat() { //programmer重寫eat()方法 fmt.Printf("我是%s,在%s公司餐廳吃飯。\n", pr.name, pr.company) } func main() { xiaoguan := people{"行小觀", 20} zhangsan := student{people{"張三", 20}, "數學", "銀河大學"} lisi := programmer{people{"李四", 21},"Go", "火星有限公司"} var h human h = xiaoguan h.say() h.eat() fmt.Println("------------") var a adult a = lisi a.say() a.eat() a.work() fmt.Println("------------") var t teenager t = zhangsan t.say() t.eat() t.learn() } ``` 執行: ``` 我是行小觀,今年20。 我是行小觀,在吃飯。 ------------ 我是李四,今年21。 我是李四,在火星有限公司公司餐廳吃飯。 我在火星有限公司用Go工作。 ------------ 我是張三,今年20。 我是張三,在銀河大學學校食堂吃飯。 我在銀河大學學習數學。 ``` 這段程式碼比較長,你可以直接複製貼上執行一下,下面好好地解釋一下。 # 2. 介面的宣告 上例中,我們聲明瞭三個介面`human`、`adult`、`teenager`: ```go type human interface { //定義human介面 say() eat() } type adult interface { //定義adult介面 say() eat() drink() work() } type teenager interface { //定義teenager介面 say() eat() learn() } ``` 例子擺在這裡了,可以很容易總結出它的特點。 1. 介面`interface`和結構體`strcut`的宣告類似: ```go type interface_name interface { } ``` 2. 介面內部定義了一組方法的簽名。何為方法的簽名?即方法的方法名、引數列表、返回值列表(沒有接收者)。 ```go type interface_name interface { 方法簽名1 方法簽名2 ... } ``` # 3. 如何實現介面? 先說一下上例程式碼的具體內容。 有三個介面分別是: 1. `human`介面:有`say()`、`eat()`方法簽名。 2. `adult`介面:有`say()`、`eat()`、`drink()`、`work()`方法簽名。 3. `teenager`介面:有`say()`、`eat()`、`learn()`方法簽名。 有三個結構體分別是: 1. `people`結構體:有`say()`、`eat()`方法。 2. `student`結構體:有匿名欄位`people`,所以可以說`student`“繼承”了`people`。有`learn()`方法,並“重寫”了`eat()`方法。 3. `programmer`結構體:有匿名欄位`people`,所以可以說`programmer`“繼承”了`people`。有`work()`、`drink()`方法,並“重寫”了`eat()`方法。 前面說過,介面就是規範,要想實現介面就必須遵守並具備介面所要求的一切。現在好好看看上面三個結構體和三個介面之間的關係: `people`結構體有`human`介面要求的`say()`、`eat()`方法。 `student`結構體有`teenager`介面要求的`say()`、`eat()`、`learn()`方法。 `programmer`結構體有`adult`介面要求的`say()`、`eat()`、`drink()`、`work()`方法。 雖然`student`和`programmer`都重寫了`say()`方法,即內部實現和接收者不同,但這沒關係,因為介面中只是一組方法簽名(不管內部實現和接收者)。 所以我們現在可以說:`people`實現了`human`介面,`student`實現了`human`、`teenager`介面,`programmer`實現了`human`、`adult`介面。 是不是感覺很巧妙?不需要像Java一樣使用`implements`關鍵字來顯式地實現介面,**只要型別實現了介面中定義的所有方法簽名,就可以說該型別實現了該介面**。(前面都是用結構體舉例,結構體就是一個型別)。 換句話說:**介面負責指定一個型別應該具有的方法,該型別負責決定這些方法如何實現**。 在Go中,實現介面可以這樣理解:`programmer`說話像`adult`、吃飯像`adult`、喝酒像`adult`、工作像`adult`,所以`programmer`是`adult`。 # 4. 介面值 介面也是值,這就意味著介面能像值一樣進行傳遞,並可以作為函式的引數和返回值。 ## 4.1. 介面變數存值 ```go func main() { xiaoguan := people{"行小觀", 20} zhangsan := student{people{"張三", 20}, "數學", "銀河大學"} lisi := programmer{people{"李四", 21},"Go", "火星有限公司"} var h human //定義human型別變數 h = xiaoguan var a adult //定義adult型別變數 a = lisi var t teenager //定義teenager型別變數 t = zhangsan } ``` 如果定義了一個介面型別變數,那麼該變數中可以儲存實現了該介面的任意型別值: ```go func main() { //這三個人都實現了human介面 xiaoguan := people{"行小觀", 20} zhangsan := student{people{"張三", 20}, "數學", "銀河大學"} lisi := programmer{people{"李四", 21},"Go", "火星有限公司"} var h human //定義human型別變數 //所以h變數可以存這三個人 h = xiaoguan h = zhangsan h = lisi } ``` 不能儲存未實現該`interface`介面的型別值: ```go func main() { xiaoguan := people{"行小觀", 20} //實現human介面 zhangsan := student{people{"張三", 20}, "數學", "銀河大學"} //實現teenager介面 lisi := programmer{people{"李四", 21},"Go", "火星有限公司"} //實現adult介面 var a adult //定義adult型別變數 //但zhangsan沒實現adult介面 a = zhangsan //所以a不能存zhangsan,會報錯 } ``` 否則會類似這樣報錯: ```go cannot use zhangsan (type student) as type adult in assignment: student does not implement adult (missing drink method) ``` 也可以定義介面型別切片: ```go func main() { var sli = make([]human, 3) sli[0] = xiaoguan sli[1] = zhangsan sli[2] = lisi for _, v := range sli { v.say() } } ``` ## 4.2. 空介面 所謂空介面,即定義了零個方法簽名的介面。 空介面可以用來儲存任何型別的值,因為空介面中定義了零個方法簽名,這就相當於每個型別都會實現實現空介面。 空介面長這樣: ```go interface {} ``` 下例程式碼展示了空介面可以儲存任何型別的值: ```go package main import "fmt" type people struct { name string age int } func main() { xiaoguan := people{"行小觀", 20} var ept interface{} //定義一個空介面變數 ept = 10 //可以存整數 ept = xiaoguan //可以存結構體 ept = make([]int, 3) //可以存切片 } ``` ## 4.3. 介面值作為函式引數或返回值 看下例: ```go package main import "fmt" type sayer interface {//介面 say() } func foo(a sayer) { //函式的引數是介面值 a.say() } type people struct { //結構體型別 name string age int } func (p people) say() { //people實現了介面sayer fmt.Printf("我是%s,今年%d歲。", p.name, p.age) } type MyInt int //MyInt型別 func (m MyInt) say() { //MyInt實現了介面sayer fmt.Printf("我是%d。\n", m) } func main() { xiaoguan := people{"行小觀", 20} foo(xiaoguan) //結構體型別作為引數 i := MyInt(5) foo(i) //MyInt型別作為引數 } ``` 執行: ```go 我是行小觀,今年20歲。 我是5。 ``` 由於`people`和`MyInt`都實現了`sayer`介面,所以它們都能作為`foo`函式的引數。 # 5. 型別斷言 上一小節說過,interface型別變數中可以儲存實現了該interface介面的任意型別值。 那麼給你一個介面型別的變數,你怎麼知道該變數中儲存的是什麼型別的值呢?這時就需要使用型別斷言了。型別斷言是這樣使用的: ```go t := var_interface.(val_type) ``` `var_interface`:一個介面型別的變數。 `val_type`:該變數中儲存的值的型別。 你可能會問:我的目的就是要知道介面變數中儲存的值的型別,你這裡還讓我提供值的型別? 注意:這是**型別斷言**,你得有個假設(猜)才行,然後去驗證猜對得對不對。 如果正確,則會返回該值,你可以用`t`去接收;如果不正確,則會報`panic`。 話說多了容易迷糊,直接看程式碼。還是用本章一開始舉的那個例子: ```go func main() { zhangsan := student{people{"張三", 20}, "數學", "銀河大學"} var x interface{} = zhangsan //x介面變數中存了一個student型別結構體 var y interface{} = "HelloWorld" //y介面變數中存了一個string型別的字串 /*現在假設你不知道x、y中存的是什麼型別的值*/ //現在使用型別斷言去驗證 //a := x.(people) //報panic //fmt.Println(a) //panic: interface conversion: interface {} is main.student, not main.people a := x.(student) fmt.Println(a) //列印{{張三 20} 數學 銀河大學} b := y.(string) fmt.Println(b) //列印 HelloWorld } ``` 第一次,我們斷言`x`中儲存的變數是`people`型別,但實際上是`student`型別,所以報panic。 第二次,我們斷言`x`中儲存的變數是`student`型別,斷言對了,所以會把`x`的值賦給`a`。 第三次,我們斷言`y`中儲存的變數是`string`型別,也斷言對了。 有時候我們並不需要值,只想知道介面變數中是否儲存了某型別的值,型別斷言可以返回兩個值: ```go t, ok := var_interface.(val_type) ``` `ok`是個布林值,如果斷言對了,為true;如果斷言錯了,為false且不報`panic`,但`t`會被置為“零值”。 ```go //斷言錯誤 value, ok := x.(people) fmt.Println(value, ok) //列印{ 0} false //斷言正確 _, ok := y.(string) fmt.Println(ok) //true ``` # 6. 型別選擇 型別斷言其實就是在猜介面變數中儲存的值的型別。 因為我們並不確定該介面變數中儲存的是什麼型別的值,所以肯定會考慮足夠多的情況:當是`int`型別的值時,採取這種操作,當是`string`型別的值時,採取那種操作等。這時你可能會採用`if...else...`來實現: ```go func main() { xiaoguan := people{"行小觀", 20} var x interface{} = 12 if value, ok := x.(string); ok { //x的值是string型別 fmt.Printf("%s是個字串。開心", value) } else if value, ok := x.(int); ok { //x的值是int型別 value *= 2 fmt.Printf("翻倍了,%d是個整數。哈哈", value) } else if value, ok := x.(people); ok { //x的值是people型別 fmt.Println("這是個結構體。", value) } } ``` 這樣顯得有點囉嗦,使用`switch...case...`會更加簡潔。 ```go switch value := x.(type) { case string: fmt.Printf("%s是個字串。開心", value) case int: value *= 2 fmt.Printf("翻倍了,%d是個整數。哈哈", value) case human: fmt.Println("這是個結構體。", value) default: fmt.Printf("前面的case都沒猜對,x是%T型別", value) fmt.Println("x的值為", value) } ``` 這就是型別選擇,看起來和普通的 switch 語句相似,但不同的是 case 是型別而不是值。 當介面變數`x`中儲存的值和某個case的型別匹配,便執行該case。如果所有case都不匹配,則執行 default,並且此時`value`的型別和值會和`x`中儲存的值相同。 # 7. “繼承”介面 這裡的“繼承”並不是面向物件的繼承,只是借用該詞表達意思。 我們已經在[【Go語言入門系列】(八)Go語言是不是面嚮物件語言?](https://mp.weixin.qq.com/s/Yau8Y1jM8dgLJwyuAM432A)一文中使用結構體時已經體驗了**匿名欄位**(嵌入欄位)的好處,這樣可以複用許多程式碼,比如欄位和方法。如果你對通過匿名欄位“繼承”得到的欄位和方法不滿意,還可以“重寫”它們。 對於介面來說,也可以通過“繼承”來複用程式碼,實際上就是把一個介面當做匿名欄位嵌入另一個介面中。下面是一個例項: ```go package main import "fmt" type animal struct { //結構體animal name string age int } type dog struct { //結構體dog animal //“繼承”animal address string } type runner interface { //runner介面 run() } type watcher interface { //watcher介面 runner //“繼承”runner介面 watch() } func (a animal) run() { //animal實現runner介面 fmt.Printf("%s會跑\n", a.name) } func (d dog) watch() { //dog實現watcher介面 fmt.Printf("%s在%s看門\n", d.name, d.address) } func main() { a := animal{"小動物", 12} d := dog{animal{"哮天犬", 13}, "天庭"} a.run() d.run() //哮天犬可以呼叫“繼承”得到的介面中的方法 d.watch() } ``` 執行: ```go 小動物會跑 哮天犬會跑 哮天犬在天庭看門 ``` # [作者簡介](https://mp.weixin.qq.com/s/PF7srGAwzd_w5pU6eOEZow) >【作者】:[行小觀](https://mp.weixin.qq.com/s/PF7srGAwzd_w5pU6eOEZow) > >【公眾號】:[行人觀學](https://mp.weixin.qq.com/s/PF7srGAwzd_w5pU6eOEZow) > >【簡介】:一個面向學習的賬號,用有趣的語言寫系列文章。包括Java、Go、資料結構和演算法、計算機基礎等相關文章。 > >--- >本文章屬於系列文章「[Go語言入門系列](https://mp.weixin.qq.com/mp/appmsgalbum?action=getalbum&album_id=1441283546689404928)」,本系列從Go語言基礎開始介紹,適合從零開始的初學者。 > >--- >歡迎關注,我們一起踏上程式設計的行程。 > **如有錯誤,還請指