一肢础、概念
1.接口定義
Go 語言的接口類型非常特別,它的作用和 Java 語言的接口一樣歌殃,但是在形式上有很大的差別乔妈。Java 語言需要在類的定義上顯式實現(xiàn)了某些接口蝙云,才可以說這個類具備了接口定義的能力氓皱。但是 Go 語言的接口是隱式的,只要結(jié)構(gòu)體上定義的方法在形式上(名稱勃刨、參數(shù)和返回值)和接口定義的一樣波材,那么這個結(jié)構(gòu)體就自動實現(xiàn)了這個接口,我們就可以使用這個接口變量來指向這個結(jié)構(gòu)體對象身隐。下面我們看個例子
package main
import "fmt"
// 可以聞
type Smellable interface {
smell()
}
// 可以吃
type Eatable interface {
eat()
}
// 蘋果既可能聞又能吃
type Apple struct {}
func (a Apple) smell() {
fmt.Println("apple can smell")
}
func (a Apple) eat() {
fmt.Println("apple can eat")
}
// 花只可以聞
type Flower struct {}
func (f Flower) smell() {
fmt.Println("flower can smell")
}
func main() {
var s1 Smellable
var s2 Eatable
var apple = Apple{}
var flower = Flower{}
s1 = apple
s1.smell()
s1 = flower
s1.smell()
s2 = apple
s2.eat()
}
--------------------
apple can smell
flower can smell
apple can eat
上面的代碼定義了兩種接口廷区,Apple 結(jié)構(gòu)體同時實現(xiàn)了這兩個接口,而 Flower 結(jié)構(gòu)體只實現(xiàn)了 Smellable 接口贾铝。我們并沒有使用類似于 Java 語言的 implements 關(guān)鍵字隙轻,結(jié)構(gòu)體和接口就自動產(chǎn)生了關(guān)聯(lián)埠帕。
2.空接口
如果一個接口里面沒有定義任何方法,那么它就是空接口玖绿,任意結(jié)構(gòu)體都隱式地實現(xiàn)了空接口敛瓷。
Go 語言為了避免用戶重復定義很多空接口,它自己內(nèi)置了一個斑匪,這個空接口的名字特別奇怪呐籽,叫 interface{} ,初學者會非常不習慣蚀瘸。之所以這個類型名帶上了大括號狡蝶,那是在告訴用戶括號里什么也沒有。我始終認為這種名字很古怪贮勃,它讓代碼看起來有點丑陋贪惹。
空接口里面沒有方法,所以它也不具有任何能力衙猪,其作用相當于 Java 的 Object 類型馍乙,可以容納任意對象,它是一個萬能容器垫释。比如一個字典的 key 是字符串丝格,但是希望 value 可以容納任意類型的對象,類似于 Java 語言的 Map 類型棵譬,這時候就可以使用空接口類型 interface{}显蝌。
package main
import "fmt"
func main() {
// 連續(xù)兩個大括號,是不是看起來很別扭
var user = map[string]interface{}{
"age": 30,
"address": "Beijing Tongzhou",
"married": true,
}
fmt.Println(user)
// 類型轉(zhuǎn)換語法來了
var age = user["age"].(int)
var address = user["address"].(string)
var married = user["married"].(bool)
fmt.Println(age, address, married)
}
-------------
map[age:30 address:Beijing Tongzhou married:true]
30 Beijing Tongzhou true
代碼中 user 字典變量的類型是 map[string]interface{}订咸,從這個字典中直接讀取得到的 value 類型是 interface{}曼尊,需要通過類型轉(zhuǎn)換才能得到期望的變量。
3.用接口來模擬多態(tài)
package main
import "fmt"
type Fruitable interface {
eat()
}
type Fruit struct {
Name string // 屬性變量
Fruitable // 匿名內(nèi)嵌接口變量
}
func (f Fruit) want() {
fmt.Printf("I like ")
f.eat() // 外結(jié)構(gòu)體會自動繼承匿名內(nèi)嵌變量的方法
}
type Apple struct {}
func (a Apple) eat() {
fmt.Println("eating apple")
}
type Banana struct {}
func (b Banana) eat() {
fmt.Println("eating banana")
}
func main() {
var f1 = Fruit{"Apple", Apple{}}
var f2 = Fruit{"Banana", Banana{}}
f1.want()
f2.want()
}
---------
I like eating apple
I like eating banana
使用這種方式模擬多態(tài)本質(zhì)上是通過組合屬性變量(Name)和接口變量(Fruitable)來做到的脏嚷,屬性變量是對象的數(shù)據(jù)骆撇,而接口變量是對象的功能,將它們組合到一塊就形成了一個完整的多態(tài)性的結(jié)構(gòu)體父叙。
《GoInAction》第118頁也提供了一個例子:
// Sample program to show how polymorphic behavior with interfaces.
package main
import (
"fmt"
)
// notifier is an interface that defines notification
// type behavior.
type notifier interface {
notify()
}
// user defines a user in the program.
type user struct {
name string
email string
}
// notify implements the notifier interface with a pointer receiver.
func (u *user) notify() {
fmt.Printf("Sending user email to %s<%s>\n",
u.name,
u.email)
}
// admin defines a admin in the program.
type admin struct {
name string
email string
}
// notify implements the notifier interface with a pointer receiver.
func (a *admin) notify() {
fmt.Printf("Sending admin email to %s<%s>\n",
a.name,
a.email)
}
// main is the entry point for the application.
func main() {
// Create a user value and pass it to sendNotification.
bill := user{"Bill", "bill@email.com"}
sendNotification(&bill)
// Create an admin value and pass it to sendNotification.
lisa := admin{"Lisa", "lisa@email.com"}
sendNotification(&lisa)
}
// sendNotification accepts values that implement the notifier
// interface and sends notifications.
func sendNotification(n notifier) {
n.notify()
}
在第 53 行中神郊,我們再次聲明了多態(tài)函數(shù) sendNotification,這個函數(shù)接受一個實現(xiàn)了notifier 接口的值作為參數(shù)趾唱。既然任意一個實體類型都能實現(xiàn)該接口涌乳,那么這個函數(shù)可以針對任意實體類型的值來執(zhí)行 notifier 方法。因此甜癞,這個函數(shù)就能提供多態(tài)的行為夕晓。
4.接口的組合繼承
接口的定義也支持組合繼承,比如我們可以將兩個接口定義合并為一個接口如下
type Smellable interface {
smell()
}
type Eatable interface {
eat()
}
type Fruitable interface {
Smellable
Eatable
}
這時 Fruitable 接口就自動包含了 smell() 和 eat() 兩個方法悠咱,它和下面的定義是等價的蒸辆。
type Fruitable interface {
smell()
eat()
}
5.接口變量的賦值
變量賦值本質(zhì)上是一次內(nèi)存淺拷貝,切片的賦值是拷貝了切片頭柒室,字符串的賦值是拷貝了字符串的頭部逗宜,而數(shù)組的賦值呢是直接拷貝整個數(shù)組雄右。接口變量的賦值會不會不一樣呢擂仍?接下來我們做一個實驗
package main
import "fmt"
type Rect struct {
Width int
Height int
}
func main() {
var a interface {}
var r = Rect{50, 50}
a = r
var rx = a.(Rect)
r.Width = 100
r.Height = 100
fmt.Println(rx)
}
------
{50 50}
6.嵌入類型
《GoInAction》也提供了例子
// Sample program to show how to embed a type into another type and
// the relationship between the inner and outer type.
package main
import (
"fmt"
)
// user defines a user in the program.
type user struct {
name string
email string
}
// notify implements a method that can be called via
// a value of type user.
func (u *user) notify() {
fmt.Printf("Sending user email to %s<%s>\n",
u.name,
u.email)
}
// admin represents an admin user with privileges.
type admin struct {
user // Embedded Type
level string
}
// main is the entry point for the application.
func main() {
// Create an admin user.
ad := admin{
user: user{
name: "john smith",
email: "john@yahoo.com",
},
level: "super",
}
// We can access the inner type's method directly.
ad.user.notify()
// The inner type's method is promoted.
ad.notify()
}
這展示了內(nèi)部類型是如何存在于外部類型內(nèi),并且總是可訪問的熬甚。不過乡括,借助內(nèi)部類型提升诲泌,notify 方法也可以直接通過 ad 變量來訪問
再改造一下:
// notifier is an interface that defined notification
// type behavior.
type notifier interface {
notify()
}
// sendNotification accepts values that implement the notifier
// interface and sends notifications.
func sendNotification(n notifier) {
n.notify()
}
// main is the entry point for the application.
func main() {
// Create an admin user.
ad := admin{
user: user{
name: "john smith",
email: "john@yahoo.com",
},
level: "super",
}
// Send the admin user a notification.
// The embedded inner type's implementation of the
// interface is "promoted" to the outer type.
sendNotification(&ad)
}
在代碼清單 5-58 的第 37 行哀蘑,我們創(chuàng)建了一個名為 ad 的變量绘迁,其類型是外部類型 admin缀台。這個類型內(nèi)部嵌入了 user 類型膛腐。之后第 48 行依疼,我們將這個外部類型變量的地址傳給 sendNotification 函數(shù)。編譯器認為這個指針實現(xiàn)了 notifier 接口棍丐,并接受了這個值的傳遞歌逢。不過如果看一下整個示例程序秘案,就會發(fā)現(xiàn) admin 類型并沒有實現(xiàn)這個接口阱高。由于內(nèi)部類型的提升赤惊,內(nèi)部類型實現(xiàn)的接口會自動提升到外部類型未舟。這意味著由于內(nèi)部類型的實現(xiàn)员串,外部類型也同樣實現(xiàn)了這個接口昵济。
如果外部類型并不需要使用內(nèi)部類型的實現(xiàn)访忿,而想使用自己的一套實現(xiàn)海铆,該怎么辦卧斟?
// notify implements a method that can be called via
// a value of type Admin.
func (a *admin) notify() {
fmt.Printf("Sending admin email to %s<%s>\n",
a.name,
a.email)
}
// main is the entry point for the application.
func main() {
// Create an admin user.
ad := admin{
user: user{
name: "john smith",
email: "john@yahoo.com",
},
level: "super",
}
// Send the admin user a notification.
// The embedded inner type's implementation of the
// interface is NOT "promoted" to the outer type.
sendNotification(&ad)
// We can access the inner type's method directly.
ad.user.notify()
// The inner type's method is NOT promoted.
ad.notify()
}
---------------------------------------------
Sending admin email to john smith<john@yahoo.com>
Sending user email to john smith<john@yahoo.com>
Sending admin email to john smith<john@yahoo.com>
這表明,如果外部類型實現(xiàn)了 notify 方法板乙,內(nèi)部類型的實現(xiàn)就不會被提升募逞。不過內(nèi)部類型的值一直存在放接,因此還可以通過直接訪問內(nèi)部類型的值纠脾,來調(diào)用沒有被提升的內(nèi)部類型實現(xiàn)的方法捧韵。
關(guān)于嵌套結(jié)構(gòu)體的應用再来,可以在Golang sort自定義排序中看到
二芒篷、值接收和引用接收
在Golang 學習筆記六 函數(shù)function和方法method的區(qū)別講方法時,有個例子贸呢,當通過值或指針調(diào)用方法時,go編譯器會自動幫我們處理方法接收者的不一致结执。
type user struct{
name string
email string
}
func (u user) printName(){
fmt.Printf("name: %s\n", u.name)
}
func (u *user) printEmail(){
fmt.Printf("email: %s\n", u.email)
}
func main() {
bill := user{"bill","bill@gmail.com"}
lisa := &user{"lisa","lisa@gmail.com"};
bill.printName()
lisa.printName()
bill.printEmail()
lisa.printEmail()
}
正常打永小:
name: bill
name: lisa
email: bill@gmail.com
email: lisa@gmail.com
但是悟泵,如果通過接口類型的值調(diào)用方法,規(guī)則有很大不同:
在上面代碼中加上兩個接口
type printNamer interface{
printName()
}
type printEmailer interface{
printEmail()
}
func sendPrintName(n printNamer) {
n.printName()
}
func sendPrintEmail(n printEmailer){
n.printEmail()
}
func main() {
...
sendPrintName(bill)
sendPrintName(lisa)
sendPrintEmail(bill)
sendPrintEmail(lisa)
這里sendPrintEmail(bill)
編譯不通過衡招,提示:cannot use bill (type user) as type printEmailer in argument to sendPrintEmail:user does not implement printEmailer (printEmail method has pointer receiver)
觀察一下區(qū)別州刽,bill是一個值奶栖,printEmail接收者是個指針冻晤,失敗了。但是另外一個不一致的卻能通過绣硝,那就是lisa是個指針,但是printName的接收者要求是值挠铲,為啥就能通過呢痰洒。
在《Go in Action》第118頁描述了方法集的規(guī)則:
使用指針作為接收者聲明的方法脯宿,只能在接口類型的值是一個指針的時候被調(diào)用嗡靡。使用值作為接收者聲明的方法,在接口類型的值為值或者指針時,都可以被調(diào)用。
為什么會有這種限制菜谣?事實上,編譯器并不是總能自動獲得一個值的地址,如代碼清單 5-46 所示暮蹂。
代碼清單 5-46 listing46.go
01 // 這個示例程序展示不是總能
02 // 獲取值的地址
03 package main
04
05 import "fmt"
06
07 // duration 是一個基于 int 類型的類型
08 type duration int
09
10 // 使用更可讀的方式格式化 duration 值
11 func (d *duration) pretty() string {
12 return fmt.Sprintf("Duration: %d", *d)
13 }
14
15 // main 是應用程序的入口
16 func main() {
17 duration(42).pretty()
18
19 // ./listing46.go:17: 不能通過指針調(diào)用 duration(42)的方法
20 // ./listing46.go:17: 不能獲取 duration(42)的地址
21 }
這里編譯通過滩届,運行也會報錯券犁。我們改成變量調(diào)用就可以了:
dd := duration(42)
dd.pretty()