一、概念
1.接口定义
Go 语言的接口类型非常特别,它的作用和 Java 语言的接口一样,但是在形式上有很大的差别。Java 语言需要在类的定义上显式实现了某些接口,才可以说这个类具备了接口定义的能力。但是 Go 语言的接口是隐式的,只要结构体上定义的方法在形式上(名称、参数和返回值)和接口定义的一样,那么这个结构体就自动实现了这个接口,我们就可以使用这个接口变量来指向这个结构体对象。下面我们看个例子
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 结构体同时实现了这两个接口,而 Flower 结构体只实现了 Smellable 接口。我们并没有使用类似于 Java 语言的 implements 关键字,结构体和接口就自动产生了关联。
2.空接口
如果一个接口里面没有定义任何方法,那么它就是空接口,任意结构体都隐式地实现了空接口。
Go 语言为了避免用户重复定义很多空接口,它自己内置了一个,这个空接口的名字特别奇怪,叫 interface{} ,初学者会非常不习惯。之所以这个类型名带上了大括号,那是在告诉用户括号里什么也没有。我始终认为这种名字很古怪,它让代码看起来有点丑陋。
空接口里面没有方法,所以它也不具有任何能力,其作用相当于 Java 的 Object 类型,可以容纳任意对象,它是一个万能容器。比如一个字典的 key 是字符串,但是希望 value 可以容纳任意类型的对象,类似于 Java 语言的 Map 类型,这时候就可以使用空接口类型 interface{}。
package main
import "fmt"
func main() {
// 连续两个大括号,是不是看起来很别扭
var user = map[string]interface{}{
"age": 30,
"address": "Beijing Tongzhou",
"married": true,
}
fmt.Println(user)
// 类型转换语法来了
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{},需要通过类型转换才能得到期望的变量。
3.用接口来模拟多态
package main
import "fmt"
type Fruitable interface {
eat()
}
type Fruit struct {
Name string // 属性变量
Fruitable // 匿名内嵌接口变量
}
func (f Fruit) want() {
fmt.Printf("I like ")
f.eat() // 外结构体会自动继承匿名内嵌变量的方法
}
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
使用这种方式模拟多态本质上是通过组合属性变量(Name)和接口变量(Fruitable)来做到的,属性变量是对象的数据,而接口变量是对象的功能,将它们组合到一块就形成了一个完整的多态性的结构体。
《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 行中,我们再次声明了多态函数 sendNotification,这个函数接受一个实现了notifier 接口的值作为参数。既然任意一个实体类型都能实现该接口,那么这个函数可以针对任意实体类型的值来执行 notifier 方法。因此,这个函数就能提供多态的行为。
4.接口的组合继承
接口的定义也支持组合继承,比如我们可以将两个接口定义合并为一个接口如下
type Smellable interface {
smell()
}
type Eatable interface {
eat()
}
type Fruitable interface {
Smellable
Eatable
}
这时 Fruitable 接口就自动包含了 smell() 和 eat() 两个方法,它和下面的定义是等价的。
type Fruitable interface {
smell()
eat()
}
5.接口变量的赋值
变量赋值本质上是一次内存浅拷贝,切片的赋值是拷贝了切片头,字符串的赋值是拷贝了字符串的头部,而数组的赋值呢是直接拷贝整个数组。接口变量的赋值会不会不一样呢?接下来我们做一个实验
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()
}
这展示了内部类型是如何存在于外部类型内,并且总是可访问的。不过,借助内部类型提升,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 行,我们创建了一个名为 ad 的变量,其类型是外部类型 admin。这个类型内部嵌入了 user 类型。之后第 48 行,我们将这个外部类型变量的地址传给 sendNotification 函数。编译器认为这个指针实现了 notifier 接口,并接受了这个值的传递。不过如果看一下整个示例程序,就会发现 admin 类型并没有实现这个接口。由于内部类型的提升,内部类型实现的接口会自动提升到外部类型。这意味着由于内部类型的实现,外部类型也同样实现了这个接口。
如果外部类型并不需要使用内部类型的实现,而想使用自己的一套实现,该怎么办?
// 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>
这表明,如果外部类型实现了 notify 方法,内部类型的实现就不会被提升。不过内部类型的值一直存在,因此还可以通过直接访问内部类型的值,来调用没有被提升的内部类型实现的方法。
关于嵌套结构体的应用,可以在Golang sort自定义排序中看到
二、值接收和引用接收
在Golang 学习笔记六 函数function和方法method的区别讲方法时,有个例子,当通过值或指针调用方法时,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
但是,如果通过接口类型的值调用方法,规则有很大不同:
在上面代码中加上两个接口
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)
观察一下区别,bill是一个值,printEmail接收者是个指针,失败了。但是另外一个不一致的却能通过,那就是lisa是个指针,但是printName的接收者要求是值,为啥就能通过呢。
在《Go in Action》第118页描述了方法集的规则:
使用指针作为接收者声明的方法,只能在接口类型的值是一个指针的时候被调用。使用值作为接收者声明的方法,在接口类型的值为值或者指针时,都可以被调用。
为什么会有这种限制?事实上,编译器并不是总能自动获得一个值的地址,如代码清单 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: 不能通过指针调用 duration(42)的方法
20 // ./listing46.go:17: 不能获取 duration(42)的地址
21 }
这里编译通过,运行也会报错。我们改成变量调用就可以了:
dd := duration(42)
dd.pretty()