Go語言基礎之介面(面向物件程式設計下)
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)
}