【Go語言入門系列】(九)寫這些就是爲了搞懂怎麼用接口

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

1. 引入例子

若是你使用過Java等面嚮對象語言,那麼確定對接口這個概念並不陌生。簡單地來講,接口就是規範,若是你的類實現了接口,那麼該類就必須具備接口所要求的一切功能、行爲。接口中一般定義的都是方法。編程

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

若是你以前還沒用過面嚮對象語言,那也不要緊,由於Go的接口和Java的接口有區別。直接看下面一個實例代碼,來感覺什麼是Go的接口,後面也圍繞該例代碼來介紹。app

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

例子擺在這裏了,能夠很容易總結出它的特色。code

  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語言基礎開始介紹,適合從零開始的初學者。


歡迎關注,咱們一塊兒踏上編程的行程。

若有錯誤,還請指正。

相關文章
相關標籤/搜索