接口
通過關鍵字type和interface,我們可以聲明出接口類型。接口類型的類型字面量與結構體類型的看起來有些相似,它們都用花括號包裹一些核心信息。只不過,結構體類型包裹的是它的字段聲明,而接口類型包裹的是它的方法定義。
實現接口
接口類型聲明中的這些方法所代表的就是該接口的方法集合。一個接口的方法集合就是它的全部特徵。對於任何數據類型,只要它的方法集合中完全包含了一個接口的全部特徵(即全部的方法),那麼它就一定是這個接口的實現類型:
type Pet interface {
SetName(name string)
Name() string
Category() string
}
這裏聲明瞭一個接口類型Pet,它包含3個方法定義。這3個方法共同組成了接口類型Pet的方法集合。只要一個數據類型的方法集合中有3個方法,那麼它就就一定是Pet接口類型的實現。這是一種無浸入式的接口實現方式。這種方式還有一個專有名詞,叫“Duck typing”,中文常譯作“鴨子類型”。
下面的是上一篇結尾的那個例子,不過Cat換成了Dog:
package main
import "fmt"
type Pet interface {
SetName(name string)
Name() string
Category() string
}
type Dog struct {
name string // 名字。
}
func (dog *Dog) SetName(name string) {
dog.name = name
}
func (dog Dog) Name() string {
return dog.name
}
func (dog Dog) Category() string {
return "dog"
}
func main() {
// 示例1。
dog := Dog{"little pig"}
_, ok := interface{}(dog).(Pet)
fmt.Printf("Dog implements interface Pet: %v\n", ok)
_, ok = interface{}(&dog).(Pet)
fmt.Printf("*Dog implements interface Pet: %v\n", ok)
fmt.Println()
// 示例2。
var pet Pet = &dog
fmt.Printf("This pet is a %s, the name is %q.\n",
pet.Category(), pet.Name())
}
聲明的Dog有3個方法,其中2個是值方法Name和Category,還有一個指針方法SetName。Dog類型本身的方法集合中只有2個方法,就是所有的值方法。而它的指針類型*Dog方法集合包含了3個方法,就是它擁有Dog類型附帶的所有值方法和指針方法。而這3個方法正好是Pet接口中,所以*Dog類型就成爲了Pet接口的實現類型。
在上面,示例2的那一小段代碼,把main主函數開頭聲明的Dog類型的變量dog,把它的指針賦值給了類型爲Pet的變量pet。這裏的變量pet的值,可以被叫做它的實際值(也稱動態值)。該值的類型可以被叫做這個表量的實際類型(也稱動態類型)。
動態類型的叫法是相對於靜態類型而言的。對於變量pet,它的靜態類型就是Pet,並且不會改變。但是他的動態會隨着賦給他的動態值而變化。這裏的動態類型是*Dog類型,而動態值就是&dog的值(就是dog的地址)。
給接口賦值
下面的示例定義了簡單的結構體和接口類型:
package main
import "fmt"
type Pet interface {
Name() string
}
type Dog struct {
name string
}
// 如果這是一個值方法?
func (d *Dog) SetName (name string) {
d.name = name
}
func (d Dog) Name() string {
return d.name
}
func main() {
dog := Dog{"Snoopy"}
fmt.Println(dog.Name())
var pet Pet = dog // 這個如果是一個取址表達式?
dog.SetName("Goofy ")
fmt.Println(dog.Name())
fmt.Println(pet.Name())
}
這裏的SetName方法必須是指針方法。因爲如果是值方法,接受者就是dog的副本,該方法改變的也只是副本的name的值,不會影響的dog變量本身。如果是指針方法,那麼當SetName方法執行後,dog的name字典就就被改變了。
然後接着看接下來的一層。dog賦值給了pet,然後dog的name字段確實變了,但是這裏pet裏還是原來的值。這裏的原因和上面的一樣的。如果使用一個變量給另外一個變量賦值,那麼真正賦值給後者的,其實是一個副本。這裏如果是把&dog賦值給pet,那麼pet的值也就會跟着dog的進行變化了。
上面可以這麼理解,但是嚴格來講,即使像前面那樣把dog的值賦給了pet,pet的值與dog的值也是不同的。在給一個接口變量賦值的時候,該變量的動態類型會與它的動態值一起被存儲在一個專用的數據結構中。無論從存儲的內容還是存儲的結構來看,pet的值與dog的值都是不同的。不過可以認爲,此時的pet的值中包含了dog的值的副本。
接口變量的nil值
這裏要討論的是接口變量在聲明情況下才真正爲nil:
package main
import "fmt"
type Pet interface {
Name() string
}
type Dog struct {
name string
}
func (d Dog) Name() string {
return d.name
}
func main() {
var dog *Dog
fmt.Println(dog)
fmt.Println(dog == nil) // true
var pet Pet = dog
// var pet Pet = nil // 註釋掉上面的,試試這句
fmt.Println(pet)
fmt.Println(pet == nil) // false
// fmt.Printf("%T", pet) // 打印動態類型
這裏先聲明瞭一個*Dog類型的變量dog,並沒有對他進行初始化。所以它的值就是nil。
然後把dog賦值給了接口類型pet,此時判斷pet是否爲nil時返回的false。
這裏確實把值爲nil的dog變量賦值給了pet。pet的動態值也確實是nil,但是pet的值不是nil。動態值只是pet值的一部分,還有動態類型。pet的動態類型是*Dog,可以通過fmt.Printf函數和佔位符%T打印變量的類型。另外reflect包的TypeOf函數也可以起到類似的作用。
如果把nil直接賦值給pet的話,那麼pet就是真正的nil了。在Go語言裏,字面量nil表示的值叫做無類型的nil。這個是真正的nil,因爲他的類型也是nil。而例子中,雖然dog的值是nil,但是當把這個變量賦值pet的時候,其實是賦值給pet的值是一個*Dog類型的nil值。對於接口變量,需要動態值和動態類型都是nil,那纔是真正的nil。要想讓一個接口變量的值真正爲nil,可以把一個字面的nil賦值給它,或者只聲明接口而不做初始化也可以。
接口的組合
接口類型的間的嵌入也被稱爲接口的組合。組合的接口之間不能有同名的方法存在,如果有同名的方法就會產生衝突而無法通過編譯。與結構體類型間的嵌入很相似,只要把一個接口類型的名稱直接寫的另一個接口類型的成員列表中就可以進行組合:
type Animal interface {
ScientificName() string
Category() string
}
type Pet interface {
Animal
Name() string
}
組合後,Animal接口包含的所有方法也就是成爲了Pet接口的方法。
Go語言團隊鼓勵我們聲明體量較小的接口,並建議我們通過這種接口間的組合來擴展程序、增加程序的靈活性。相比於包含很多方法的大接口而言,小接口可以更加專注地表達某一種能力或某一類特徵,同時也更容易被組合在一起。善用接口組合和小接口可以讓你的程序框架更加穩定和靈活。