Go语言接口简介

Go 也有 接口interface )的概念,它是一种特殊的类型,定义了一系列方法签名。其他任意类型,只要实现了接口声明的所有方法,就可以被视为接口类型,赋值给接口变量。

 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
34
35
36
37
38
39
40
41
42
43
44
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)
}

type Vertex struct {
	X, Y float64
}

func (v *Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
	var a Abser
	f := MyFloat(-math.Sqrt2)
	v := Vertex{3, 4}

	a = f  // a MyFloat implements Abser
	a = &v // a *Vertex implements Abser

	// In the following line, v is a Vertex (not *Vertex)
	// and does NOT implement Abser.
	a = v

	fmt.Println(a.Abs())
}

上面这个例子,MyFloat*Vertex 都实现了接口 Abser ,因此可以被赋值给 Abser 类型的变量。

请注意 VertexAbs 方法,接收者参数是指针类型 *Vertex ,而不是值类型 Vertex 。换句话讲,Vertex 并未实现 Abser 接口,因此第 41 行赋值编译会报错。

隐式实现

跟其他语言不同,Go 语言无须用 implements 显式实现接口。一个类型只要实现接口声明的所有方法,它就自动实现了接口。这种接口实现机制就是所谓的 隐式实现implemented implicitly )。

请看这个例子,类型 T 实现了接口 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
package main

import "fmt"

type I interface {
	M()
}

type T struct {
	S string
}

// This method means type T implements the interface I,
// but we don't need to explicitly declare that it does so.
func (t T) M() {
	fmt.Println(t.S)
}

func main() {
	var i I = T{"hello"}
	i.M()
}

隐式实现机制将接口的定义和实现完全 解耦,两者可以放在不同的包,无须提前规划好,因而更加灵活。

接口值

接口类型的变量值在底层可以看作是一个由 和实际 类型 构成的二元组:

1
(value, 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
34
35
36
37
38
39
40
package main

import (
	"fmt"
	"math"
)

type I interface {
	M()
}

type T struct {
	S string
}

func (t *T) M() {
	fmt.Println(t.S)
}

type F float64

func (f F) M() {
	fmt.Println(f)
}

func describe(i I) {
	fmt.Printf("(%v, %T)\n", i, i)
}

func main() {
	var i I

	i = &T{"hello"}
	describe(i)
	i.M()

	i = F(math.Pi)
	describe(i)
	i.M()
}

请看这个例子,接口 i 被赋为 *T 类型的值,describe 可以输出它的实际类型 *T 。当我们调用方法 M 时,执行的是 *T 定义的版本。

空底层值

如果接口保存的底层值是 nil ,那么方法被调用时的接收者参数也是 nil

这在其他编程语言,很可能会触发指针异常。好在 Go 的方法只要编写得当,就能优雅地处理空接收者参数。请看这个例子,方法 M 可以很好地处理这种情况:

 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
34
35
36
package main

import "fmt"

type I interface {
	M()
}

type T struct {
	S string
}

func (t *T) M() {
	if t == nil {
		fmt.Println("<nil>")
		return
	}
	fmt.Println(t.S)
}

func describe(i I) {
	fmt.Printf("(%v, %T)\n", i, i)
}

func main() {
	var i I

	var t *T
	i = t
	describe(i)
	i.M()

	i = &T{"hello"}
	describe(i)
	i.M()
}

需要特别注意,就算接口保存了一个空的底层值,接口本身并不是空的,因为它还保存着类型信息。

空接口值

空接口值跟空底层值不同,它不仅底层值是空的,实际类型也是空的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 空底层值
var t *T
var i I = t
// 底层值:nil
// 实际类型:*T

// 空接口值
var i I = nil
// 底层值:nil
// 实际类型:nil
// 报错
i.M()

以空接口为身份进行方法调用,将报 运行时错误run time error )。因为空接口底层二元组中,连实际类型都是 nilGo 怎么知道该调哪个版本的方法呢?

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

import "fmt"

type I interface {
	M()
}

func describe(i I) {
	fmt.Printf("(%v, %T)\n", i, i)
}

func main() {
	var i I
	describe(i)
	i.M()
}

空接口

如果接口类型没有声明任何方法,就是所谓的 空接口

1
interface{}

空接口不要求实现任何方法,因此可以保存任意类型的任意值:

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

import "fmt"

func describe(i interface{}) {
	fmt.Printf("(%v, %T)\n", i, i)
}

func main() {
	var i interface{}
	describe(i)

	i = 42
	describe(i)

	i = "hello"
	describe(i)
}

空接口通常用在需要处理未知数据类型的代码场景。举个例子, fmt.Print 接受 interface{} 类型作为参数,因为待打印的数据类型是不确定的。

类型断言

类型断言type assertion )提供了将接口值转换成实际类型的手段:

1
t := i.(T)

这个语句先检查接口底层值,确保它的实际类型是 T ,然后将实际值赋给变量 t ;如果底层值不是类型 T ,语句将触发 panic 报错。

为了判断一个接口值是否为特定类型,类型断言支持返回一个额外的布尔值,表明断言是否成功:

1
t, ok := i.(T)
  • 如果 i 底层值实际类型是 Tt 就获得底层值,而 ok 就是 true
  • 否则,ok 就是 falset 就是类型 T 的零值,但不会 panic
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func main() {
	var i interface{} = "hello"

	s := i.(string)
	fmt.Println(s)

	s, ok := i.(string)
	fmt.Println(s, ok)

	f, ok := i.(float64)
	fmt.Println(f, ok)

	f = i.(float64) // panic
	fmt.Println(f)
}

注意到,这种语法跟读取映射表有点类似。

类型分支

类型分支type switch )是一种可以连续写多个类型断言的语句结构。

类型分支跟普通的 switch 语句类似,只不过 case 语句指定的是类型,而不是值。接口值的实际类型跟每个 case 语句指定的类型逐一比较,以确定匹配的 case 分支。

1
2
3
4
5
6
7
8
switch v := i.(type) {
case T:
    // here v has type T
case S:
    // here v has type S
default:
    // no match; here v has the same type as i
}

类型分支中的声明语法跟类型断言 i.(T) 一样,只不过类型 T 被换成关键字 type

上面这个类型分支语句,测试接口值 i 的底层值是否为类型 TS 。在 TS 类型 case 分支,变量 v 的类型便是对应的 TS ,而值就是 i 的底层值。

如果所有的 case 分支都不命中,便执行 default 分支,这时变量 v 的类型和值都跟 i 一样。

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

import "fmt"

func do(i interface{}) {
	switch v := i.(type) {
	case int:
		fmt.Printf("Twice %v is %v\n", v, v*2)
	case string:
		fmt.Printf("%q is %v bytes long\n", v, len(v))
	default:
		fmt.Printf("I don't know about type %T!\n", v)
	}
}

func main() {
	do(21)
	do("hello")
	do(true)
}

【小菜学Go语言】系列文章首发于公众号【小菜学编程】,敬请关注:

【小菜学Go语言】系列文章首发于公众号【小菜学编程】,敬请关注: