Go 專欄|接口 interface
Duck Typing,鴨子類型,在維基百科里是這樣定義的:
If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.
翻譯過來就是:如果某個東西長得像鴨子,游泳像鴨子,嘎嘎叫像鴨子,那它就可以被看成是一只鴨子。
它是動態(tài)編程語言的一種對象推斷策略,它更關注對象能做什么,而不是對象的類型本身。
例如:在動態(tài)語言 Python 中,定義一個這樣的函數(shù):
def hello_world(duck):
duck.say_hello()
當調用此函數(shù)的時候,可以傳入任意類型,只要它實現(xiàn)了 say_hello() 就可以。如果沒實現(xiàn),運行過程中會出現(xiàn)錯誤。
Go 語言作為一門靜態(tài)語言,它通過接口的方式完美支持鴨子類型。
接口類型
之前介紹的類型都是具體類型,而接口是一種抽象類型,是多個方法聲明的集合。在 Go 中,只要目標類型實現(xiàn)了接口要求的所有方法,我們就說它實現(xiàn)了這個接口。
先來看一個例子:
package main
import "fmt"
// 定義接口,包含 Eat 方法
type Duck interface {
Eat()
}
// 定義 Cat 結構體,并實現(xiàn) Eat 方法
type Cat struct{}
func (c *Cat) Eat() {
fmt.Println("cat eat")
}
// 定義 Dog 結構體,并實現(xiàn) Eat 方法
type Dog struct{}
func (d *Dog) Eat() {
fmt.Println("dog eat")
}
func main() {
var c Duck = &Cat{}
c.Eat()
var d Duck = &Dog{}
d.Eat()
s := []Duck{
&Cat{},
&Dog{},
}
for _, n := range s {
n.Eat()
}
}
使用 type 關鍵詞定義接口:
type Duck interface {
Eat()
}
接口包含了一個 Eat() 方法,然后定義兩個結構體類型 Cat 和 Dog,分別實現(xiàn)了 Eat 方法。
// 定義 Cat 結構體,并實現(xiàn) Eat 方法
type Cat struct{}
func (c *Cat) Eat() {
fmt.Println("cat eat")
}
// 定義 Dog 結構體,并實現(xiàn) Eat 方法
type Dog struct{}
func (d *Dog) Eat() {
fmt.Println("dog eat")
}
遍歷接口切片,通過接口類型可以直接調用對應方法:
s := []Duck{
&Cat{},
&Dog{},
}
for _, n := range s {
n.Eat()
}
// 輸出
// cat eat
// dog eat
接口賦值
接口賦值分兩種情況:
將對象實例賦值給接口
將一個接口賦值給另一個接口
下面來分別說說:
將對象實例賦值給接口
還是用上面的例子,因為 Cat 實現(xiàn)了 Eat 接口,所以可以直接將 Cat 實例賦值給接口。
var c Duck = &Cat{}
c.Eat()
在這里一定要傳結構體指針,如果直接傳結構體會報錯:
var c Duck = Cat{}
c.Eat()
# command-line-arguments
./09_interface.go:25:6: cannot use Cat{} (type Cat) as type Duck in assignment:
Cat does not implement Duck (Eat method has pointer receiver)
但是如果反過來呢?比如使用結構體來實現(xiàn)接口,使用結構體指針來賦值:
// 定義 Cat 結構體,并實現(xiàn) Eat 方法
type Cat struct{}
func (c Cat) Eat() {
fmt.Println("cat eat")
}
var c Duck = &Cat{}
c.Eat() // cat eat
沒有問題,可以正常執(zhí)行。
將一個接口賦值給另一個接口
還是上面的例子,可以直接將 c 的值直接賦值給 d:
var c Duck = &Cat{}
c.Eat()
var d Duck = c
d.Eat()
再來,我再定義一個接口 Duck1,這個接口包含兩個方法 Eat 和 Walk,然后結構體 Dog 實現(xiàn)兩個方法,但是 Cat 只實現(xiàn) Eat 方法。
type Duck1 interface {
Eat()
Walk()
}
// 定義 Dog 結構體,并實現(xiàn) Eat 方法
type Dog struct{}
func (d *Dog) Eat() {
fmt.Println("dog eat")
}
func (d *Dog) Walk() {
fmt.Println("dog walk")
}
那么在賦值時,使用 Duck1 賦值給 Duck 是可以的,反過來就會報錯。
var c1 Duck1 = &Dog{}
var c2 Duck = c1
c2.Eat()
所以,已經(jīng)初始化的接口變量 c1 直接賦值給另一個接口變量 c2,要求 c2 的方法集是 c1 的方法集的子集。
空接口
具有 0 個方法的接口稱為空接口,它表示為 interface {}。由于空接口有 0 個方法,所以所有類型都實現(xiàn)了空接口。
func main() {
// interface 形參
s1 := "Hello World"
i := 50
strt := struct {
name string
}{
name: "AlwaysBeta",
}
test(s1)
test(i)
test(strt)
}
func test(i interface{}) {
fmt.Printf("Type = %T, value = %v\n", i, i)
}
類型斷言
類型斷言是作用在接口值上的操作,語法如下:
x.(T)
其中 x 是接口類型的表達式,T 是斷言類型。
作用是判斷操作數(shù)的動態(tài)類型是否滿足指定的斷言類型。
有兩種情況:
T是具體類型T是接口類型
下面來分別舉例說明:
具體類型
類型斷言會檢查 x 的動態(tài)類型是否為 T,如果是,則輸出 x 的值;如果不是,程序直接 panic。
func main() {
// 類型斷言
var n interface{} = 55
assert(n) // 55
var n1 interface{} = "hello"
assert(n1) // panic: interface conversion: interface {} is string, not int
}
func assert(i interface{}) {
s := i.(int)
fmt.Println(s)
}
接口類型
類型斷言會檢查 x 的動態(tài)類型是否滿足接口類型 T,如果滿足,則輸出 x 的值,這個值可能是綁定實例的副本,也可能是指針的副本;如果不滿足,程序直接 panic。
func main() {
// 類型斷言
assertInterface(c) // &{}
}
func assertInterface(i interface{}) {
s := i.(Duck)
fmt.Println(s)
}
如果有兩個接收值,那么斷言不會在失敗時崩潰,而是會多返回一個布爾值,一般命名為 ok,來表示斷言是否成功。
func main() {
// 類型斷言
var n1 interface{} = "hello"
assertFlag(n1)
}
func assertFlag(i interface{}) {
if s, ok := i.(int); ok {
fmt.Println(s)
}
}
類型查詢
語法類似類型斷言,只需將 T 直接用關鍵詞 type 替代。
作用主要有兩個:
查詢一個接口變量綁定的底層變量類型
查詢一個接口變量的底層變量是否還實現(xiàn)了其他接口
func main() {
// 類型查詢
SearchType(50) // Int: 50
SearchType("zhangsan") // String: zhangsan
SearchType(c) // dog eat
SearchType(50.1) // Unknown type
}
func SearchType(i interface{}) {
switch v := i.(type) {
case string:
fmt.Printf("String: %s\n", i.(string))
case int:
fmt.Printf("Int: %d\n", i.(int))
case Duck:
v.Eat()
default:
fmt.Printf("Unknown type\n")
}
}
總結
本文從鴨子類型引出 Go 的接口,然后用一個例子簡單展示了接口類型的用法,接著又介紹了接口賦值,空接口,類型斷言和類型查詢。
相信通過本篇文章大家能對接口有了整體的概念,并掌握了基本用法。
文章中的腦圖和源碼都上傳到了 GitHub,有需要的同學可自行下載。
地址: https://github.com/yongxinz/gopher/tree/main/sc
關注公眾號 AlwaysBeta,回復「goebook」領取 Go 編程經(jīng)典書籍。
Go 專欄文章列表:
