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())。

所以,如图所示:
1.png

注意,上图中的实例 c 是指针,是指针类型的 Circle 实例。

对于值类型的 Square 实例s,ins2 保存的内容则如下图:
2.png

实际上接口实例中保存的内容,在反射 (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,那么接口实例的第二个指针就必须找到值类型的实例的地址。但实际上,并非所有值类型的实例都能获取到它们的地址。
3.png

哪些值类型的实例找不到地址?最常见的是那些简单数据类型的别名类型,如果匿名生成它们的实例,它们的地址就会被 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()
}

除此之外,类型嵌套时,如果内部类型实现了接口,那么外部类型也会自动实现接口,因为内部属性是属于外部属性的。

上一篇 Go 学习笔记(十八)struct 的导出和暴露问题
Go 学习笔记(目录)
下一篇 Go 学习笔记(二十)空接口