接口类型的本质就是如果一个数据类型实现了自身的方法集,那么该接口类型变量就能够引用该数据类型的值。
Go不是一种典型的OO语言,它在语法上不支持类和继承的概念。
没有继承是否就无法拥有多态行为了呢?答案是否定的,Go语言引入了一种新类型—Interface,它在效果上实现了类似于C++的“多态”概念,虽然与C++的多态在语法上并非完全对等,但至少在最终实现的效果上,它有多态的影子。
虽然Go语言没有类的概念,但它支持的数据类型可以定义对应的method(s)。本质上说,所谓的method(s)其实就是函数,只不过与普通函数相比,这类函数是作用在某个数据类型上的,所以在函数签名中,会有个receiver(接收器)来表明当前定义的函数会作用在该receiver上。
Go语言支持的除Interface类型外的任何其它数据类型都可以定义其method(而并非只有struct才支持method),只不过实际项目中,method(s)多定义在struct上而已。
从这一点来看,我们可以把Go中的struct看作是不支持继承行为的轻量级的“类”。
An interface type specifies a method set called its interface. A variable of interface type can store a value of any type with a method set that is any superset of the interface. Such a type is said to implement the interface. The value of an uninitialized variable of interface type is nil.
说实话,这段说明对新手来说比较晦涩,这正是本篇笔记试图解释清楚的地方。
从语法上看,Interface定义了一个或一组method(s),这些method(s)只有函数签名,没有具体的实现代码(有没有联想起C++中的虚函数?)。若某个数据类型实现了Interface中定义的那些被称为"methods"的函数,则称这些数据类型实现(implement)了interface。举个例子来说明。
package main
import (
"fmt"
"math"
)
type Abser interface {
Abs() float64
}
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
func main() {
var a Abser
fmt.Println(float64(math.Sqrt2))
f := MyFloat(-math.Sqrt2)
a = f // a MyFloat implements Abser
fmt.Println(a.Abs())
}
//输出
1.4142135623730951
1.4142135623730951
上面的代码中,第8-10行是通过type语法声明了一个名为Abser的interface类型(Go中约定的interface类型名通常取其内部声明的method名的er形式)。而第12-19行通过type语法声明了MyFloat类型且为该类型定义了名为Abs()的method。
根据上面的解释,Abs()是interface类型Abser定义的方法,而MyFloat实现了该方法,所以,MyFloat实现了Abser接口。
Interface类型的更通用定义可归纳如下:
type Namer interface {
Method1(param_list) return_type
Method2(param_list) return_type
...
}
上面的示例用type语法声明了一个名为Namer的interface类型(但Namer不是个具体的变量,此时内存中还没有它对应的对象)。interface类型是可以定义变量的,也即interface type can have values,例如:
var ai Namer
此时,定义了一个变量名为ai的Namer类型变量,在Go的底层实现中,ai本质上是个指针,其内存布局如下(内存布局图引用自<The Way to Go - A Thorough Introduction to the Go Programming Language>一书第11.1节):
它的method table ptr是不是与C++中类的虚函数表非常类似?而这正是interface类型的变量具有多态特性的关键:
ai共占2个机器字,1个为receiver字段,1个为method table ptr字段。ai可以被赋值为任何变量,只要这个变量实现了interface定义的method(s) set,赋值后,ai的receiver字段用来hold那个变量或变量副本的地址(若变量类型小于等于1个机器字大小,则receiver直接存储那个变量;若变量类型大于1个机器字,则Go底层会在堆上申请空间存储那个变量的副本,然后receiver存储那个副本的地址,即此时receiver是个指向变量副本的指针)。而由变量实现的接口method(s)组成的interface table的指针会填充到ai的method table ptr字段。当ai被赋值为另一个变量后,其receiver和method table ptr会更新为新变量的相关值。
关于interface类型内部实现细节,可以参考GoLang官网Blog推荐过的一篇文章“Go Data Structures: Interfaces”,写的很清楚,强烈推荐。
所以,如果某个函数的入参是个interface类型时,任何实现了该interface的变量均可以作为合法参数传入且函数的具体行为会自动作用在传入的这个实现了interface的变量上,这不正是类似于C++中多态的行为吗?
Interface“多态”特性实例
Go语言自带的标准Packages提供的接口很多都借助了Interface以具备“可以处理任何未知数据类型”的能力。例如被广泛使用的fmt包,其功能描述如下:
Package fmt implements formatted I/O with functions analogous to C's printf and scanf. The format 'verbs' are derived from C's but are simpler.
它除了可以格式化打印Go的built-in类型外,还可以正确打印各种自定义类型,只要这些自定义数据类型实现了fmt的Print API入参所需的interface接口。
以fmt包的Printf()函数为例,其函数签名格式如下:
func Printf(format string, a ...interface{}) (n int, err error)
它的入参除了用以描述如何格式化的'format'参数外,还需要interface类型的可变长参数。该函数在实现底层的打印行为时,要求传入的可变长参数实现了fmt包中定义的Stringer接口,这个接口类型定义及描述如下:
type Stringer interface {
String() string
}
所以,自定义类型想要调用fmt.Printf()做格式化打印,那只需实现Stringer接口就行。
例如,下面是一段简单的打印代码:
package main
import "fmt"
type IPAddr [4]byte
func main() {
addrs := map[string]IPAddr{
"loopback": {127, 0, 0, 1},
"googleDNS": {8, 8, 8, 8},
}
for n, a := range addrs {
fmt.Printf("%v: %v\n", n, a)
}
}