接口

Go语言中的接口是一组方法的签名。

接口的本质是引入一个新的中间层,调用方可以通过接口与具体实现分离,解除上下游的耦合,上层的模块不再需要依赖下层的具体模块,只需要依赖一个约定好的接口。

Go语言与鸭子类型

关于鸭子类型:

If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.

Duck Typing鸭子类型是动态编程语言的一种对象推断策略,它更关乎对象能如何被使用,而不是对象的类型本身。

动态语言python中,定义一个函数

1
2
def hello_world(coder):
    coder.say_hello()

当调用函数时,可以传入任意类型,只要它实现了say_hello()函数即可。如果没有实现,运行中会报错。

动态语言中,变量绑定的类型是不确定的,在运行期间才能确定。函数和方法可以接收任何类型的参数,且调用时不检查参数类型,不需要实现接口。

而在静态语言C++/Java中,比如显式声明实现了某个接口,才能用在需要这个接口的地方。编译器会在编译期进行检查,这也是静态语言比动态语言安全的原因。

Go采用了折中的做法,作为静态语言,它通过接口的方式支持鸭子类型。不要求类型显式声明实现接口,只需要实现接口的方法,编译器就可以检测到,是一种 “非侵入式”的实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import ("fmt")

type IGreeting interface {
    sayHello()
}

func sayHello(i IGreeting) {
    i.sayHello()
}

type Go struct {}
func (g Go) sayHello() {
    fmt.Println("hi, I'm Go")
}

type PHP struct{}
func (p PHP) sayHello() {
    fmt.Println("hi, I'm PHP")
}

func main() {
    golang := Go{}
    php := PHP{}
    sayHello(golang)
    sayHello(php)
}

值接收者与指针接收者实现接口

方法能给用户自定义的类型添加新的行为。方法和函数的区别在于,方法有一个接收者。接收者可以是指针类型,也可以是值类型。

调用方法时,类型的值对象既可以调用值接收者方法,也可以调用指针接收者方法;类型的指针对象也是一样都可以调用。编译器在中间做了处理。

接口在定义一组方法时,没有对实现接口的接收者做限制,实现接口的方法接收者可以是值类型,也可以是指针类型。

两种类型的实现方法不能同时存在,编译器会在结构体值类型和指针类型都实现一个方法时报错"method redeclared"。

以下代码块仅供举例,不能实际运行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type Cat struct {}
type Duck interface {
    Quack()
}

func (c  Cat) Quack() {}  // 使用结构体实现接口
func (c *Cat) Quack() {}  // 使用结构体指针实现接口

var d Duck = Cat{}      // 使用结构体初始化变量
var d Duck = &Cat{}     // 使用结构体指针初始化变量

实现接口的类型和对象初始化返回的类型两个维度共组成了四种情况,然而这四种情况不是都能通过编译器的检查:

结构体实现接口 结构体指针实现接口
结构体初始化变量 通过 不通过
结构体指针初始化变量 通过 通过

下面两种情况通过编译是应当的:

  • 方法接收者和初始化的对象都是结构体
  • 方法接收者和初始化对象都是指针

第三种情况,使用结构体实现接口,使用指针对象调用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type Cat struct{}

func (c Cat) Quack() {
	fmt.Println("meow")
}

func main() {
	var c Duck = &Cat{}
	c.Quack()
}

作为指针的&Cat{}变量能隐式地获取到指向的结构体(解引用),然后将以结构体的方式调用接口实现方法。

注意,这种情况虽然使用指针对象调用,但是由于真正执行方法的是结构体对象的拷贝(值传递),因此方法里对对象的修改仍不能反应到调用的对象上。

第四种情况,使用结构体指针实现接口,使用结构体对象调用,这种情况编译是不通过的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
type Duck interface {
	Quack()
}

type Cat struct{}

func (c *Cat) Quack() {
	fmt.Println("meow")
}

func main() {
	var c Duck = Cat{}
	c.Quack()
}

