Golang接口使用教程詳解
前言
go語言并沒有面向對象的相關概念,go語言提到的接口和java、c++等語言提到的接口不同,它不會顯示的說明實現(xiàn)了接口,沒有繼承、子類、implements關鍵詞。
一、概述
在 Go 語言中接口包含兩種含義:它既是方法的集合, 同時還是一種類型。在Go 語言中是隱式實現(xiàn)的,意思就是對于一個具體的類型,不需要聲明它實現(xiàn)了哪些接口,只需要提供接口所必需的方法。
go語言通過隱性的方式實現(xiàn)了接口功能,相對比較靈活。
Go語言接口的特點:
- interface 是方法或行為聲明的集合
- interface接口方式實現(xiàn)比較隱性,任何類型的對象實現(xiàn)interface所包含的全部方法,則表明該類型實現(xiàn)了該接口。
- interface還可以作為一種通用的類型,其他類型變量可以給interface聲明的變量賦值。
- interface 可以作為一種數(shù)據(jù)類型,實現(xiàn)了該接口的任何對象都可以給對應的接口類型變量賦值。
二、接口類型
2.1 接口的定義
每個接口類型由任意個方法簽名組成,接口的定義格式如下:
type 接口類型名 interface{
方法名1( 參數(shù)列表1 ) 返回值列表1
方法名2( 參數(shù)列表2 ) 返回值列表2
…
}
說明
- 接口類型名:使用 type 將接口定義為自定義的類型名。Go語言的接口在命名時,一般會在單詞后面添加
er
,如有寫操作的接口叫Writer
,有字符串功能的接口叫Stringer
,有關閉功能的接口叫Closer
等。接口名最好要能突出該接口的類型含義。 - 方法名:當方法名首字母是大寫時,且這個接口類型名首字母也是大寫時,這個方法可以被接口所在的包(package)之外的代碼訪問。
- 參數(shù)列表、返回值列表:參數(shù)列表和返回值列表中的參數(shù)變量名可以被忽略。
舉個例子,定義一個包含Write
方法的Writer
接口。
type writer interface{ Write([]byte) error }
2.2 實現(xiàn)接口的條件
接口就是規(guī)定了一個需要實現(xiàn)的方法列表,在 Go 語言中一個類型只要實現(xiàn)了接口中規(guī)定的所有方法,那么我們就稱它實現(xiàn)了這個接口。
示例
定義的Eater
接口類型,它包含一個Eat
方法。
// Eater 接口 type Eater interface { Eat() }
有一個Dog
結構體類型如下。
type Dog struct {}
因為Eater
接口只包含一個Eat
方法,所以只需要給Dog
結構體添加一個Eat
方法就可以滿足Eater
接口的要求。
//Dog類型的Eat方法 func (d Dog) Eat() { fmt.Println("吃骨頭!") }
這樣就稱為Dog
實現(xiàn)了Eater
接口。
完整代碼
// Eater 接口 type Eater interface { Eat() } type Dog struct {} //Dog類型的Eat方法 func (d Dog) Eat() { fmt.Println("吃骨頭!") } func main() { dog := Dog{} dog.Eat() }
2.3 為什么需要接口
多數(shù)情況下,數(shù)據(jù)可能包含不同的類型,卻會有一個或者多個共同點,這些共同點就是抽象的基礎。
示例
// Eater 接口 type Eater interface { Eat() } type Dog struct {} //Dog類型的Eat方法 func (d Dog) Eat() { fmt.Println("狗狗喜歡吃骨頭!") } type Cat struct {} func (c Cat) Eat(){ fmt.Println("小貓喜歡吃魚!") } func main() { dog := Dog{} dog.Eat() cat := Cat{} cat.Eat() }
從動物身上,可以抽象出來一個eat
方法,這樣即使在擴展其它動物進來,也只需要實現(xiàn)Eater 接口
中的Eat()
方法就可以完成對吃
這個動作的調(diào)用。
接口可以理解為某一個方面的抽象,可以是多對一的(多個類型實現(xiàn)一個接口),這也是多態(tài)的體現(xiàn)。
2.4 接口類型變量
一個接口類型的變量能夠存儲所有實現(xiàn)了該接口的類型變量。
例如在上面的示例中,Dog
和Cat
類型均實現(xiàn)了Eater
接口,此時一個Eater
類型的變量就能夠接收Cat
和Dog
類型的變量。
var x Eater // 聲明一個Eater類型的變量x a := Cat{} // 聲明一個Cat類型變量a b := Dog{} // 聲明一個Dog類型變量b x = a // 可以把Cat類型變量直接賦值給x x.Eat() // 小貓喜歡吃魚! x = b // 可以把Dog類型變量直接賦值給x x.Eat() // 狗狗喜歡吃骨頭!
三、值接收者和指針接收者
通過下方一個示例來演示實現(xiàn)接口使用值接收者和使用指針接收者有什么區(qū)別。
定義一個Mover
接口,它包含一個Move
方法。
// Mover 定義一個接口類型 type Mover interface { Move() }
3.1 值接收者實現(xiàn)接口
我們定義一個Dog
結構體類型,并使用值接收者為其定義一個Move
方法。
// Dog 狗結構體類型 type Dog struct{} // Move 使用值接收者定義Move方法實現(xiàn)Mover接口 func (d Dog) Move() { fmt.Println("狗會動") }
此時實現(xiàn)Mover
接口的是Dog
類型。
var x Mover // 聲明一個Mover類型的變量x var d1 = Dog{} // d1是Dog類型 x = d1 // 可以將d1賦值給變量x x.Move() var d2 = &Dog{} // d2是Dog指針類型 x = d2 // 也可以將d2賦值給變量x x.Move()
從上面的代碼中我們可以發(fā)現(xiàn),使用值接收者實現(xiàn)接口之后,不管是結構體類型還是對應的結構體指針類型的變量都可以賦值給該接口變量。
3.2 指針接收者實現(xiàn)接口
我們再來測試一下使用指針接收者實現(xiàn)接口有什么區(qū)別。
// Cat 貓結構體類型 type Cat struct{} // Move 使用指針接收者定義Move方法實現(xiàn)Mover接口 func (c *Cat) Move() { fmt.Println("貓會動") }
此時實現(xiàn)Mover
接口的是*Cat
類型,我們可以將*Cat
類型的變量直接賦值給Mover
接口類型的變量x
。
var c1 = &Cat{} // c1是*Cat類型 x = c1 // 可以將c1當成Mover類型 x.Move()
但是不能給將Cat
類型的變量賦值給Mover
接口類型的變量x
。
// 下面的代碼無法通過編譯 var c2 = Cat{} // c2是Cat類型 x = c2 // 不能將c2當成Mover類型
由于Go語言中有對指針求值的語法糖,對于值接收者實現(xiàn)的接口,無論使用值類型還是指針類型都沒有問題。但是我們并不總是能對一個值求址,所以對于指針接收者實現(xiàn)的接口要額外注意。
四、類型與接口的關系
4.1 一個類型實現(xiàn)多個接口
一個類型可以同時實現(xiàn)多個接口,而接口間彼此獨立,不知道對方的實現(xiàn)。
示例
動物不僅有吃的屬性,還有動的屬性,可以通過定義兩個接口,讓同一個動物分別實現(xiàn)這兩種屬性
// Eater 接口 type Eater interface { Eat() } // Mover 接口 type Mover interface { Move() } type Dog struct {} //Dog類型的Eat方法 func (d Dog) Eat() { fmt.Println("狗狗喜歡吃骨頭!") } //Dog類型的Move方法 func (d Dog) Move(){ fmt.Println("狗狗喜歡玩耍!") } func main() { //初始化結構體 dog := Dog{} //dog實現(xiàn)了Eater和Mover兩個接口 eat := dog move := dog eat.Eat() //對Eater類型調(diào)用Eat方法 move.Move() //對Mover類型調(diào)用Move方法 }
程序中的結構體Dog
分別實現(xiàn)了Eater和Mover兩個接口中的方法。
4.2 多種類型實現(xiàn)同一接口
Go語言中不同的類型還可以實現(xiàn)同一接口。
一個接口的所有方法,不一定需要由一個類型完全實現(xiàn),接口的方法可以通過在類型中嵌入其他類型或者結構體來實現(xiàn)。
// WashingMachine 洗衣機 type WashingMachine interface { wash() dry() } // 甩干器 type dryer struct{} // 實現(xiàn)WashingMachine接口的dry()方法 func (d dryer) dry() { fmt.Println("甩一甩") } // 洗衣機 type haier struct { dryer //嵌入甩干器 } // 實現(xiàn)WashingMachine接口的wash()方法 func (h haier) wash() { fmt.Println("洗刷刷") } func main() { h := haier{} h.dry() h.wash() }
五、接口嵌套
接口與接口之間可以通過互相嵌套形成新的接口類型。例如Go標準庫io
源碼中就有很多接口之間互相組合的示例。
// src/io/io.go type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) } type Closer interface { Close() error } // ReadWriter 是組合Reader接口和Writer接口形成的新接口類型 type ReadWriter interface { Reader Writer } // ReadCloser 是組合Reader接口和Closer接口形成的新接口類型 type ReadCloser interface { Reader Closer } // WriteCloser 是組合Writer接口和Closer接口形成的新接口類型 type WriteCloser interface { Writer Closer }
對于這種由多個接口類型組合形成的新接口類型,同樣只需要實現(xiàn)新接口類型中規(guī)定的所有方法就算實現(xiàn)了該接口類型。
接口也可以作為結構體的一個字段,我們來看一段Go標準庫sort
源碼中的示例。
// src/sort/sort.go // Interface 定義通過索引對元素排序的接口類型 type Interface interface { Len() int Less(i, j int) bool Swap(i, j int) } // reverse 結構體中嵌入了Interface接口 type reverse struct { Interface }
通過在結構體中嵌入一個接口類型,從而讓該結構體類型實現(xiàn)了該接口類型,并且還可以改寫該接口的方法。
// Less 為reverse類型添加Less方法,重寫原Interface接口類型的Less方法 func (r reverse) Less(i, j int) bool { return r.Interface.Less(j, i) }
Interface
類型原本的Less
方法簽名為Less(i, j int) bool
,此處重寫為r.Interface.Less(j, i)
,即通過將索引參數(shù)交換位置實現(xiàn)反轉。
在這個示例中還有一個需要注意的地方是reverse
結構體本身是不可導出的(結構體類型名稱首字母小寫),sort.go
中通過定義一個可導出的Reverse
函數(shù)來讓使用者創(chuàng)建reverse
結構體實例。
func Reverse(data Interface) Interface { return &reverse{data} }
這樣做的目的是保證得到的reverse
結構體中的Interface
屬性一定不為nil
,否者r.Interface.Less(j, i)
就會出現(xiàn)空指針panic。
六、空接口
Golang 中的接口可以不定義任何方法,沒有定義任何方法的接口就是空接口。空接口表示沒有任何約束,因此任何類型變量都可以實現(xiàn)空接口。
空接口在實際項目中用的是非常多的,用空接口可以表示任意數(shù)據(jù)類型。
示例
func main() { //定義一個空接口x,x變量可以接收任意的數(shù)據(jù)類型 var x interface{} str := "Hello Go" x = str fmt.Printf("type:%T,value:%v\n",x,x) num := 10 x = num fmt.Printf("type:%T,value:%v\n",x,x) bool := true x = bool fmt.Printf("type:%T,value:%v\n",x,x) }
運行結果
type:string,value:Hello Go
type:int,value:10
type:bool,value:true
1、空接口作為函數(shù)的參數(shù)
// 空接口作為函數(shù)參數(shù) func show(a interface{}) { fmt.Printf("type:%T value:%v\n", a, a) } func main() { show(1) show(true) show(3.14) var mapStr = make(map[string]string) mapStr["name"] = "Leefs" mapStr["age"] = "12" show(mapStr) }
運行結果
type:int value:1
type:bool value:true
type:float64 value:3.14
type:map[string]string value:map[age:12 name:Leefs]
2、map的值實現(xiàn)空接口
func main() { // 空接口作為 map 值 var studentInfo = make(map[string]interface{}) studentInfo["name"] = "Jeyoo" studentInfo["age"] = 18 studentInfo["married"] = false fmt.Println(studentInfo) }
運行結果
map[age:18 married:false name:Jeyoo]
3、切片實現(xiàn)空接口
func main() { var slice = []interface{}{"Jeyoo", 20, true, 32.2} fmt.Println(slice) }
運行結果
[Jeyoo 20 true 32.2]
七、類型斷言
一個接口的值(簡稱接口值)是由一個具體類型和具體類型的值兩部分組成的。這兩部分分別稱為接口的動態(tài)類型和動態(tài)值。
如果我們想要判斷空接口中值的類型,那么這個時候就可以使用類型斷言,其語法格式:
x.(T)
說明
- x: 表示類型為 interface{}的變量
- T: 表示斷言 x 可能是的類型
該語法返回兩個參數(shù),第一個參數(shù)是 x 轉化為 T 類型后的變量,第二個值是一個布爾值,若為 true 則表示斷言成功,為 false 則表示斷言失敗。
示例
func main() { var x interface{} x = "Hello GO" v, ok := x.(string) if ok { fmt.Println(v) } else { fmt.Println("類型斷言失敗") } }
上面的示例中如果要斷言多次就需要寫多個 if 判斷,這個時候我們可以使用 switch 語句來 實現(xiàn):
注意:類型.(type)只能結合 switch 語句使用
// justifyType 對傳入的空接口類型變量x進行類型斷言 func justifyType(x interface{}) { switch v := x.(type) { case string: fmt.Printf("x is a string,value is %v\n", v) case int: fmt.Printf("x is a int is %v\n", v) case bool: fmt.Printf("x is a bool is %v\n", v) default: fmt.Println("unsupport type!") } }
由于接口類型變量能夠動態(tài)存儲不同類型值的特點,所以很多初學者會濫用接口類型(特別是空接口)來實現(xiàn)編碼過程中的便捷。
只有當有兩個或兩個以上的具體類型必須以相同的方式進行處理時才需要定義接口。切記不要為了使用接口類型而增加不必要的抽象,導致不必要的運行時損耗。
總結
在 Go 語言中接口是一個非常重要的概念和特性,使用接口類型能夠實現(xiàn)代碼的抽象和解耦,也可以隱藏某個功能的內(nèi)部實現(xiàn),但是缺點就是在查看源碼的時候,不太方便查找到具體實現(xiàn)接口的類型。
相信很多讀者在剛接觸到接口類型時都會有很多疑惑,請牢記接口是一種類型,一種抽象的類型。區(qū)別于我們在之前章節(jié)提到的那些具體類型(整型、數(shù)組、結構體類型等),它是一個只要求實現(xiàn)特定方法的抽象類型。
以上就是Golang接口使用教程詳解的詳細內(nèi)容,更多關于Golang接口的資料請關注腳本之家其它相關文章!