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

行人觀學發表於2020-09-09

【Go語言入門系列】前面的文章:

1. 引入例子

如果你使用過Java等面嚮物件語言,那麼肯定對介面這個概念並不陌生。簡單地來說,介面就是規範,如果你的類實現了介面,那麼該類就必須具有介面所要求的一切功能、行為。介面中通常定義的都是方法。

就像玩具工廠要生產玩具,生產前肯定要先拿到一個生產規範,該規範要求了玩具的顏色、尺寸和功能,工人就按照這個規範來生產玩具,如果有一項要求沒完成,那就是不合格的玩具。

如果你之前還沒用過面嚮物件語言,那也沒關係,因為Go的介面和Java的介面有區別。直接看下面一個例項程式碼,來感受什麼是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. 介面的宣告

上例中,我們宣告瞭三個介面humanadultteenager

type human interface { //定義human介面
	say()
	eat()
}

type adult interface { //定義adult介面
	say()
	eat()
	drink()
	work()
}

type teenager interface { //定義teenager介面
	say()
	eat()
	learn()
}

例子擺在這裡了,可以很容易總結出它的特點。

  1. 介面interface和結構體strcut的宣告類似:
type interface_name interface {
    
}
  1. 介面內部定義了一組方法的簽名。何為方法的簽名?即方法的方法名、引數列表、返回值列表(沒有接收者)。
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()方法。

雖然studentprogrammer都重寫了say()方法,即內部實現和接收者不同,但這沒關係,因為介面中只是一組方法簽名(不管內部實現和接收者)。

所以我們現在可以說:people實現了human介面,student實現了humanteenager介面,programmer實現了humanadult介面。

是不是感覺很巧妙?不需要像Java一樣使用implements關鍵字來顯式地實現介面,只要型別實現了介面中定義的所有方法簽名,就可以說該型別實現了該介面。(前面都是用結構體舉例,結構體就是一個型別)。

換句話說:介面負責指定一個型別應該具有的方法,該型別負責決定這些方法如何實現

在Go中,實現介面可以這樣理解:programmer說話像adult、吃飯像adult、喝酒像adult、工作像adult,所以programmeradult

4. 介面值

介面也是值,這就意味著介面能像值一樣進行傳遞,並可以作為函式的引數和返回值。

4.1. 介面變數存值

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
}

如果定義了一個介面型別變數,那麼該變數中可以儲存實現了該介面的任意型別值:

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介面的型別值:

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,會報錯
}

否則會類似這樣報錯:

cannot use zhangsan (type student) as type adult in assignment:
student does not implement adult (missing drink method)

也可以定義介面型別切片:

func main() {
    var sli = make([]human, 3)
	sli[0] = xiaoguan
	sli[1] = zhangsan
	sli[2] = lisi

	for _, v := range sli {
		v.say()
	}
}

4.2. 空介面

所謂空介面,即定義了零個方法簽名的介面。

空介面可以用來儲存任何型別的值,因為空介面中定義了零個方法簽名,這就相當於每個型別都會實現實現空介面。

空介面長這樣:

interface {}

下例程式碼展示了空介面可以儲存任何型別的值:

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. 介面值作為函式引數或返回值

看下例:

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型別作為引數
}

執行:

我是行小觀,今年20歲。
我是5。

由於peopleMyInt都實現了sayer介面,所以它們都能作為foo函式的引數。

5. 型別斷言

上一小節說過,interface型別變數中可以儲存實現了該interface介面的任意型別值。

那麼給你一個介面型別的變數,你怎麼知道該變數中儲存的是什麼型別的值呢?這時就需要使用型別斷言了。型別斷言是這樣使用的:

t := var_interface.(val_type)

var_interface:一個介面型別的變數。

val_type:該變數中儲存的值的型別。

你可能會問:我的目的就是要知道介面變數中儲存的值的型別,你這裡還讓我提供值的型別?

注意:這是型別斷言,你得有個假設(猜)才行,然後去驗證猜對得對不對。

如果正確,則會返回該值,你可以用t去接收;如果不正確,則會報panic

話說多了容易迷糊,直接看程式碼。還是用本章一開始舉的那個例子:

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型別,也斷言對了。

有時候我們並不需要值,只想知道介面變數中是否儲存了某型別的值,型別斷言可以返回兩個值:

t, ok := var_interface.(val_type)

ok是個布林值,如果斷言對了,為true;如果斷言錯了,為false且不報panic,但t會被置為“零值”。

//斷言錯誤
value, ok := x.(people)
fmt.Println(value, ok) //列印{ 0} false

//斷言正確
_, ok := y.(string)
fmt.Println(ok) //true

6. 型別選擇

型別斷言其實就是在猜介面變數中儲存的值的型別。

因為我們並不確定該介面變數中儲存的是什麼型別的值,所以肯定會考慮足夠多的情況:當是int型別的值時,採取這種操作,當是string型別的值時,採取那種操作等。這時你可能會採用if...else...來實現:

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...會更加簡潔。

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語言是不是面嚮物件語言?一文中使用結構體時已經體驗了匿名欄位(嵌入欄位)的好處,這樣可以複用許多程式碼,比如欄位和方法。如果你對通過匿名欄位“繼承”得到的欄位和方法不滿意,還可以“重寫”它們。

對於介面來說,也可以通過“繼承”來複用程式碼,實際上就是把一個介面當做匿名欄位嵌入另一個介面中。下面是一個例項:

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()
}

執行:

小動物會跑
哮天犬會跑
哮天犬在天庭看門

作者簡介

【作者】:行小觀

【公眾號】:行人觀學

【簡介】:一個面向學習的賬號,用有趣的語言寫系列文章。包括Java、Go、資料結構和演算法、計算機基礎等相關文章。


本文章屬於系列文章「Go語言入門系列」,本系列從Go語言基礎開始介紹,適合從零開始的初學者。


歡迎關注,我們一起踏上程式設計的行程。

如有錯誤,還請指正。

相關文章