【Go語言入門系列】前面的文章:算法
若是你使用過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。 我是張三,在銀河大學學校食堂吃飯。 我在銀河大學學習數學。
這段代碼比較長,你能夠直接複製粘貼運行一下,下面好好地解釋一下。函數
上例中,咱們聲明瞭三個接口human
、adult
、teenager
:學習
type human interface { //定義human接口 say() eat() } type adult interface { //定義adult接口 say() eat() drink() work() } type teenager interface { //定義teenager接口 say() eat() learn() }
例子擺在這裏了,能夠很容易總結出它的特色。code
interface
和結構體strcut
的聲明相似:type interface_name interface { }
type interface_name interface { 方法簽名1 方法簽名2 ... }
先說一下上例代碼的具體內容。對象
有三個接口分別是:繼承
human
接口:有say()
、eat()
方法簽名。
adult
接口:有say()
、eat()
、drink()
、work()
方法簽名。
teenager
接口:有say()
、eat()
、learn()
方法簽名。
有三個結構體分別是:
people
結構體:有say()
、eat()
方法。student
結構體:有匿名字段people
,因此能夠說student
「繼承」了people
。有learn()
方法,並「重寫」了eat()
方法。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
。
接口也是值,這就意味着接口能像值同樣進行傳遞,並能夠做爲函數的參數和返回值。
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() } }
所謂空接口,即定義了零個方法簽名的接口。
空接口能夠用來保存任何類型的值,由於空接口中定義了零個方法簽名,這就至關於每一個類型都會實現實現空接口。
空接口長這樣:
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) //能夠存切片 }
看下例:
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。
因爲people
和MyInt
都實現了sayer
接口,因此它們都能做爲foo
函數的參數。
上一小節說過,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
類型斷言其實就是在猜接口變量中存儲的值的類型。
由於咱們並不肯定該接口變量中存儲的是什麼類型的值,因此確定會考慮足夠多的狀況:當是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
中存儲的值相同。
這裏的「繼承」並非面向對象的繼承,只是借用該詞表達意思。
咱們已經在【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語言基礎開始介紹,適合從零開始的初學者。
歡迎關注,咱們一塊兒踏上編程的行程。
若有錯誤,還請指正。