$ go build interface.go
./interface.go:20:6: cannot use Cat literal (type Cat) as type Duck in assignment:
	Cat does not implement Duck (Quack method has pointer receiver)

使用结构体对象的方式调用方法,值拷贝的是结构体对象,这时编译器不会去获取该对象的指针然后调用接口方法,因为这个对象并不是原先的对象。编译器也不会创建一个指针指向原先的对象。

因此,当使用指针实现接口时,只有指针类型的对象才能实现这个接口;当使用结构体实现接口时,指针类型和结构体类型的对象都会实现这个接口。

**为了方便记忆,可以这样去理解:实现了接收者是值类型的方法,就会自动实现接收者是指针类型的方法。而实现了指针类型的方法,不会自动生成对应的值类型方法。这点在给接口赋值时需要注意。**一定要检查类型的方法是值接收者还是指针接收者。

如何选择用哪种?

  • 如果方法的接收者是值类型,无论调用者是对象还是指针,修改的都是对象的副本,不影响调用者;
  • 如果方法的接收者是指针类型,则方法的修改会反应到调用者本身。

使用指针接收者的理由:

  • 方法能直接修改调用者
  • 避免每次调用方法都复制该值,仅传递指针即可

接口值和nil比较

interface的值包含两部分 ,分别是它的动态类型和动态值。因此,interface为nil,当且仅当它的动态类型和动态值都是nil。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

type TestStruct struct{}

func NilOrNot(v interface{}) bool {
	return v == nil
}

func main() {
	var s *TestStruct
	fmt.Println(s == nil)      // #=> true
	fmt.Println(NilOrNot(s))   // #=> false
}

$ go run main.go
true
false
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

type MyError struct {}

func (i MyError) Error() string {
	return "MyError"
}

func main() {
	err := Process()
	fmt.Println(err) // <nil> 值是nil
	fmt.Println(err == nil) // false 类型不是nil
}

func Process() error {
	var err *MyError = nil
	return err
}

数据结构iface与eface

接口也是Go语言中的一种类型,能够出现在变量的定义、函数的入参和返回值中并对它们进行约束。

内部实现中有两种不同的接口,一种是带一组方法的接口,另一种是不带任何方法的接口,分别为iface和eface。

1
2
3
4
5
6
7
8
type eface struct { // 16 字节
	_type *_type
	data  unsafe.Pointer
}
type iface struct { // 16 字节
	tab  *itab
	data unsafe.Pointer
}

空的interface不包含任何方法,因此eface只包含指向动态类型和数据的两个指针。

iface由含有类型信息的itab以及指向动态数据的指针。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
type _type struct { // go中描述各种类型的底层结构体,各种类型在_type基础上再添加业务字段
	size       uintptr // 类型占用的内存空间,用于内存分配
	ptrdata    uintptr
	hash       uint32  // 快速确定类型是否相等
	tflag      tflag
	align      uint8
	fieldAlign uint8
	kind       uint8
	equal      func(unsafe.Pointer, unsafe.Pointer) bool // 判断类型的多个对象是否相等
	gcdata     *byte
	str        nameOff
	ptrToThis  typeOff
}
type itab struct { // 32 字节
	inter *interfacetype // 接口的类型
	_type *_type // 动态值的类型
	hash  uint32 // 拷贝自_type.hash,用于类型判断
	_     [4]byte
	fun   [1]uintptr // 放置和接口方法对应的动态类型的方法地址,实现接口调用方法时的动态分派。多个方法按名称字典序排序,通过偏移量查找
}
1
2
3
4
5
type interfacetype struct {
	typ     _type // 接口类型
	pkgpath name  // 包路径
	mhdr    []imethod // 接口定义的方法列表
}

接口的构造和赋值,以及类型断言等是在汇编中实现的。

Go运行时通过itab中的fun字段来实现接口变量调用实体类型的方法,itab中的fun字段是在运行期间动态生成的。

原因在于,Go中的类型可能无意中实现了多个接口,很多接口业务代码是不需要的,所以不能为每个类型实现的每个接口都提前生成一个itab。实际上,运行时维护了一个全局的itab表,当需要一个itab对象时,先根据interfacetype和_type去全局itab表里查找,有则直接返回,没有就根据这两个字段新生成一个itab,然后放入全局表。

