1 接口
1.1 接口介紹
接口(interface)是Go語言中核心部分,Go語言提供面向接口編程,那么接口是什么?
現實生活中,有許多接口的例子,比如說電子設備上的充電接口,這個充電接口能干什么,在接口設計時就定義好了,比如說這個接口既能充電可以進行數據的傳輸;之后只需電子設備是實現這個接口的功能,就像手機上的Type-C接口既可以充電又可以數據傳輸。
在Golang中接口(interface)是一種類型,一種抽象的類型。在接口類型中可以定義一組方法,但是這些不需要實現。並且interface不能包含任何變量。對於某個自定義類型可以使用這個接口,但是必須實現這個接口中定義的所有方法。從這點來看,接口不關心事物的屬性,只關心事物具有的行為。
1.2 為什么要使用接口
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())
}
上面的代碼中定義二樓貓和狗,它們都會叫,都有Say()
這個方法,你會發現main
函數中明顯有重復的代碼,無果后續再有其他動物的話,這樣的代碼還會一直重復下去。那我們能不能這樣考慮問題呢,把這些動物歸結成“能叫的動物“,只是不同的動物有不同的叫法。
像類似的例子在編程中經常遇到:
比如一個網上商城可能使用支付寶、微信、銀聯等方式在線支付,那能不能把它們當成“支付方式“來處理呢?
再比如三角形、四邊形、圓形都能計算周長和面積,那能不能把它們當成“圖形”來處理呢?
對於上面的這些問題,Go語言中提供了接口類型。當看到一個接口類型的值是,我們不知道它是什么,唯一知道的是通過它的方法能做什么。
1.3 基本語法
type 接口名 interface {
method1(參數列表) 返回值列表
method2(參數列表) 返回值列表
}
//實現接口所有方法
func (t 自定義類型) method1(參數列表) 返回值列表 {
//方法實現
}
func (t 自定義類型) method2(參數列表) 返回值列表 {
//方法實現
}
接口里的所有方法都沒有方法體,即接口的方法都是沒有實現的方法。接口體現了程序設計的多態和高內聚低耦合的思想。
Golang中的皆苦,不需要顯式的實現,只要一個變量,含有接口類型中的所有方法,那么這個變量就實現了這個接口。
1.4 接口的使用案例
對於1.2中的示例,使用接口實現:
//定義一個Sayer的接口
type Sayer interface {
Say()
}
//定義兩個結構體Cat和Dog
type Cat struct {}
type Dog struct {}
// Dog實現了Sayer接口
func (d Dog) Say() {
fmt.Println("汪汪")
}
// Cat實現了Sayer接口
func (c cat) Say() {
fmt.Println("喵喵")
}
那實現了接口有什么用?
接口類型變量能夠存儲所有實現了該接口的實例。例如上面的示例中,Sayer
類型的變量能夠存儲Dog
和Cat
類型的變量。
func main() {
var x Sayer //聲明一個Sayer類型的變量x
a := Cat{}
b := Dog{}
x = a //只有自定義類型實現了某個接口,才能將自定義類型的變量賦值給接口類型變量
x.Say()
x = b
x.Say()
}
1.5 值類型接收者和指針接收者實現接口的區別
通過下面的例子來看下二者的區別:
//聲明一個Mover的接口
type Mover interface {
move()
}
//聲明一個dog的結構體
type dog struct {}
1.5.1 值類型接收者實現接口
值類型接收者dog
實現Mover
接口:
func (d dog) move() {
fmt.Println("狗會動")
}
func main() {
var x Mover
var dog1 = dog{}
x = dog1
var dog2 = &dog{}
x = dog2 //x可以接收*dog類型
x.move
}
從上面的代碼可以看出,對於struct
如果接收者是值類型,不管是結構體還是結構體指針類型的變量,只要這個結構體實現對應的接口,那么這兩個變量都可以賦值給該接口變量。因為Go語言中有對指針類型變量求值的語法糖,*dog2
等價於dog2
。
1.5.2 指針類型接收者實現接口
指針類型的接收者*dog
實現接口:
func (d *dog) move() {
fmt.Println("狗會動")
}
func main() {
var x Mover
var dog1 = dog{}
x = dog1 //x不可以接收dog類型,會報dog類型沒有實現Mover接口
var dog2 = &dog{}
x = dog2 //x可以接收*dog類型
x.move
}
從上面的示例可知,如果struct
的接收者是指針類型,只能將結構體指針賦值給接口類型的變量。
1.5.3 面試題
下面代碼能否編譯?為什么?
type People interface {
Speak(string) string
}
type Student struct{}
func (stu *Student) Speak(think string) (talk string) {
if think == "sb" {
talk = ""
} else {
talk = "您好"
}
return
}
func main() {
var peo People = Student{}
think := ""
fmt.Println(peo.Speak(think))
}
1.6 空接口
1.6.1 空接口的定義
空接口是指沒有定義任何方法的接口,因此任何類型都實現了空接口。
空接口類型的變量可以存儲任意類型的變量:
func main() {
// 定義一個空接口x
var x interface{}
s := "Hello viktor"
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)
}
1.6.2 空接口的應用
使用空接口可以接收任意類型的函數參數:
// 空接口作為函數參數
func show(a interface{}) {
fmt.Printf("type:%T value:%v\n", a, a)
}
使用空接口可以保存任意值的字典:
// 空接口作為map值
var studentInfo = make(map[string]interface{})
studentInfo["name"] = "viktor"
studentInfo["age"] = 18
studentInfo["married"] = false
fmt.Println(studentInfo)
1.7 接口總結及注意事項
-
接口本身不能創建實例,但是可以指向一個實現了該接口的自定義類型的變量(參考1.5中的示例);
-
接口中的所有的方法都沒有方法體,即都是沒有實現的方法;
-
在Golang中,一個自定義類型需要將某個接口的所有方法都實現,則稱這個自定義類型實現了該接口;
-
一個自定義類型只有實現了某個接口,才能將該自定義類型的實例(變量)賦給接口類型;
-
一個自定義類型可以實現多個接口;
-
Golang接口中不能有任何變量;
-
一個接口(比如A接口)可以嵌套多個別的接口(比如B,C接口),這時如果要實現A接口,必須將B,C接口的方法也全部實現;
// Sayer 接口
type Sayer interface {
say()
}
// Mover 接口
type Mover interface {
move()
}
// 接口嵌套
type animal interface {
Sayer
Mover
}
//cat結構體實現animal接口
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()
}
-
interface類型默認是一個指針(引用類型),如果沒有對interface初始化就使用,那么就會輸出
nil
; -
空接口
interface{}
沒有任何方法,所以所有類型都 實現了空接口,即我們可以把任何一個變量賦給空接口。
2 接口 vs 繼承
從一個事物的角度來看,比如一個籃球運動員或者大學生:
籃球運動員或者大學生可以分別繼承運動員或者學生的一些屬性;但是,籃球運動員或者大學生有可能會有一些相同的行為,比如會說英語,那么就可以定義一個會說英語的接口,分別讓二者實現接口。
接口和繼承的區別:
-
接口和繼承解決的問題不同:繼承的價值主要在於,解決代碼的復用性和可維護性;接口的價值主要在於,設計,設計好各種規范(方法),讓其它自定義類型去實現這些方法;
-
接口比繼承更加靈活。繼承是滿足
is - a
關系,而接口只需滿足like - a
的關系; -
接口在一定程度上實現代碼解耦。
3 面向對象編程-多態
3.1 基本介紹
變量(實例)具有多種形態。面向對象的第三大特征,在Go語言中,多態特征是通過接口實現的。可以按照統一的接口來調用不同的實現。這時接口變量就呈現不同的形態。
3.2 快速入門案例
//聲明/定義一個接口
type Usb interface {
//聲明了兩個沒有實現的方法
Start()
Stop()
}
type Phone struct {
}
//讓Phone 實現 Usb接口的方法
func (p Phone) Start() {
fmt.Println("手機開始工作。。。")
}
func (p Phone) Stop() {
fmt.Println("手機停止工作。。。")
}
type Camera struct {
}
//讓Camera 實現 Usb接口的方法
func (c Camera) Start() {
fmt.Println("相機開始工作~~~。。。")
}
func (c Camera) Stop() {
fmt.Println("相機停止工作。。。")
}
//計算機
type Computer struct {
}
//編寫一個方法Working 方法,接收一個Usb接口類型變量
//只要是實現了 Usb接口 (所謂實現Usb接口,就是指實現了 Usb接口聲明所有方法)
func (c Computer) Working(usb Usb) {//通過usb接口變量來調用Start和Stop方法
usb.Start()
usb.Stop()
}
func main() {
//測試
//先創建結構體變量
computer := Computer{}
phone := Phone{}
camera := Camera{}
//關鍵點
computer.Working(phone)
computer.Working(camera)
}
在上面的代碼中,Working(usb Usb)
方法,既可以接收手機變量,又可以接收相機變量,就體現了Usb
接口多態的特性
3.3 接口體現多態的兩種形式
多態參數
在一個函數或者是一個方法的參數如果是一個接口類型,那么該參數可以接收實現了該接口的所有的自定義類型。如3.2中的案例。
多態數組
自定義類型只要實現了接口,那么都可以存放在接口的數組中。看如下案例:
package main
import (
"fmt"
)
//聲明/定義一個接口
type Usb interface {
//聲明了兩個沒有實現的方法
Start()
Stop()
}
type Phone struct {
name string
}
//讓Phone 實現 Usb接口的方法
func (p Phone) Start() {
fmt.Println("手機開始工作。。。")
}
func (p Phone) Stop() {
fmt.Println("手機停止工作。。。")
}
type Camera struct {
name string
}
//讓Camera 實現 Usb接口的方法
func (c Camera) Start() {
fmt.Println("相機開始工作。。。")
}
func (c Camera) Stop() {
fmt.Println("相機停止工作。。。")
}
func main() {
//定義一個Usb接口數組,可以存放Phone和Camera的結構體變量
//這里就體現出多態數組
var usbArr [3]Usb
usbArr[0] = Phone{"vivo"}
usbArr[1] = Phone{"小米"}
usbArr[2] = Camera{"尼康"}
fmt.Println(usbArr)
}
4 類型斷言
4.1 基本介紹
在前面的所有示例中,都是將一個變量(示例)賦值給一個接口。由於一個接口可以被多個自定義類型實現,我們都知道在Go語言中不同類型之前是不能賦值的,此時與這樣的一個需求,將接口類型的變量賦值給自定義類型的變量,比如下面的代碼,該如何實現?
type Point struct {
x, y int
}
func main() {
var a interface{}
var point Point = Point{1,2}
a = point //ok
//如何將a賦給一個Point變量?
var b Point
b = a //?
fmt.Println(b)
}
類比:可以將接口理解成一個很大的容器,當把一個自定義類型賦給接口,就相當於把它放入這個大容器里面,由於這個容器里面可以放很多不同的自定義類型,當想要把剛才的那個放入容器里面的自定義類型賦給其它的自定義類型,就需要先找到它。這個找的過程就是類型斷言。
類型斷言的基本語法:
x.(T)
其中:
-
x:表示類型為
interface{}
的變量 -
T:表示斷言
x
可能是的類型
該語法返回兩個參數,第一個參數是x
轉化為T
類型后的變量,第二個值是一個布爾值,若為true
則表示斷言成功,為false
則表示斷言失敗。
對上面代碼的改進:
type Point struct {
x, y int
}
func main() {
var a interface{}
var point Point = Point{1,2}
a = point //ok
//如何將a賦給一個Point變量?
var b Point
// b = a //?
b = a.(Point) //類型斷言,表示判斷a是否指向Point類型的變量,如果是就轉成Point類型並賦給b變量,否則報錯
fmt.Println(b)
}
4.2 類型斷言的使用
類型斷言,由於接口是一般類型,不知道具體類型,如果要轉成具體類型,就需要使用類型斷言,具體如下:
func main() {
var x interface{}
var b float32 = 6.6
x = b2 //空接口,可以接收任意類型
//x -> flaot32 [使用類型斷言]
y := x.(float32)
fmt.Printf("y的類型是 %T 值是%v\n", y, y)
}
對於上面代碼,在進行類型斷言時,如果類型不匹配,就會報panic
,因此進行類型短延時,要確保原來的空接口指向的就是斷言的類型。
斷言時也可以帶上檢測機制,如下示例 :
func main() {
var x interface{}
var b2 float32 = 2.1
x = b2 //空接口,可以接收任意類型
// x=>float32 [使用類型斷言]
//類型斷言(帶檢測的)
if y, ok := x.(float32); ok {
fmt.Println("convert success")
fmt.Printf("y 的類型是 %T 值是=%v", y, y)
} else {
fmt.Println("convert fail")
}
fmt.Println("繼續執行...")
}
4.3 類型斷言的最佳實踐
循環判斷傳入參數的類型:
//編寫一個函數,可以判斷輸入的參數是什么類型
func TypeJudge(items... interface{}) {
for index, x := range items {
switch x.(type) {
case bool :
fmt.Printf("第%v個參數是 bool 類型,值是%v\n", index, x)
case float32 :
fmt.Printf("第%v個參數是 float32 類型,值是%v\n", index, x)
case float64 :
fmt.Printf("第%v個參數是 float64 類型,值是%v\n", index, x)
case int, int32, int64 :
fmt.Printf("第%v個參數是 整數 類型,值是%v\n", index, x)
case string :
fmt.Printf("第%v個參數是 string 類型,值是%v\n", index, x)
default :
fmt.Printf("第%v個參數是 類型 不確定,值是%v\n", index, x)
}
}
}
func main() {
var n1 float32 = 1.1
var n2 float64 = 2.3
var n3 int32 = 30
var name string = "tom"
address := "北京"
n4 := 300
TypeJudge(n1, n2, n3, name, address, n4, stu1, stu2)
}