Go 学习笔记(十九)Go 接口
本文原创地址:博客园骏马金龙Go 基础系列:Go 接口
接口用法简介
接口 (interface) 是一种类型,用来定义行为(方法)。
type Namer interface {
my_method1()
my_method2(para)
my_method3(para) return_type
...
}
但这些行为不会在接口上直接实现,而是需要用户自定义的方法来实现。所以,在上面的 Namer 接口类型中的方法my_methodN
都是没有实际方法体的,仅仅只是在接口 Namer 中存放这些方法的签名 (签名 = 函数名+参数(类型)+返回值(类型)
)。
当用户自定义的类型实现了接口上定义的这些方法,那么自定义类型的值 (也就是实例) 可以赋值给接口类型的值(也就是接口实例)。这个赋值过程使得接口实例中保存了用户自定义类型实例。
例如:
package main
import (
"fmt"
)
// Shaper 接口类型
type Shaper interface {
Area() float64
}
// Circle struct类型
type Circle struct {
radius float64
}
// Circle类型实现Shaper中的方法Area()
func (c *Circle) Area() float64 {
return 3.14 * c.radius * c.radius
}
// Square struct类型
type Square struct {
length float64
}
// Square类型实现Shaper中的方法Area()
func (s *Square) Area() float64 {
return s.length * s.length
}
func main() {
// Circle类型的指针类型实例
c := new(Circle)
c.radius = 2.5
// Square类型的值类型实例
s := Square{3.2}
// Sharpe接口实例ins1,它只能是值类型的
var ins1 Shaper
// 将Circle实例c赋值给接口实例ins1
// 那么ins1中就保存了实例c
ins1 = c
fmt.Println(ins1)
// 使用类型推断将Square实例s赋值给接口实例
ins2 := s
fmt.Println(ins2)
}
上面将输出:
&{2.5}
{3.2}
从上面输出结果中可以看出,两个接口实例 ins1 和 ins2 被分别赋值后, 分别保存了指针类型的 Circle 实例 c 和值类型的 Square 实例 s 。
另外,从上面赋值 ins1 和 ins2 的赋值语句上看:
ins1 = c
ins2 := s
是否说明接口实例 ins 就是自定义类型的实例?实际上接口是指针类型 (指向什么见下文)。这个时候,自定义类型的实例 c、s 称为具体实例,ins 实例是抽象实例,因为 ins 接口中定义的行为(方法) 并没有具体的行为模式,而 c、s 中的行为是具体的。
因为接口实例 ins 也是自定义类型的实例,所以 当接口实例中保存了自定义类型的实例后,就可以直接从接口上调用它所保存的实例的方法 。例如:
fmt.Println(ins1.Area()) // 输出19.625
fmt.Println(ins2.Area()) // 输出10.24
这里ins1.Area()
调用的是 Circle 类型上的方法 Area(),ins2.Area()
调用的则是 Square 类型上的方法 Area()。这说明 Go 的接口可以实现面向对象中的多态:可以按需调用名称相同、功能不同的方法 。
接口实例中存的是什么
前面说了,接口类型是指针类型,但是它到底存放了什么东西?
接口类型的数据结构是 2 个指针,占用 2 个机器字长。
当将类型实例c
赋值给接口实例ins1
后,用println()
函数输出 ins1 和 c,比较它们的地址:
println(ins1)
println(c)
输出结果:
(0x4ceb00,0xc042068058)
0xc042068058
从结果中可以看出,接口实例中包含了两个地址,其中第二个地址和类型实例 c 的地址是完全相同的。而第二个地址c
是 Circle 的指针类型实例,所以 ins 中的第二个值也是指针。
ins 中的第一个是指针是什么?它所指向的是一个内部表结构 iTable,这个 Table 中包含两部分:第一部分是实例 c 的类型信息,也就是*Circle
,第二部分是这个类型 (Circle) 的方法集,也就是 Circle 类型的所有方法(此示例中 Circle 只定义了一个方法 Area())。
所以,如图所示:
注意,上图中的实例 c 是指针,是指针类型的 Circle 实例。
对于值类型的 Square 实例s
,ins2 保存的内容则如下图:
实际上接口实例中保存的内容,在反射 (reflect) 中体现的淋漓尽致,reflect 所有的一切都离不开接口实例保存的内容。
方法集 (Method Set) 规则
实例的 method set 决定了它所实现的接口,以及通过 receiver 可以调用的方法。
方法集是类型的方法集合,对于非接口类型, 每个类型都分两个 Method Set:值类型实例是一个 Method Set,指针类型的实例是另一个 Method Set 。两个 Method Set 由不同 receiver 类型的方法组成:
实例的类型 | receiver |
---|---|
值类型:T | (T Type) |
指针类型:*T | (T Type) 或 (T *Type) |
也就是说:
- 值类型的实例的 Method Set 只由值类型的 receiver
(T Type)
组成 - 指针类型的实例的 Method Set 由值类型和指针类型的 receiver 共同组成,即
(T Type)
和(T *Type)
这是什么意思呢?从 receiver 的角度去考虑:
receiver | 实例的类型 |
---|---|
(T Type) | T 或 *T |
(T *Type) | *T |
上面的意思是:
- 如果某类型实现接口的方法的 receiver 是
(T *Type)
类型的,那么只有指针类型的实例*T
才算是实现了这个接口 - 如果某类型实现接口的方法的 receiver 是
(T Type)
类型的,那么值类型的实例T
和指针类型的实例*T
都算实现了这个接口
举个例子。接口方法 Area(),自定义类型 Circle 有一个 receiver 类型为(c *Circle)
的 Area() 方法时,说明实现了接口的方法,但只有 Circle 实例的类型为指针类型时,这个实例才算是实现了接口,才能赋值给接口实例,才能当作一个接口参数。如下:
package main
import "fmt"
// Shaper 接口类型
type Shaper interface {
Area() float64
}
// Circle struct类型
type Circle struct {
radius float64
}
// Circle类型实现Shaper中的方法Area()
// receiver类型为指针类型
func (c *Circle) Area() float64 {
return 3.14 * c.radius * c.radius
}
func main() {
// 声明2个接口实例
var ins1, ins2 Shaper
// Circle的指针类型实例
c1 := new(Circle)
c1.radius = 2.5
ins1 = c1
fmt.Println(ins1.Area())
// Circle的值类型实例
c2 := Circle{3.0}
// 下面的将报错
ins2 = c2
fmt.Println(ins2.Area())
}
报错结果:
cannot use c2 (type Circle) as type Shaper
in assignment:
Circle does not implement Shaper (Area method has
pointer receiver)
它的意思是,Circle 值类型的实例 c2 没有实现 Share 接口的 Area()方法,它的 Area() 方法是指针类型的 receiver。换句话说, 值类型的 c2 实例的 Method Set 中没有 receiver 类型为指针的 Area() 方法 。
所以,上面应该改成:
ins2 = &c2
再声明一个方法,它的 receiver 是值类型的。下面的代码一切正常。
type Square struct{
length float64
}
// 实现方法Area(),receiver为值类型
func (s Square) Area() float64{
return s.length * s.length
}
func main() {
var ins3,ins4 Shaper
// 值类型的Square实例s1
s1 := Square{3.0}
ins3 = s1
fmt.Println(ins3.Area())
// 指针类型的Square实例s2
s2 := new(Square)
s2.length=4.0
ins4 = s2
fmt.Println(ins4.Area())
}
所以,从 struct 类型定义的方法的角度去看, 如果这个类型的方法有指针类型的 receiver 方法,则只能使用指针类型的实例赋值给接口变量,才算是实现了接口 。如果这个类型的方法全是值类型的 receiver 方法,则可以随意使用值类型或指针类型的实例赋值给接口变量。下面这两个对应关系,对于理解很有帮助:
实例的类型 | receiver |
---|---|
值类型:T | (T Type) |
指针类型:*T | (T Type) 或 (T *Type) |
receiver | 实例的类型 |
---|---|
(T Type) | T 或 *T |
(T *Type) | *T |
很经常的,我们会直接使用推断类型的赋值方式 ( 如ins2 := c2
)将实例赋值给一个变量,我们以为这个变量是接口的实例,但实际上并不一定。正如上面值类型的 c2 赋值给 ins2,这个 ins2 将是从 c2 数据结构拷贝而来的另一个副本数据结构,并非接口实例,但这时通过 ins2 也能调用 Area() 方法:
c2 = Circle{3.2}
ins2 := c2
fmt.Println(ins2.Area()) // 正常执行
之所以能调用,是因为 Circle 类型中有 Area() 方法,但这不是通过接口去调用的。
所以, 在使用接口的时候,应当尽量使用 var 先声明接口类型的实例,再将类型的实例赋值给接口实例 ( 如var ins1,ins2 Shaper
),或者使用ins1 := Shaper(c1)
的方式 。这样,如果赋值给接口实例的类型实例没有实现该接口,将会报错。
但是,为什么要限制指针类型的 receiver 只能是指针类型的实例的 Method Set 呢?
看下图,假如指针类型的 receiver 可以组成值类型实例的 Method Set,那么接口实例的第二个指针就必须找到值类型的实例的地址。但实际上,并非所有值类型的实例都能获取到它们的地址。
哪些值类型的实例找不到地址?最常见的是那些简单数据类型的别名类型,如果匿名生成它们的实例,它们的地址就会被 Go 彻底隐藏,外界找不到这个实例的地址。
例如:
package main
import "fmt"
type myint int
func (m *myint) add() myint {
return *m + 1
}
func main() {
fmt.Println(myint(3).add())
}
以下是报错信息:找不到 myint(3) 的地址
abc\abc.go:11:22: cannot call pointer method on myint(3)
abc\abc.go:11:22: cannot take the address of myint(3)
这里的myint(3)
是匿名的 myint 实例,它的底层是简单数据类型 int,myint(3)
的地址会被彻底隐藏,只会提供它的值对象 3。
普通方法和实现接口方法的区别
对于普通方法,无论是值类型还是指针类型的实例,都能正常调用,且调用时拷贝的内容都由 receiver 的类型决定 。
func (T Type) method1 // 值类型receiver
func (T *Type) method2 // 指针类型receiver
指针类型的 receiver 决定了无论是值类型还是指针类型的实例,都拷贝实例的指针。值类型的 receiver 决定了无论是值类型还是指针类型的实例,都拷贝实例本身 。
所以,对于 person 数据结构:
type person struct {}
p1 := person{} // 值类型的实例
p2 := new(person) // 指针类型的实例
p1.method1()
和p2.method1()
都是拷贝整个 person 实例,只不过 Go 对待p2.method1()
时多一个 "步骤":将其解除引用。所以p2.method1()
等价于(*p2).method1()
。
p1.method2()
和p2.method2()
都拷贝 person 实例的指针,只不过 Go 对待p1.method2()
时多一个 "步骤":创建一个额外的引用。所以,p1.method2()
等价于(&p1).method2()
。
而类型实现接口方法时,method set 规则决定了类型实例是否实现了接口。
receiver | 实例的类型 |
---|---|
(T Type) | T 或 *T |
(T *Type) | *T |
对于接口 abc、接口方法 method1()、method2() 和结构 person:
type abc interface {
method1
method2
}
type person struct {}
func (T person) method1 // 值类型receiver
func (T *person) method2 // 指针类型receiver
p1 := abc(person) // 接口变量保存值类型实例
p2 := abc(&person) // 接口变量保存指针类型实例
p2.method1()
、p2.method2()
以及p1.method1()
都是允许的,都会通过接口实例去调用具体 person 实例的方法。
但p1.method2()
是错误的,因为 method2()的 receiver 是指针类型的,导致 p1 没有实现接口 abc 的 method2() 方法。
接口类型作为参数
将接口类型作为参数很常见。这时,那些实现接口的实例都能作为接口类型参数传递给函数 / 方法。
例如,下面的 myArea() 函数的参数是n Shaper
,是接口类型。
package main
import (
"fmt"
)
// Shaper 接口类型
type Shaper interface {
Area() float64
}
// Circle struct类型
type Circle struct {
radius float64
}
// Circle类型实现Shaper中的方法Area()
func (c *Circle) Area() float64 {
return 3.14 * c.radius * c.radius
}
func main() {
// Circle的指针类型实例
c1 := new(Circle)
c1.radius = 2.5
myArea(c1)
}
func myArea(n Shaper) {
fmt.Println(n.Area())
}
上面myArea(c1)
是将 c1 作为接口类型参数传递给 n,然后调用c1.Area()
,因为实现了接口方法,所以调用的是 Circle 的 Area()。
如果实现接口方法的 receiver 是指针类型的,但却是值类型的实例,将没法作为接口参数传递给函数,原因前面已经解释过了,这种类型的实例没有实现接口。
以接口作为方法或函数的参数,将使得一切都变得灵活且通用,只要是实现了接口的类型实例,都可以去调用它。
用的非常多的fmt.Println()
,它的参数也是接口,而且是变长的接口参数:
$ go doc fmt Println
func Println(a ...interface{}) (n int, err error)
每一个参数都会放进一个名为 a 的 Slice 中,Slice 中的元素是接口类型,而且是空接口,这使得无需实现任何方法,任何东西都可以丢带 fmt.Println() 中来,至于每个东西怎么输出,那就要看具体情况。
接口类型的嵌套
接口可以嵌套,嵌套的内部接口将属于外部接口,内部接口的方法也将属于外部接口。
例如,File 接口内部嵌套了 ReadWrite 接口和 Lock 接口。
type ReadWrite interface {
Read(b Buffer) bool
Write(b Buffer) bool
}
type Lock interface {
Lock()
Unlock()
}
type File interface {
ReadWrite
Lock
Close()
}
除此之外,类型嵌套时,如果内部类型实现了接口,那么外部类型也会自动实现接口,因为内部属性是属于外部属性的。