利用编译器自动检测是否实现接口

开源库中通常有这样的用法,目的就是检测对象是否实现了接口,如果没实现会有编译错误。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import "io"

type myWriter struct {}

func (w myWriter) Write(p []byte) (n int, err error) {
    return
}

func main() {
    // 检查 *myWriter 类型是否实现了 io.Writer 接口
    var _ io.Writer = (*myWriter)(nil)
    // 检查 myWriter 类型是否实现了 io.Writer 接口
    var _ io.Writer = myWriter{}
}

利用interface实现多态

Go语言并没有设计诸如虚函数、纯虚函数、继承、多重继承等概念,但它通过接口却非常优雅地支持了面向对象的特性。

多态是一种运行期的行为,它有以下几个特点:

  • 一种类型具有多种类型的能力
  • 允许不同的对象对同一消息做出灵活的反应
  • 以一种通用的方式对待个使用的对象
  • 非动态语言必须通过继承和接口的方式来实现

实现多态的原理是,根据接口对象运行时当前itab.fun字段指向的方法的地址调用当前绑定的动态对象的方法。

类型转换与类型断言

类型转换和类型断言本质都是把一个类型转换成另一个类型。不同之处在于类型断言是对接口变量进行的操作。

类型转换

类型转换前后的两种类型要相互兼容,语法为

1
<结果类型> := <目标类型>(<表达式>)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import "fmt"

func main() {
	var i int = 9

	var f float64
	f = float64(i)
	fmt.Printf("%T, %v\n", f, f)

	f = 10.8
	a := int(f)
	fmt.Printf("%T, %v\n", a, a)
}

类型断言

因为空接口 interface{}没有定义任何函数,因此 Go 中所有类型都实现了空接口。当一个函数的形参是 interface{},那么在函数中,需要对形参进行断言,从而得到它的真实类型。

1
2
<目标类型的值>,ok := <表达式interface>.(目标类型) // 安全类型断言
<目标类型的值> := <表达式interface>.(目标类型)  //非安全类型断言

一个类型断言表达式的语法为i.(T),其中i是一个接口值,T是一个类型名。T可以为一个非接口类型或者接口类型。

i称为断言值,T称为断言类型。

断言可能成功或者失败:

  • 对于 T 是一个非接口类型的情况,如果断言值 i 的动态类型存在并且此动态类型和 T 为同一类型, 则此断言将成功;否则,此断言失败。 当此断言成功时,此类型断言表达式的估值结果为断言 值 i 的动态值的一个复制。 我们可以把此种情况看作是一次拆封动态值的尝试。
  • 对于 T 是一个接口类型的情况,当断言值 i 的动态类型存在并且此动态类型实现了接口类型 T ,则 此断言将成功;否则,此断言失败。 当此断言成功时,此类型断言表达式的估值结果为一个包裹了断言值 i 的动态值的一个复制的 T 值。

使用类型断言的场景

Go中有四种接口相关的类型转换场景:

  1. 将一个非接口值转换为一个接口值。此时要求非接口值的类型必须实现了接口的类型。
  2. 将一个接口值转换为另一个接口类型(前者接口值的类型实现了后者接口类型)
  3. 将一个接口值转换为一个非接口类型(非接口类型必须实现了接口值的接口类型)
  4. 将一个接口值转换为另一个接口类型(前者接口值的类型未实现后者目标接口类型,但是前者的动态类型实现了目标接口类型)

后两种情况的合法性需要在运行时通过类型断言来验证。第二种情况也可以用类型断言。

非安全类型断言导致panic

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main

import "fmt"

type Student struct {
	Name string
	Age int
}

func main() {
	var i interface{} = new(Student)
	s := i.(Student) // panic: interface conversion: interface {} is *main.Student, not main.Student
	fmt.Println(s)
}
1
2
3
4
5
6
7
func main() {
	var i interface{} = new(Student)
	s, ok := i.(Student) // 安全的类型断言
	if ok { // false
		fmt.Println(s)
	}
}

