接口
接口(interface)定義了一個對象的行爲規範,只定義規範不實現,由具體的對象來實現規範的細節。
接口類型
在Go語言中接口(interface)是一種類型,是一種抽象的類型。
interface是一組method的集合,是duck-type programming的一種體現。接口做的事情就像是定義一個協議(規則),只要一臺機器有洗衣服和甩乾的功能,我就稱它爲洗衣機。不關心屬性(數據),只關心行爲(方法)。
爲了保護你的Go語言職業生涯,請牢記接口(interface)是一種類型。
爲什麼要使用接口
package main
import (
"fmt"
)
type Cat struct{}
func (c Cat)Say() string{
return "喵喵喵"
}
type Dog struct{}
func (d Dog)Say() string{
return "汪汪汪"
}
func main(){
c := Cat{}
fmt.Println("貓:",c.Say())
d := Dog{}
fmt.Println("狗:",d.Say())
}
上面的代碼中定義了貓和狗,然後它們都會叫,你會發現main函數中明顯有重複的代碼,如果我們後續再加上豬、青蛙等動物的話,我們的代碼還會一直重複下去。那我們能不能把它們當成“能叫的動物”來處理呢?
類似的例子在我們編程的過程中會經常遇到:
比如一個網上商城可能使用支付寶、微信、銀聯等方式去在線支付,我們能不能把它們當成“支付方式”來處理呢?
比如三角形、四邊形、圓形都能計算周長和麪積,我們能不能把它們都當成“圖形”來處理呢?
比如銷售、行政、程序員都能計算月薪,我們能不能把它們都當成“員工”來處理呢?
Go語言中爲了解決類似上面的問題,就設計了接口這個概念。接口區別於我們之前所有的具體類型,接口是一種抽象的類型,當你看到一個接口類型的值時,你不知道它是什麼,唯一知道的是通過它的方法能做什麼。
接口的定義
Go語言提倡面向接口編程。
每個接口由數個方法組成,接口的定義格式如下:
type 接口類型名 interface {
方法名1(參數列表1) 返回值列表1
方法名2(參數列表2) 返回值列表2
...
}
其中:
- 接口名:使用type將接口定義爲自定義的類型名。Go語言的接口在命名時,一般會在單詞後面添加er,如有寫操作的接口叫writer,有字符串功能的接口叫Stringer等。接口名最好能突出該接口的類型含義。
- 方法名:當方法名首字母是大寫且這個接口類型名首字母也是大寫時,這個方法可以被接口所在的包(package)之外的代碼訪問。
- 參數列表、返回值列表:參數列表和返回值列表中的參數變量名可以省略。
舉個例子:
type writer interface{
Write([]byte) err
}
當你看到這個接口類型的值時,你不知道它是什麼,但是你唯一知道的就是可以通過它的Write方法來做一些事情。
實現接口的條件
一個對象只要全部實現了接口裏面定義的方法,那麼他就是現實了這個接口。我們來定義一個Animal接口:
type Sayer interface{
say()
}
定義dog和cat兩個結構體
type dog struct{}
type cat struct{}
因爲Sayer接口裏只有一個say方法,所以我們只需要給dog和cat分別實現say方法就可以實現Sayer接口了。
//dog實現了Sayer接口
func (d dog) say(){
fmt.Println("汪汪汪")
}
//cat實現了Sayer接口
func (c cat) say(){
fmt.Println("喵喵喵")
}
接口的實現就是這麼簡單,只要實現了接口中的所有方法,就實現了這個接口。
接口類型變量
那實現了接口由什麼用呢?
接口類型變量能夠存儲所有實現了該接口的實例,例如上面的示例中,Sayer類型的變量能夠存儲dog和cat類型的變量。
func main(){
var x Sayer
a := cat{}
b := dog{}
x =a
x.say()
x = b
x.say()
}
值接收者和指針接收者實現接口的區別
使用值接收者實現接口和使用指針接收者實現接口有什麼區別呢?接下來我們通過一個例子看一下其中的區別。
我們有一個Mover接口和一個dog結構體。
type Mover interface{
move()
}
type dog struct{}
值接收者實現接口
func (d dog)move(){
fmt.Println("狗會動")
}
此時實現接口的是dog 類型:
func main(){
var x Mover
var wangcai = dog{}
x = wangcai
var fugui = &dog{}
x = fugui
x.move()
}
從上面的代碼中我們可以發現,使用值接收者實現接口之後,不管是dog結構體還是結構體指針dog類型的變量都可以賦值給該接口變量。因爲Go語言中有對指針類型變量求值的語法糖,dog指針fugui內部會自動求值fuzhi。
指針接收者實現接口
同樣的代碼我們再來測試一下使用指針接收者有什麼區別:
func(d *dog)move(){
fmt.Println("狗會動")
}
func main(){
var x Mover
var wangcai = dog{}
x = wangcai
var fugui = &dog{}
x = fugui
}
此時實現Mover接口的是*dog類型,所以不能給x傳入dog類型的wangcai, 此時x只能存儲 **dog類型的值。
類型與接口的關係
一個類型可以同時實現多個接口,而接口間彼此獨立,不知道對方的實現。例如,狗可以叫,也可以動,我們就分別定義Sayer接口和Mover接口,如下: Mover接口。
type Sayer interface{
say()
}
type Mover interface{
move()
}
dog既可以實現Sayer接口,也可以實現Mover接口。
type Sayer interface{
say()
}
type Mover interface{
move()
}
type dog struct{
name string
}
func (d dog)say(){
fmt.Printf("%s會叫汪汪汪\n",d.name)
}
func (d dog)move(){
fmt.Printf("%s會動\n",d.name)
}
func main(){
var x Sayer
var y Mover
var a = dog{
name:"旺財"
}
x = a
y = a
x.say()
y.move()
}
多個類型實現同一個接口
GO語言中不同的類型還可以實現同一個接口,首先我們定義一個Mover接口,它必須由一個move方法。
type dog struct{
name string
}
type car struct{
brand string
}
func (d dog)move(){
fmt.Printf("%s會跑\n",d.name)
}
func (c car)move(){
fmt.Printf("%s速度70邁\n",c.brand)
}
func main(){
var x Mover
var a = dog{name:"旺財"}
var b = car{brand:"寶馬"}
x = a
x.move()
x = b
x.move()
}
上面的帶啊執行結果如下:
旺財會跑
寶馬速度70邁
並且一個接口方法,不一定需要一個類型完全實現,接口的方法可以通過在類型中嵌入其他類型或者結構體來實現。
type washingmachine interface{
wash()
dry()
}
type dryer struct{}
func (d dryer) dry(){
fmt.Println("甩一甩")
}
type haier struct{
dryer
}
func (h haier)wash(){
fmt.Println("洗刷刷")
}
接口嵌套
接口與接口間可以通過嵌套創造出新的接口。
type Sayer interface{
say()
}
type Mover interface{
move()
}
type animal interface{
Sayer
Mover
}
嵌套得到的接口的使用和普通接口一樣,這裏我們讓cat實現animal接口:
type Sayer interface{
say()
}
type Mover interface{
move()
}
type animal interface{
Sayer
Mover
}
type cat struct {
name string
}
func (c cat)say(){
fmt.println("喵喵喵")
}
func (c cat)move(){
fmt.Println("貓會動")
}
func main(){
var x animal
x = cat{name:"花花"}
x.move()
x.say()
}
空接口
空接口的定義
空接口類型的變量可以存儲任何類型的變量。
func main(){
var x interface{}
s := "hello 沙河"
x = s
fmt.Printf("type:%T value:%v\n",x,x)
i :=100
x = i
fmt.Printf("type:%T value:%v\n", x,x)
b := true
x = b
fmt.Printf("type:%T value:%v\n", x,x )
}
空接口的應用
空接口作爲函數的參數
使用空接口實現可以接收任意類型的函數參數。
func show(a interface{}){
fmt.Printf("type:%T value:%v\n", a,a)
}
空接口作爲map的值
使用空接口實現可以保存任意值的字典。
var studentInfo = make(map[string]interface{})
studentInfo["name"] = "沙河娜扎"
studentInfo["age"] = 18
studentInfo["married"] = false
fmt.Println(studentInfo)
類型斷言
空接口可以存儲任意類型的值,那我們如何獲取其存儲的具體數據呢?
接口值
一個接口的值(簡稱接口值)是由 “一個具體類型” 和 “具體類型的值” 兩部分組成的。這兩部分分別稱爲接口的 “動態類型”和“動態值”。
我們來看一個具體的例子:
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil
想要判斷空接口的值這個時候就可以使用類型斷言,其語法格式爲:
x.(T)
其中:
x 表示類型爲interface{}的變量。
T表示斷言x可能是的類型。
該語法返回兩個參數,第一個參數是x轉化爲T類型後的變量,第二個值是一個布爾值,若爲true則表示斷言成功,若爲false則表示斷言失敗。
舉個例子:
func main(){
var x interface{}
x = "hello 沙河"
v,ok := x.(string)
if ok {
fmt.Println(v)
}else {
fmt.Println("類型斷言失敗")
}
}
上面的示例中如果需要斷言多次就需要寫多個if判斷,這個時候我們可以使用switch語句來實現:
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,value is %v\n",v)
case bool:
fmt.Printf("x is a bool, value is %v\n",v)
default:
fmt.Printf("unsupport type!")
}
}
因爲空接口可以存儲任意類型值的特點,所以空接口在Go語言中的使用十分廣泛。
關於接口需要注意的是,只有當有兩個或者兩個以上的具體類型必須以相同的方式進行處理時才需要定義接口。不要爲了接口而寫接口。那樣只會增加不必要的抽象,導致不必要的運行時損耗。