Golang通脈之介面
介面(interface
)定義了一個對象的行為規範,只定義規範不實現,由具體的對象來實現規範的細節。
介面類型
在Go語言中介面(interface)是一種類型,一種抽象的類型。
interface
是一組函數或方法的集合,是duck-type programming
的一種體現。介面做的事情就像是定義一個協議(規則),不關心屬性(數據),只關心行為(方法),請牢記介面(interface
)是一種類型。
介面與鴨子類型:
維基百科的定義:
If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.
翻譯過來就是:如果某個東西長得像鴨子,像鴨子一樣游泳,像鴨子一樣嘎嘎叫,那它就可以被看成是一隻鴨子。
Duck Typing,鴨子類型,是動態程式語言的一種對象推斷策略,它更關注對象能如何被使用,而不是對象的類型本身。Go 語言作為一門靜態語言,它通過介面的方式完美支援鴨子類型。
而在靜態語言如 Java, C++ 中,必須要顯示地聲明實現了某個介面之後,才能用在任何需要這個介面的地方。如果你在程式中調用某個數,卻傳入了一個根本就沒有實現另一個的類型,那在編譯階段就不會通過。這也是靜態語言比動態語言更安全的原因。
動態語言和靜態語言的差別在此就有所體現。靜態語言在編譯期間就能發現類型不匹配的錯誤,不像動態語言,必須要運行到那一行程式碼才會報錯。當然,靜態語言要求程式設計師在編碼階段就要按照規定來編寫程式,為每個變數規定數據類型,這在某種程度上,加大了工作量,也加長了程式碼量。動態語言則沒有這些要求,可以讓人更專註在業務上,程式碼也更短,寫起來更快。
Go 語言作為一門現代靜態語言,是有後發優勢的。它引入了動態語言的便利,同時又會進行靜態語言的類型檢查。Go 採用了折中的做法:不要求類型顯示地聲明實現了某個介面,只要實現了相關的方法即可,編譯器就能檢測到。
總結一下,鴨子類型是一種動態語言的風格,在這種風格中,一個對象有效的語義,不是由繼承自特定的類或實現特定的介面,而是由它”當前方法和屬性的集合”決定。Go 作為一種靜態語言,通過介面實現了鴨子類型,實際上是 Go 的編譯器在其中作了隱匿的轉換工作。
Go語言的多態性:
在Java語言中,多態是通過繼承和重寫來體現的,而Go中的多態性就是在介面的幫助下實現的。介面可以在Go中隱式地實現。如果類型為介面中聲明的所有方法提供了定義,則該類型實現了這個介面。
任何定義了介面所有方法的類型都被稱為隱式地實現了該介面。
類型介面的變數可以保存實現介面的任何值。介面的這個屬性用於實現Go中的多態性。
什麼是介面
簡言之:
- 介面是一組方法簽名
- 介面把所有的具有共性的方法定義在一起,任何其他類型只要實現了這些方法就是實現了這個介面
介面的定義
Go語言提倡面向介面編程。
每個介面由數個方法組成,介面的定義格式如下:
type 介面類型名 interface{
方法名1( 參數列表1 ) 返回值列表1
方法名2( 參數列表2 ) 返回值列表2
…
}
其中:
- 介面名:使用
type
將介面定義為自定義的類型名。Go語言的介面在命名時,一般會在單詞後面添加er
,如有寫操作的介面叫Writer
,有字元串功能的介面叫Stringer
等。介面名最好要能突出該介面的類型含義。 - 方法名:當方法名首字母是大寫且這個介面類型名首字母也是大寫時,這個方法可以被介面所在的包(package)之外的程式碼訪問。
- 參數列表、返回值列表:參數列表和返回值列表中的參數變數名可以省略。
舉個例子:
type writer interface{
Write([]byte) error
}
當你看到這個介面類型的值時,你不知道它是什麼,唯一知道的就是可以通過它的Write方法來做一些事情。
實現介面的條件
一個類型只要實現了介面中的全部方法,那麼就實現了這個介面。換句話說,介面就是一組需要實現的方法簽名。
定義一個介面並實現它:
type IPhone interface {
call()
}
type NokiaPhone struct {
}
func (nokiaPhone NokiaPhone) call() {
fmt.Println("I am Nokia, I can call you!")
}
type MobilePhone struct {
}
func (mobilePhone MobilePhone) call() {
fmt.Println("I am mobile, I can call you!")
}
介面的實現就是這麼簡單,只要實現了介面中的所有方法,就實現了這個介面。
介面類型變數
為什麼要實現介面呢?
介面類型變數能夠存儲所有實現了該介面的實例。 例如上面的示例中,IPhone
類型的變數能夠存儲NokiaPhone
和MobilePhone
類型的變數。
func main() {
var x IPhone // 聲明一個Sayer類型的變數x
a := NokiaPhone{} // 實例化一個NokiaPhone
b := MobilePhone{} // 實例化一個MobilePhone
x = a // 可以把NokiaPhone實例直接賦值給x
x.call() // I am Nokia, I can call you!
x = b // 可以把Phone實例直接賦值給x
x.call() // I am mobile, I can call you!
}
Tips: 觀察下面的程式碼,體味此處_
的妙用
// 摘自gin框架routergroup.go
type IRouter interface{ ... }
type RouterGroup struct { ... }
var _ IRouter = &RouterGroup{} // 確保RouterGroup實現了介面IRouter
值接收者和指針接收者實現介面的區別
使用值接收者實現介面和使用指針接收者實現介面有什麼區別呢?
定義一個方法,有值類型接收者和指針類型接收者兩種。二者都可以調用方法,因為 Go 語言編譯器自動做了轉換,所以值類型接收者和指針類型接收者是等價的。但是在介面的實現中,值類型接收者和指針類型接收者不一樣。
type Stringer interface {
String() string
}
type person struct {
name string
age uint
addr address
}
type address struct {
province string
city string
}
func (p person) String() string{
return fmt.Sprintf("the name is %s,age is %d",p.name,p.age)
}
func (addr address) String() string{
return fmt.Sprintf("the addr is %s%s",addr.province,addr.city)
}
func printString(s fmt.Stringer){
fmt.Println(s.String())
}
func main(){
p := person{}
printString(p) //正常輸出
printString(p.addr) //正常輸出
printString(&p) //正常輸出
}
把變數 p 的指針作為實參傳給 printString 函數也是可以的,編譯運行都正常。這就證明了以值類型接收者實現介面的時候,不管是類型本身,還是該類型的指針類型,都實現了該介面。
值接收者(p person
)實現了 Stringer
介面,那麼類型 person
和它的指針類型*person
就都實現了 Stringer
介面。
再把接收者改成指針類型:
type Stringer interface {
String() string
}
type person struct {
name string
age uint
addr address
}
type address struct {
province string
city string
}
func (p *person) String() string{
return fmt.Sprintf("the name is %s,age is %d",p.name,p.age)
}
func (addr address) String() string{
return fmt.Sprintf("the addr is %s%s",addr.province,addr.city)
}
func printString(s fmt.Stringer){
fmt.Println(s.String())
}
func main(){
p := person{}
printString(p) //cannot use p (type person) as type fmt.Stringer in argument to printString:person does not implement fmt.Stringer (String method has pointer receiver)
printString(&p) //正常輸出
}
修改成指針類型接收者後會發現,提示錯誤:
cannot use p (type person) as type fmt.Stringer in argument to printString:person does not implement fmt.Stringer (String method has pointer receiver)
意思就是類型 person
沒有實現 Stringer
介面。這就證明了以指針類型接收者實現介面的時候,只有對應的指針類型才被認為實現了該介面。
總結:
當值類型作為接收者時,person
類型和*person
類型都實現了該介面。
當指針類型作為接收者時,只有*person
類型實現了該介面。
可以發現,實現介面的類型都有*person
,這也表明指針類型比較萬能,不管哪一種接收者,它都能實現該介面。
類型與介面的關係
一個類型實現多個介面
一個類型可以同時實現多個介面,而介面間彼此獨立,不知道對方的實現。 例如,狗可以叫,也可以動。分別定義Sayer介面和Mover介面:
// Sayer 介面
type Sayer interface {
say()
}
// Mover 介面
type Mover interface {
move()
}
dog既可以實現Sayer介面,也可以實現Mover介面。
type dog struct {
name string
}
// 實現Sayer介面
func (d dog) say() {
fmt.Printf("%s會叫\n", d.name)
}
// 實現Mover介面
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
方法。
// Mover 介面
type Mover interface {
move()
}
如狗可以動,汽車也可以動,實現這個關係:
type dog struct {
name string
}
type car struct {
brand string
}
// dog類型實現Mover介面
func (d dog) move() {
fmt.Printf("%s會跑\n", d.name)
}
// car類型實現Mover介面
func (c car) move() {
fmt.Printf("%s速度70邁\n", c.brand)
}
這個時候可以把狗和汽車當成一個會動的物體來處理了,不再需要關注它們具體是什麼,只需要調用它們的move
方法就可以了。
func main() {
var x Mover
var a = dog{name: "旺財"}
var b = car{brand: "保時捷"}
x = a
x.move()
x = b
x.move()
}
並且一個介面的方法,不一定需要由一個類型完全實現,介面的方法可以通過在類型中嵌入其他類型或者結構體來實現。
// WashingMachine 洗衣機
type WashingMachine interface {
wash()
dry()
}
// 甩干器
type dryer struct{}
// 實現WashingMachine介面的dry()方法
func (d dryer) dry() {
fmt.Println("脫水")
}
// 海爾洗衣機
type haier struct {
dryer //嵌入甩干器
}
// 實現WashingMachine介面的wash()方法
func (h haier) wash() {
fmt.Println("洗衣服")
}
介面嵌套
介面與介面間可以通過嵌套創造出新的介面。
// Sayer 介面
type Sayer interface {
say()
}
// Mover 介面
type Mover interface {
move()
}
// 介面嵌套
type animal interface {
Sayer
Mover
}
嵌套得到的介面的使用與普通介面一樣。
空介面
空介面的定義
空介面是指沒有定義任何方法簽名的介面。由於任何類型都至少實現了0個方法,因此任何類型都實現了空介面。
空介面類型的變數可以存儲任意類型的變數。
type I interface{}
func main() {
var i I
i = 42 //這個時候i就是int類型
fmt.Printf("%v,%T\n", i, i)
i = "hello" //這個時候i就是string類型
fmt.Printf("%v,%T\n", i, i)
}
42,int
hello,string
空介面的應用
空介面作為函數的參數
使用空介面實現可以接收任意類型的函數參數。
// 空介面作為函數參數
func show(a interface{}) {
fmt.Printf("type:%T value:%v\n", a, a)
}
空介面作為map的值
使用空介面實現可以保存任意值的映射。
// 空介面作為map值
var person = make(map[string]interface{})
person["name"] = "張三"
person["age"] = 23
person["married"] = false
fmt.Println(person)
空介面對切片的影響
- 若將一個
array
或slice
賦值給空介面,這個空介面無法再進行切片 array
或slice
賦值給空介面的行為不是複製,而是類似指針效果,只不過無法再進行切片,但元素和原來的array
、slice
及其衍生的,都有關聯
func main() {
s := []int{2, 3, 5, 7, 11, 13}
var e interface{}
e = s
f := s[0:3]
f[2] = 55
fmt.Printf("%T,%v\n", s, s)
fmt.Printf("%T,%v\n", e, e)
fmt.Printf("%T,%v\n", f, f)
}
輸出
[]int,[2 3 55 7 11 13]
[]int,[2 3 55 7 11 13]
[]int,[2 3 55]
若改為
func main() {
s := []int{2, 3, 5, 7, 11, 13}
var e interface{}
e = s
g := e[1:3]
fmt.Println(g)
}
報錯
cannot slice e (type interface {})
空介面的賦值
空介面可以存儲任意值,但不代表任意類型就可以存儲空介面類型的值
從實現的角度看,任何類型的值都滿足空介面。因此空介面類型可以保存任何值,也可以從空介面中取出原值。
但要是把一個空介面類型的對象,再賦值給一個固定類型(比如 int
, string
等類型)的對象賦值,是會報錯的。
func main() {
// 聲明a變數, 類型int, 初始值為1
var a int = 1
// 聲明i變數, 類型為interface{}, 初始值為a, 此時i的值變為1
var i interface{} = a
// 聲明b變數, 嘗試賦值i
var b int = i
}
這個報錯,它就好比可以放進行禮箱的東西,肯定能放到集裝箱里,但是反過來,能放到集裝箱的東西就不一定能放到行禮箱了,在 Go 里就直接禁止了這種反向操作。
cannot use i (type interface {}) as type int in assignment: need type assertion
類型斷言
空介面可以存儲任意類型的值,那如何獲取其存儲的具體數據呢?
介面值
一個介面的值(簡稱介面值)是由一個具體類型
和具體類型的值
兩部分組成的。這兩部分分別稱為介面的動態類型
和動態值
:
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil
請看下圖分解:
想要判斷空介面中的值這個時候就可以使用類型斷言,其語法格式:
// 安全類型斷言
<目標類型的值>,<布爾參數> := <表達式>.( 目標類型 )
//非安全類型斷言
<目標類型的值> := <表達式>.( 目標類型 )
示例程式碼:
func main() {
var i1 interface{} = new (Student)
s := i1.(Student) //不安全,如果斷言失敗,會直接panic
fmt.Println(s)
var i2 interface{} = new(Student)
s, ok := i2.(Student) //安全,斷言失敗,也不會panic,只是ok的值為false
if ok {
fmt.Println(s)
}
}
type Student struct {}
斷言其實還有另一種形式,就是用在利用 switch
語句判斷介面的類型。每一個case
會被順序地考慮。當命中一個case
時,就會執行 case
中的語句,因此 case
語句的順序是很重要的,因為很有可能會有多個 case
匹配的情況:
switch ins:=s.(type) {
case Triangle:
fmt.Println("三角形。。。",ins.a,ins.b,ins.c)
case Circle:
fmt.Println("圓形。。。。",ins.radius)
case int:
fmt.Println("整型數據。。")
}
因為空介面可以存儲任意類型值的特點,所以空介面在Go語言中的使用十分廣泛。
關於介面需要注意的是,只有當有兩個或兩個以上的具體類型必須以相同的方式進行處理時才需要定義介面。不要為了介面而寫介面,那樣只會增加不必要的抽象,導致不必要的運行時損耗。