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 类型的变量。
请注意 Vertex 的 Abs 方法,接收者参数是指针类型 *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
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 )。因为空接口底层二元组中,连实际类型都是 nil ,Go 怎么知道该调哪个版本的方法呢?
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
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 )提供了将接口值转换成实际类型的手段:
这个语句先检查接口底层值,确保它的实际类型是 T ,然后将实际值赋给变量 t ;如果底层值不是类型 T ,语句将触发 panic 报错。
为了判断一个接口值是否为特定类型,类型断言支持返回一个额外的布尔值,表明断言是否成功:
- 如果 i 底层值实际类型是 T ,t 就获得底层值,而 ok 就是
true
;
- 否则,ok 就是
false
而 t 就是类型 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 的底层值是否为类型 T 或 S 。在 T 或 S 类型 case 分支,变量 v 的类型便是对应的 T 或 S ,而值就是 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语言】系列文章首发于公众号【小菜学编程】,敬请关注: