Go 面向?qū)ο缶幊唐航涌谫x值

一、接口賦值概述
在上篇教程中,學(xué)院君給大家介紹了 Go 接口的定義及實(shí)現(xiàn),和其他編程語(yǔ)言一樣,Go 接口不支持直接實(shí)例化,因?yàn)樗皇且粋€(gè)契約而已,只能通過具體的類來實(shí)現(xiàn)接口聲明的所有方法。不同之處在于,Go 接口支持賦值操作,從而快速實(shí)現(xiàn)接口與實(shí)現(xiàn)類的映射,與之相比,Java、PHP 要實(shí)現(xiàn)接口與實(shí)現(xiàn)類的映射,只能基于 IoC 容器通過依賴注入實(shí)現(xiàn),要復(fù)雜的多。
接口賦值在 Go 語(yǔ)言中分為如下兩種情況:
將實(shí)現(xiàn)接口的類實(shí)例賦值給接口;
將一個(gè)接口賦值給另一個(gè)接口。
下面我們通過代碼實(shí)例逐個(gè)介紹對(duì)應(yīng)的實(shí)現(xiàn)和注意事項(xiàng)。
二、將類實(shí)例賦值給接口
先看看將類實(shí)例賦值給接口,這要求該實(shí)例對(duì)應(yīng)的類實(shí)現(xiàn)了接口聲明的所有方法,這個(gè)是自然,否則也就不能算作實(shí)現(xiàn)該接口了。
只包含值方法
我們以之前為基本類型添加成員方法時(shí)定義過的 Integer 類型為例進(jìn)行演示:
type Integer int
// 加法運(yùn)算
func (a Integer) Add(b Integer) Integer {
return a + b
}
// 乘法運(yùn)算
func (a Integer) Multiply(b Integer) Integer {
return a * b
}
type Math interface {
Add(i Integer) Integer
Multiply(i Integer) Integer
}
按照 Go 語(yǔ)言的約定,Integer 類型實(shí)現(xiàn)了 Math 接口。然后我們可以這樣將 Integer 類型的實(shí)例 a 直接賦值給 Math 接口類型的變量 m:
var a Integer = 1
var m Math = a
fmt.Println(m.Add(1))
對(duì)于值方法而言,進(jìn)行接口賦值時(shí)傳遞 a 實(shí)例的指針引用也是可以的:
var a Integer = 1
var m Math = &a
fmt.Println(m.Add(1))
因?yàn)閷?duì)于非指針方法,Go 底層會(huì)自動(dòng)生成一個(gè)與之對(duì)應(yīng)的指針成員方法:
func (a *Integer) Add(i Integer) Integer {
return (*a).Add(i)
}
func (a *Integer) Multiply(i Integer) Integer {
return (*a).Multiply(i)
}
包含指針方法
不過如果 Integer 類型中包含了歸屬于指針的實(shí)現(xiàn)方法:
type Integer int
func (a *Integer) Add(b Integer) {
*a = (*a) + b
}
func (a Integer) Multiply(b Integer) Integer {
return a * b
}
type Math interface {
Add(i Integer)
Multiply(i Integer) Integer
}
那么在做接口賦值時(shí),就只能傳遞指針類型的變量了:
var a Integer = 1
var m Math = &a
m.Add(2)
fmt.Printf("1 + 2 = %d\n", a)
因?yàn)?Integer 類型不包含指針方法(參考前面介紹的值方法與指針方法區(qū)別),所以此時(shí)只有 *Integer 類型實(shí)現(xiàn)了 Math 接口,如果我們直接將 a 的值類型賦值給 m,編譯時(shí)會(huì)報(bào)錯(cuò):
cannot use a (type Integer) as type Math in assignment:
Integer does not implement Math (Add method has pointer receiver)
綜上所述,如果 Integer 類中實(shí)現(xiàn)接口的成員方法都是值方法,則進(jìn)行接口賦值時(shí),傳遞類實(shí)例的值類型或者指針類型均可,否則只能傳遞指針類型實(shí)例,從代碼性能角度來說,值拷貝需要消耗更多的內(nèi)存空間,統(tǒng)一使用指針類型代碼性能會(huì)更好。
三、將接口賦值給接口
接下來,我們來看如何將一個(gè)接口賦值給另一個(gè)接口:在 Go 語(yǔ)言中,只要兩個(gè)接口擁有相同的方法列表(與順序無關(guān)),那么它們就是等同的,可以相互賦值。不過,這里有一個(gè)前提,那就是接口變量持有的是基于對(duì)應(yīng)實(shí)現(xiàn)類的實(shí)例值,所以接口與接口間的賦值是基于類實(shí)例與接口間的賦值的。
完全對(duì)等
下面我們來編寫對(duì)應(yīng)的示例代碼,這是第一個(gè)接口 Number1:
type Number1 interface {
Equal(i int) bool
LessThan(i int) bool
MoreThan(i int) bool
}
這是第二個(gè)接口 Number2:
type Number2 interface {
Equal(i int) bool
MoreThan(i int) bool
LessThan(i int) bool
}
這里我們定義了兩個(gè)接口,一個(gè)叫 Number1,一個(gè)叫 Number2,兩者都定義三個(gè)相同的方法,只是順序不同而已。在 Go 語(yǔ)言中,這兩個(gè)接口實(shí)際上并無區(qū)別,因?yàn)椋?/p>
任何實(shí)現(xiàn)了
Number1接口的類,也實(shí)現(xiàn)了Number2;任何實(shí)現(xiàn)了
Number1接口的類實(shí)例都可以賦值給Number2,反之亦然;在任何地方使用
Number1接口與使用Number2并無差異。
接下來我們定義一個(gè)實(shí)現(xiàn)了這兩個(gè)接口的類 Number:
type Number int
func (n Number) Equal(i int) bool {
return int(n) == i
}
func (n Number) LessThan(i int) bool {
return int(n) < i
}
func (n Number) MoreThan(i int) bool {
return int(n) > i
}
那么下面這些賦值代碼都是合法的,會(huì)編譯通過:
var num1 Number = 1
var num2 Number1 = num1
var num3 Number2 = num2
方法子集
此外,接口賦值并不要求兩個(gè)接口完全等價(jià)(方法完全相同)。如果接口 A 的方法列表是接口 B 的方法列表的子集,那么接口 B 也可以賦值給接口 A。例如,假設(shè) Number2 接口定義如下:
type Number2 interface {
Equal(i int) bool
MoreThan(i int) bool
LessThan(i int) bool
Add(i int)
}
要讓 Number 類繼續(xù)保持實(shí)現(xiàn)這兩個(gè)接口,需要在 Number 類定義中新增一個(gè) Add 方法實(shí)現(xiàn)(這里定義了一個(gè)指針方法):
func (n *Number) Add(i int) {
*n = *n + Number(i)
}
接下來,將上面的接口賦值語(yǔ)句改寫如下即可:
var num1 Number = 1
var num2 Number2 = &num1
var num3 Number1 = num2
這樣一來,就實(shí)現(xiàn)了接口賦值,但是反過來不行:
var num1 Number = 1
var num2 Number1 = &num1
var num3 Number2 = num2 // 這一段編譯出錯(cuò)
因?yàn)?Number1 接口中沒有聲明 Add 方法,或者換句話說,實(shí)現(xiàn)了 Number2 接口的類肯定實(shí)現(xiàn)了 Number1,但是實(shí)現(xiàn)了 Number1 接口的類不一定實(shí)現(xiàn)了 Number2。這句話是不是似曾相識(shí)?沒錯(cuò),這一點(diǎn)和 Java、PHP 中子類實(shí)例可以直接賦值給父類變量,而父類實(shí)例不能直接賦值給子類變量有異曲同工之妙,我們?cè)趯W(xué)習(xí)新知識(shí)時(shí)要善于通過這種類比來降低學(xué)習(xí)成本,提高學(xué)習(xí)效率。
(本文完)
學(xué)習(xí)過程中有任何問題,可以通過下面的評(píng)論功能或加入「Go 語(yǔ)言研習(xí)社」與學(xué)院君討論:
本系列教程首發(fā)在 geekr.dev,你可以點(diǎn)擊頁(yè)面左下角閱讀原文鏈接查看最新更新的教程。
