【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. 介面的宣告
上例中,我們宣告瞭三個介面human
、adult
、teenager
:
type human interface { //定義human介面
say()
eat()
}
type adult interface { //定義adult介面
say()
eat()
drink()
work()
}
type teenager interface { //定義teenager介面
say()
eat()
learn()
}
例子擺在這裡了,可以很容易總結出它的特點。
- 介面
interface
和結構體strcut
的宣告類似:
type interface_name interface {
}
- 介面內部定義了一組方法的簽名。何為方法的簽名?即方法的方法名、引數列表、返回值列表(沒有接收者)。
type interface_name interface {
方法簽名1
方法簽名2
...
}
3. 如何實現介面?
先說一下上例程式碼的具體內容。
有三個介面分別是:
-
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
。
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。
由於people
和MyInt
都實現了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語言基礎開始介紹,適合從零開始的初學者。
歡迎關注,我們一起踏上程式設計的行程。
如有錯誤,還請指正。