switch-case-type断言

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
func judge(v interface{}) {
	fmt.Printf("%p %v\n", &v, v)

	switch v := v.(type) { // 获取v的类型并按照case顺序依次匹配,将断言的结果赋值给v
	case nil:
		fmt.Printf("%p %v\n", &v, v)
		fmt.Printf("nil type[%T] %v\n", v, v)

	case Student:
		fmt.Printf("%p %v\n", &v, v)
		fmt.Printf("Student type[%T] %v\n", v, v)

	case *Student:
		fmt.Printf("%p %v\n", &v, v)
		fmt.Printf("*Student type[%T] %v\n", v, v)

	default:
		fmt.Printf("%p %v\n", &v, v)
		fmt.Printf("unknow\n")
	}
}
type Student struct {
	Name string
	Age int
}
func main() {
	var i interface{} = new(Student)
	// var i interface{} = (*Student)(nil)
	// var i interface{}
	fmt.Printf("%p %v\n", &i, i)
	judge(i)
}

接口相关的其它内容

接口类型内嵌

一个接口类型可以内嵌另一个接口类型的名称。 此内嵌的效果相当于将此被内嵌的接口类型所指定的 所有方法原型展开到内嵌它的接口类型的定义体内。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

type Ia interface {
	fa()
}

type Ib interface {
	fb()
}

type Ic interface {
	fa()
	fb()
}

type Id = interface {
	Ia // 内嵌Ia
	Ib // 内嵌Ib
}

type Ie interface {
	Ia // 内嵌Ia
	fb()
}

type Ix interface {
	Ia // 内嵌
	Ic // 内嵌
	// 虽然有重叠,也是可以的
}

接口值相关的比较

接口值相关的比较有两种情形:

  1. 比较一个非接口值和接口值;
  2. 比较两个接口值。

对于第一种情形,非接口值的类型必须实现了接口值的类型,否则不能进行比较(假设此接口类型为I),所以此非接口值可以被隐式转化为(包裹到)一个I值中。 这意味着非接口值和接口值的比较可以转化为两个接口值的比较。

比较两个接口值其实是比较这两个接口值的动态类型和和动态值。两个接口值的比较结果只有在下面两种任一情况下才为true

  1. 这两个接口值都为nil接口值。
  2. 这两个接口值的动态类型相同、动态类型为可比较类型、并且动态值相等。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

func main() {
	var a, b, c interface{} = "abc", 123, "a"+"b"+"c"
	fmt.Println(a == b) // false
	fmt.Println(a == c) // true

	var x *int = nil
	var y *bool = nil
	var ix, iy interface{} = x, y
	var i interface{} = nil
	fmt.Println(ix == iy) // false
	fmt.Println(ix == i)  // false
	fmt.Println(iy == i)  // false

	var s []int = nil // []int为一个不可比较类型。
	i = s
	fmt.Println(i == nil) // false
	fmt.Println(i == i)   // will panic
}

指针动态值和非指针动态值

标准编译器/运行时对接口值的动态值为指针类型的情况做了特别的优化。 此优化使得接口值包裹指针动态值比包裹非指针动态值的效率更高。

对于小尺寸值,此优化的作用不大; 但是对于大尺寸值,包裹它的指针比包裹此值本身的效率高得多。 对于类型断言,此结论亦成立。

所以尽量避免在接口值中包裹大尺寸值。对于大尺寸值,应该尽量包裹它的指针。

[]T转换到[]I

一个 []T 类型的值不能直接被转换为类型 []I ,即使类型 T 实现了接口 类型 I。

比如,我们不能直接将一个[]string值转换为类型[]interface{}。 我们必须使用一个循环来实现此转换。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

