Go不是一种典型的OO语言,它在语法上不支持类和继承的概念。
没有继承是否就无法拥有多态行为了呢?答案是否定的,Go语言引入了一种新类型—Interface,它在效果上实现了类似于C++的“多态”概念,虽然与C++的多态在语法上并非完全对等,但至少在最终实现的效果上,它有多态的影子。
那么,Go的Interface类型到底是什么呢?怎么使用呢?这正是本篇笔记试图说明的问题。
在说明Interface类型前,不得不先用Go的method(s)概念来热身,因为Go语言的interface与method(s)这两个语法有非常紧密的联系。
虽然Go语言没有类的概念,但它支持的数据类型可以定义对应的method(s)。本质上说,所谓的method(s)其实就是函数,只不过与普通函数相比,这类函数是作用在某个数据类型上的,所以在函数签名中,会有个receiver来表明当前定义的函数会作用在该receiver上。
关于methods的精确语法规范,可以参考language specification或Effective Go中的说明,这里略过。
注意:Go语言支持的除Interface类型外的任何其它数据类型都可以定义其method(而并非只有struct才支持method),只不过实际项目中,method(s)多定义在struct上而已。
在struct类型上定义method(s)的语法特性与C++中的struct支持的语法非常类似(c++中的struct定义了数据,此外也支持定义数据的操作方法),从这一点来看,我们可以把Go中的struct看作是不支持继承行为的轻量级的“类”。
2. What is Interface type in Go ?
GoLang官网language specification文档对interface type的概念说明如下:
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 f := MyFloat(-math.Sqrt2) a = f // a MyFloat implements Abser fmt.Println(a.Abs()) }上面的代码中,第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语言中的威力。
引用<The Way to Go>一书第11.5节对interface类型的总结如下,值得每个Go学习者理解:
An interface is a kind of contract which the implementing type(s) must fulfill. Interfaces describe the behaviorof types, what they can do. They completely separate the definition of what an object can do from how it does it, allowing distinct implementations to be represented at different times by the same interface variable, which is what polymorphism essentially is.
Writing functions so that they accept an interface variable as a parameter makes them more general.
3. 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 }Stringer is implemented by any value that has a String method, which defines the “native” format for that value. The String method is used to print values passed as an operand to any format that accepts a string or to an unformatted printer such as Print.
所以,自定义类型想要调用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) } }其输出如下:
loopback: [127 0 0 1] googleDNS: [8 8 8 8]现在要求按规定的格式打印:IPAddr{1, 2, 3, 4}应该输出为"1.2.3.4"的格式,所以IPAddr这个自定义类型需要实现Stringer接口,实现代码如下:
// exercise-stringer.go package main import "fmt" type IPAddr [4]byte // TODO: Add a "String() string" method to IPAddr. func (ip IPAddr) String() string { return fmt.Sprintf("%v.%v.%v.%v", ip[0], ip[1], ip[2], ip[3]) } 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) } }
执行结果符合需求:
googleDNS: 8.8.8.8 loopback: 127.0.0.1
【参考资料】
1. Golang Language Specification - Methods Expression
2. Golang Language Specification - Interface Type
3. <The Way to Go - A Thorough Introduction to the Go Programming Language>一书第11.1节
4. Go Data Structures: Interfaces
5. Go Package fmt
6. A Tour of Go - Exercise: Stringers
===================== EOF ====================
有疑问加站长微信联系(非本文作者)