Golang interface 用法
作者:閃電豹貓 轉載注明出處
1. 接口 (interface) 介紹
接口是 Go 語言提供的數據類型之一,它把所有具有共性的方法 (注意與函數區別開) 定義在一起,任何其它類型只要一一實現這些方法的話,我們就稱這個類型實現了這個接口。Go 語言的接口與 C++ 的虛函數有共通之處,提高了語言的靈活性的同時也彌補了語言自身的一些不足。
Go 語言的接口與其它面向對象語言的接口不同,Go 的接口只是用來對方法進行一個收束,而正是這個收束,使得 Go 這個面向過程的語言擁有了面向對象的特征。
一般來說,Go 接口的主要功能有:
- 作為方法的收束器,進行具有 “面向對象程序設計特色” 的程序設計。
- 作為各種數據的承載者,可以用來為函數接收各類不同數量的函數參數,這也是 Go 提倡的接口編程。
2. 接口的定義和使用
2.1 定義
比如一個完整方法的接口的定義:
// 這是接口,接口內只有方法的定義,沒有具體實現
type 接口類型名 interface {
方法名1( 參數列表1 ) 返回值列表1
方法名2( 參數列表2 ) 返回值列表2
...
}
// 定義結構體
type 結構體名 struct {
變量名1 類型1
變量名2 類型2
...
}
// 實現接口方法
func ( 結構體變量1 結構體名 ) 方法名1( 參數列表1 ) 返回值列表1 {
//方法實現
}
func ( 結構體變量2 結構體名 ) 方法名2( 參數列表2 ) 返回值列表2 {
//方法實現
}
func ( 結構體變量n 結構體名 ) 方法名n( 參數列表n ) 返回值列表n {
//方法實現
}
在實踐中,我們一般將接口命名為 “什么什么er”,比如寫操作的接口可以叫Writer,讀取字符串的接口可以叫做StringReader。和變量的命名規則一樣,接口名的命名也是不能以數字開頭、只允許出現一種特殊字符_,開頭大寫則包外可見,開頭小寫則方法在包外不可見等等。
對於接口內的方法名,也是一樣的。只有接口名和方法名的首字母都大寫,才可以在包外調用這個接口的這個方法。
2.2 使用
一個接口只要全部實現了接口中聲明的方法,那么就是實現了這個接口。換句話講,接口就是一個需要具體實現的方法的列表。
下面給出一個示例代碼
// 定義接口
type Canteen interface {
MakeRice()
MakeNoodles()
}
// 定義結構體
type ZhuYuan struct {}
type HaiTang struct {}
type DingXiang struct {}
// 挨個實現接口里所聲明的方法
func (a ZhuYuan) MakeRice() {
fmt.Println("竹園餐廳的米飯")
}
func (a ZhuYuan) MakeNoodles() {
fmt.Println("竹園餐廳的面條")
}
func (a HaiTang) MakeRice() {
fmt.Println("海棠餐廳的米飯")
}
func (a HaiTang) MakeNoodles() {
fmt.Println("海棠餐廳的面條")
}
func (pa *DingXIang) MakeRice() {
fmt.Println("丁香餐廳的米飯")
}
func (pa *DingXiang) MakeNoodles() {
fmt.Println("丁香餐廳的面條")
}
該示例中,我們將竹園和海棠用結構體對象實現,而丁香是用指向結構體的指針實現的。這樣,我們在接口實例化時:
var a Canteen = ZhuYuan{} // 接受結構體,且傳入的也是結構體,可以通過編譯
var b Canteen = &ZhuYaun{} // 接受結構體,傳入的是指針,可以通過編譯,這很重要
var c Canteen = DingXiang{} // 接受指針,傳入的卻是結構體,編譯當然會失敗
var d Canteen = &DingXiang{} // 接受指針,傳入的也是指針,可以通過編譯
記住,Go 中的所有東西都是按值傳遞的。每次調用函數時,傳入的數據都會被復制。對於具有值接收者的方法,在調用該方法時將復制該值。對於上面四行代碼中的 a 和 d ,沒啥好解釋的,接受啥類型就給它啥類型嘛。對於 b ,編譯器會對指針 &ZhuYuan{} 進行拷貝,相關方法調用時,會對拷貝后的指針進行隱式解引用獲取指針指向的結構體。這也就能解釋為啥上面的var b Canteen = &ZhuYuan{}為什么能通過編譯。打個比方,只給你一個 int8 類型的值 123 ,你無法知道這個 123 的內存地址;而給你一個指針 *int8 ,你既能獲知該 int8 數的內存地址,又能知道該數是多少。
總而言之,當我們用指針實現方法時,只有指針類型的變量才可以實現接口;當我們用結構體實現方法時,結構體類型和指針類型都可以實現接口。不過,在實際開發中,這個性質沒那么重要,這里講開了是為了解釋現象背后的原理。
補充
Go 的接口是隱式實現的,也就是說,在接口的定義里的一條條方法只是聲明,具體有沒有方法的實現,Go 不在乎。
因為是隱式實現的,所以不實現方法也是可以通過編譯的,只要程序別遇到需要對未實現的方法進行傳參、返參和變量賦值,編譯器就不會檢查,程序就不會嗝屁。
如下代碼完全可以編譯運行,控制台輸出 12 並退出:
package main
import (
"fmt"
)
type hhher interface {
AAA(int, int)
PrintAge()
CCCC(string, map[int]string) (int, int)
}
type People struct {
Age int
}
func (human People) PrintAge() {
fmt.Println(human.Age)
}
func main() {
fmt.Println("Hello, playground")
alex := &People{Age:12,} // 這里就是接收結構體而傳入指針,是可行的
alex.PrintAge()
}
這里不推薦沒有把接口里的方法全部實現的做法。
2.3 數據承擔者
一個空接口 interface{} 什么方法 (method) 也沒有實現,是一個能裝入任意數量、任意數據類型的數據容器。
為什么這樣說呢?是這樣的。空接口 interface{} 也是接口,不過是沒有實現方法的接口罷了。回顧接口的定義:接口是一組方法的集合,是一種數據類型,任何其他類型如果能實現接口內所有的方法的話,我們稱那個數據類型實現了這個接口。咱們再來看空接口:里面連一個方法也沒有,不也就是任意數據類型都能實現這個接口了嘛。這就和 “空集能被任意集合包含” 一樣嘛,空接口能被任意數據類型實現。
與 C 語言的 void * 可以轉換成任意其它類型的指針 (如 char *、int * 等) 不同的是,Go 語言的 interface{} 不是一個任意數據類型,interface{} 的類型就是 interface{} 類型,不能轉換成其他接口類型,更不能轉換成其他什么類型 (比如[]int、string等等) ,只不過是 interface{} 能裝入任意數據罷了。
把其它類型的變量轉換成interface{}類型后,在程序運行時 (runtime) 內,該變量的數據類型將會發生變化,但是如果這時候要求獲取該變量的數據類型,我們會得到interface{}類型。這是為啥子呢?
在 Golang 的源代碼中,用runtime.iface表示非空接口類型,用runtime.eface表示空接口類型interface{}。雖然它們倆都用一個interface聲明,但是后者在 Golang 的源代碼更加常見,所以在實現interface{}時,使用了特殊的類型。具體的你得看 Golang 源代碼和 Go 手冊了。
- 用空接口可以讓函數和方法接受任意類型、任意數量的函數參數:
func show(a interface{}) {
fmt.Printf("a的類型是%T,a的值是%v\n", a, a)
}
空接口切片還可以用於函數的可選參數,比如:
func main() {
kkk(234, "qwerty", [5]int64{1,2,4}, false, nil)
kkk(236)
}
func kkk(key int, a ...interface{}) {
// 必選參數是一個 int 類型,可選參數用空接口切片表示
// 其類型為 []interface{},這個 a 是可以下表訪問的,而每個 a 的元素都是個空接口
if key == 234 {
fmt.Println((a[1])) // 這里需要保證a[1]下標不越界,我這里沒有進行判斷
switch ttt := a[0].(type) {
case string: fmt.Println("0th element is string interface{}")
default: fmt.Printf("idk wtf is this: %T", ttt)
}
switch ttt := a[1].(type) {
case string: fmt.Println("1st element is string interface{}")
default: fmt.Printf("idk wtf is this: %T\n", ttt)
}
} else {
fmt.Println("key wrong")
}
}
程序輸出如下:
[1 2 4 0 0]
0th element is string interface{}
idk wtf is this: [5]int64
key wrong
-
空接口還可以作為函數的返回值,但是極不推薦這樣干,因為代碼的維護、拓展與重構將會變得極為痛苦。
-
空接口可以實現保存任意類型值的字典 (map):
var alexInfo = make( map[string]interface{} )
alexInfo["name"] = "Alex"
alexInfo["age"] = 12
alexInfo["score"] = [4]int{150, 150, 150, 300}
fmt.Println(alexInfo)
控制台輸出
map[age:12 name:Alex score:[150 150 150 300]]
3. 接口類型轉換
接口 (包括空接口) 可以存儲所有的值,那么自然會涉及到類型轉換這個話題。我們將分兩部分來討論接口類型轉換,分別是以結構體實現的接口和以指針實現的接口。
3.1 指向結構體的指針實現的接口
這里挖個坑,以后再填
3.2 結構體實現的接口
這里挖個坑,以后再填
3.3 類型斷言
在 2.3.1 節中,我們的代碼已經用到了類型斷言,下面來具體介紹一下類型斷言。
如何把一個接口類型轉換成具體類型 T ?
x.(T)
- 對於非空接口:
package main
import (
"fmt"
)
type OptionForMeat interface {
Boil(int)
Fry(int)
}
type Meat struct {
Name string
}
func (a Meat) Boil(minute int) {
fmt.Printf("煮了%d分鍾的%s了\n", minute, a.Name)
}
func (a Meat) Fry(minute int) {
fmt.Printf("煎了%d分鍾的%s了\n", minute, a.Name)
}
func main() {
var aaa OptionForMeat = &Meat{Name:"Porkchop",}
switch aaa.(type) { // 斷言
case *Meat:
w := aaa.(*Meat)
w.Boil(5)
//aaa.Fry(6) // 這行會輸出 “煎了6分鍾的Porkchop了”
}
}
控制台輸出:
煮了5分鍾的Porkchop了
Go 語言的編譯器對這種情況進行了優化,switch 語句生成的匯編代碼會將目標類型的Hash與接口的itab.Hash進行比較。
- 對於空接口而言
// 只是換一行代碼
var aaa interface{} = &Meat{Name:"Porkchop",}
控制台輸出是一樣的。上述代碼在斷言時並不是直接獲取runtime._type,而是從eface._type獲取類型值,匯編指令仍會使用目標類型的Hash與變量的類型比較。
4. 總結
接口是個抽象數據類型,不要為了寫接口而寫接口,有些不需要接口的地方硬是搞成接口模式,只會帶來不必要的損耗。
希望這篇文章能對你在學習接口的過程中有所幫助。碼字不易,轉載請注明出處。