func main() {
	words := []string{
		"Go", "is", "a", "high",
		"efficient", "language.",
	}

	// fmt.Println函数的原型为:
	// func Println(a ...interface{}) (n int, err error)
	// 所以words...不能传递给此函数的调用。
	// fmt.Println(words...) // 编译不通过

	// 将[]string值转换为类型[]interface{}。
	iw := make([]interface{}, 0, len(words))
	for _, w := range words {
		iw = append(iw, w)
	}
	fmt.Println(iw...) // 编译没问题
}

接口的方法对应一个隐式声明的函数

如果接口类型I指定了一个名为m的方法原型,则编译器会隐式声明一个与之对应的函数名为I.m的函数。

此函数比m的方法原型中的参数多一个。此多出来的参数为函数I.m的第一个参数,它的类型为I

对于一个类型为I的值i,方法调用i.m(...)和函数调用I.m(i, ...)是等价的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

type I interface {
	m(int)bool
}

type T string
func (t T) m(n int) bool {
	return len(t) > n
}

func main() {
	var i I = T("gopher")
	fmt.Println(i.m(5))                          // true
	fmt.Println(I.m(i, 5))                       // true
	fmt.Println(interface {m(int) bool}.m(i, 5)) // true

	// 下面这几行被执行的时候都将会产生一个恐慌。
	I(nil).m(5)
	I.m(nil, 5)
	interface {m(int) bool}.m(nil, 5)
}

结构体方法中也可以使用隐式声明的函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import "fmt"

type Person struct {
	name string
	age  int
}

func (p *Person) setName(n string) {
	p.name = n
}

func (p Person) setAge(i int) {
	p.age = i
}

func main() {
	var p = Person{}
	(*Person).setName(&p, "Jack")
	fmt.Println(p.name) // Jack
	// invalid method expression Person.setName (needs pointer receiver: (*Person).setName)
	// Person.setName(p, "Tom")

	Person.setAge(p, 10)
	fmt.Println(p.age) // 0
	(*Person).setAge(&p, 10)
	fmt.Println(p.age) // 0
}

可见

  • 如果是指针接收者,只能用指针的方式调用这个隐式方法,执行的修改能生效
  • 如果是值接收者,可以用值或者指针的方式调用这个隐式方法,不过执行的修改都不会对原来的对象生效。

例子

fmt.Println(v interface{})

fmt.Println 函数的参数是 interface。对于内置类型,函数内部会用穷举法,得出它的真实类型,然后转换为字符串打印。而对于自定义类型,首先确定该类型是否实现了 String() 方法,如果实现了,则直接打印输出 String() 方法的结果;否则,会通过反射来遍历对象的成员进行打印。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

type Student struct {
	Name string
	Age int
}
func (s Student) String() string {
	return fmt.Sprintf("[Name: %s], [Age: %d]", s.Name, s.Age)
}

func main() {
	var s = Student{ // 这里用对象或者对象指针都可以达到效果,因为实现接口的是值接收者。
		Name: "qcrao",
		Age: 18,
	}

	fmt.Println(s)
}

接口类型之间的类型转换

Go中常见的两个接口

1
2
3
4
5
6
7
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

进行一些转换和赋值

1
2
3
4
5
6
var r io.Reader
tty, err := os.OpenFile("/Users/qcrao/Desktop/test", os.O_RDWR, 0) // *os.File
if err != nil {
    return nil, err
}
r = tty

r是类型为io.Reader的接口对象,声明时它的静态类型是io.Reader,动态类型是nil,动态值是nil。

此时的r对象内存情况

r=tty

r = tty,将r的动态类型变为os.File** ,因为os.File 实现了io.Reader接口。

由于*os.File 还实现了io.Writer接口,因此下面的类型断言也是成功的

1
2
var w io.Writer
w = r.(io.Writer) // r是一个io.Reader接口对象,它的动态类型*os.File实现了io.Writer接口,因此可以执行该断言

w=r.(io.Writer)

和r相比,仅仅是fun对应的函数变了。

最后再一次赋值

1
2
var empty interface{}
empty = w

empty是一个空接口,对应的是eface,eface里的_type指向*os.File

empty=w

参考链接

面向信仰编程

深度解密Go语言之关于 interface 的10个问题

go101