在面向对象编程中,可以这么说:“接口定义了对象的行为”, 那么具体的实现行为就取决于对象了。
在Go中,接口是一组方法签名(声明的是一组方法的集合)。当一个类型为接口中的所有方法提供定义时,它被称为实现该接口。它与oop非常相似。接口指定类型应具有的方法,类型决定如何实现这些方法。
让我们来看看这个例子: Animal
类型是一个接口,我们将定义一个 Animal
作为任何可以说话的东西。这是 Go 类型系统的核心概念:我们根据类型可以执行的操作而不是其所能容纳的数据类型来设计抽象。
1 2 3 |
|
非常简单:我们定义 Animal
为任何具有 Speak
方法的类型。Speak
方法没有参数,返回一个字符串。所有定义了该方法的类型我们称它实现了 Animal
接口。Go 中没有 implements
关键字,判断一个类型是否实现了一个接口是完全是自动地。让我们创建几个实现这个接口的类型:
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 |
|
我们现在有四种不同类型的动物:Dog
、Cat
、Llama
和 JavaProgrammer
。在我们的 main
函数中,我们创建了一个 []Animal{Dog{}, Cat{}, Llama{}, JavaProgrammer{}}
,看看每只动物都说了些什么:
1 2 3 4 5 6 |
|
interface{}
类型
interface{}
类型,空接口,是导致很多混淆的根源。interface{}
类型是没有方法的接口。由于没有 implements
关键字,所以所有类型都至少实现了 0 个方法,所以 所有类型都实现了空接口。这意味着,如果您编写一个函数以 interface{}
值作为参数,那么您可以为该函数提供任何值。例如:
1 2 3 |
|
这里是让人困惑的地方:在 DoSomething
函数内部,v
的类型是什么?新手们会认为 v
是任意类型的,但这是错误的。v
不是任意类型,它是 interface{}
类型。对的,没错!当将值传递给DoSomething
函数时,Go 运行时将执行类型转换(如果需要),并将值转换为 interface{}
类型的值。所有值在运行时只有一个类型,而 v
的一个静态类型是 interface{}
。
这可能让您感到疑惑:好吧,如果发生了转换,到底是什么东西传入了函数作为 interface{}
的值呢?(具体到上例来说就是 []Animal
中存的是啥?)
一个接口值由两个字(32 位机器一个字是 32 bits,64 位机器一个字是 64 bits)组成;一个字用于指向该值底层类型的方法表,另一个字用于指向实际数据。我不想没完没了地谈论这个。
在我们上面的例子中,当我们初始化变量 animals
时,我们不需要像这样 Animal(Dog{})
来显示的转型,因为这是自动地。这些元素都是 Animal
类型,但是他们的底层类型却不相同。
为什么这很重要呢?理解接口是如何在内存中表示的,可以使得一些潜在的令人困惑的事情变得非常清楚。比如,像 “我可以将 []T 转换为 []interface{}
吗?” 这种问题就容易回答了。下面是一些烂代码的例子,它们代表了对 interface{}
类型的常见误解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
运行这段代码你会得到如下错误:cannot use names (type []string) as type []interface {} in argument to PrintAll
。如果想使其正常工作,我们必须将 []string
转为 []interface{}
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
很丑陋,但是生活就是这样,没有完美的事情。(事实上,这种情况不会经常发生,因为 []interface{}
并没有像你想象的那样有用)
指针和接口
接口的另一个微妙之处是接口定义没有规定一个实现者是否应该使用一个指针接收器或一个值接收器来实现接口。当给定一个接口值时,不能保证底层类型是否为指针。在前面的示例中,我们将方法定义在值接收者之上。让我们稍微改变一下,将 Cat
的 Speak()
方法改为指针接收器:
1 2 3 |
|
运行上述代码,会得到如下错误:
1 2 |
|
该错误的意思是:你尝试将 Cat
转为 Animal
,但是只有 *Cat
类型实现了该接口。你可以通过传入一个指针 (new(Cat)
或者 &Cat{}
)来修复这个错误。
1 |
|
让我们做一些相反的事情:我们传入一个 *Dog
指针,但是不改变 Dog
的 Speak()
方法:
1 |
|
这种方式可以正常工作,因为一个指针类型可以通过其相关的值类型来访问值类型的方法,但是反过来不行。即,一个 *Dog
类型的值可以使用定义在 Dog
类型上的 Speak()
方法,而 Cat
类型的值不能访问定义在 *Cat
类型上的方法。
这可能听起来很神秘,但当你记住以下内容时就清楚了:Go 中的所有东西都是按值传递的。每次调用函数时,传入的数据都会被复制。对于具有值接收者的方法,在调用该方法时将复制该值。例如下面的方法:
1 2 3 |
|
是 func(T, string)
类型的方法。方法接收器像其他参数一样通过值传递给函数。
因为所有的参数都是通过值传递的,这就可以解释为什么 *Cat
的方法不能被 Cat
类型的值调用了。任何一个 Cat
类型的值可能会有很多 *Cat
类型的指针指向它,如果我们尝试通过 Cat
类型的值来调用 *Cat
的方法,根本就不知道对应的是哪个指针。相反,如果 Dog
类型上有一个方法,通过 *Dog
来调用这个方法可以确切的找到该指针对应的 Gog
类型的值,从而调用上面的方法。运行时,Go 会自动帮我们做这些,所以我们不需要像 C语言中那样使用类似如下的语句 d->Speak()
。
结语
我希望读完此文后你可以更加得心应手地使用 Go 中的接口,记住下面这些结论:
- 通过考虑数据类型之间的相同功能来创建抽象,而不是相同字段
interface{}
的值不是任意类型,而是interface{}
类型- 接口包含两个字的大小,类似于
(type, value)
- 函数可以接受
interface{}
作为参数,但最好不要返回interface{}
- 指针类型可以调用其所指向的值的方法,反过来不可以
- 函数中的参数甚至接受者都是通过值传递
- 一个接口的值就是就是接口而已,跟指针没什么关系
- 如果你想在方法中修改指针所指向的值,使用
*
操作符
空接口是接口类型的特殊形式,空接口没有任何方法,因此任何类型都无须实现空接口。从实现的角度看,任何值都满足这个接口的需求。因此空接口类型可以保存任何值,也可以从空接口中取出原值。
提示
空接口类型类似于 C# 或 Java 语言中的 Object、C语言中的 void*、C++ 中的 std::any。在泛型和模板出现前,空接口是一种非常灵活的数据抽象保存和使用的方法。
空接口的内部实现保存了对象的类型和指针。使用空接口保存一个数据的过程会比直接用数据对应类型的变量保存稍慢。因此在开发中,应在需要的地方使用空接口,而不是在所有地方使用空接口。
将值保存到空接口
空接口的赋值如下:
- var any interface{}
- any = 1
- fmt.Println(any)
- any = "hello"
- fmt.Println(any)
- any = false
- fmt.Println(any)
代码输出如下:
1
hello
false
对代码的说明:
- 第 1 行,声明 any 为 interface{} 类型的变量。
- 第 3 行,为 any 赋值一个整型 1。
- 第 4 行,打印 any 的值,提供给 fmt.Println 的类型依然是 interface{}。
- 第 6 行,为 any 赋值一个字符串 hello。此时 any 内部保存了一个字符串。但类型依然是 interface{}。
- 第 9 行,赋值布尔值。
从空接口获取值
保存到空接口的值,如果直接取出指定类型的值时,会发生编译错误,代码如下:
- // 声明a变量, 类型int, 初始值为1
- var a int = 1
- // 声明i变量, 类型为interface{}, 初始值为a, 此时i的值变为1
- var i interface{} = a
- // 声明b变量, 尝试赋值i
- var b int = i
第8行代码编译报错:
cannot use i (type interface {}) as type int in assignment: need type assertion
编译器告诉我们,不能将i变量视为int类型赋值给b。
在代码第 15 行中,将 a 的值赋值给 i 时,虽然 i 在赋值完成后的内部值为 int,但 i 还是一个 interface{} 类型的变量。类似于无论集装箱装的是茶叶还是烟草,集装箱依然是金属做的,不会因为所装物的类型改变而改变。
为了让第 8 行的操作能够完成,编译器提示我们得使用 type assertion,意思就是类型断言。
使用类型断言修改第 8 行代码如下:
- var b int = i.(int)
修改后,代码可以编译通过,并且 b 可以获得 i 变量保存的 a 变量的值:1。
空接口的值比较
空接口在保存不同的值后,可以和其他变量值一样使用==
进行比较操作。空接口的比较有以下几种特性。
1) 类型不同的空接口间的比较结果不相同
保存有类型不同的值的空接口进行比较时,Go语言会优先比较值的类型。因此类型不同,比较结果也是不相同的,代码如下:
- // a保存整型
- var a interface{} = 100
- // b保存字符串
- var b interface{} = "hi"
- // 两个空接口不相等
- fmt.Println(a == b)
代码输出如下:
false
2) 不能比较空接口中的动态值
当接口中保存有动态类型的值时,运行时将触发错误,代码如下:
- // c保存包含10的整型切片
- var c interface{} = []int{10}
- // d保存包含20的整型切片
- var d interface{} = []int{20}
- // 这里会发生崩溃
- fmt.Println(c == d)
代码运行到第8行时发生崩溃:
panic: runtime error: comparing uncomparable type []int
这是一个运行时错误,提示 []int 是不可比较的类型。下表中列举出了类型及比较的几种情况。
类 型 | 说 明 |
---|---|
map | 宕机错误,不可比较 |
切片([]T) | 宕机错误,不可比较 |
通道(channel) | 可比较,必须由同一个 make 生成,也就是同一个通道才会是 true,否则为 false |
数组([容量]T) | 可比较,编译期知道两个数组是否一致 |
结构体 | 可比较,可以逐个比较结构体的值 |
函数 | 可比较 |
空接口可以保存任何类型这个特性可以方便地用于容器的设计。下面例子使用 map 和 interface{} 实现了一个字典。字典在其他语言中的功能和 map 类似,可以将任意类型的值做成键值对保存,然后进行找回、遍历操作。详细实现过程请参考下面的代码。
- package main
- import "fmt"
- // 字典结构
- type Dictionary struct {
- data map[interface{}]interface{} // 键值都为interface{}类型
- }
- // 根据键获取值
- func (d *Dictionary) Get(key interface{}) interface{} {
- return d.data[key]
- }
- // 设置键值
- func (d *Dictionary) Set(key interface{}, value interface{}) {
- d.data[key] = value
- }
- // 遍历所有的键值,如果回调返回值为false,停止遍历
- func (d *Dictionary) Visit(callback func(k, v interface{}) bool) {
- if callback == nil {
- return
- }
- for k, v := range d.data {
- if !callback(k, v) {
- return
- }
- }
- }
- // 清空所有的数据
- func (d *Dictionary) Clear() {
- d.data = make(map[interface{}]interface{})
- }
- // 创建一个字典
- func NewDictionary() *Dictionary {
- d := &Dictionary{}
- // 初始化map
- d.Clear()
- return d
- }
- func main() {
- // 创建字典实例
- dict := NewDictionary()
- // 添加游戏数据
- dict.Set("My Factory", 60)
- dict.Set("Terra Craft", 36)
- dict.Set("Don't Hungry", 24)
- // 获取值及打印值
- favorite := dict.Get("Terra Craft")
- fmt.Println("favorite:", favorite)
- // 遍历所有的字典元素
- dict.Visit(func(key, value interface{}) bool {
- // 将值转为int类型,并判断是否大于40
- if value.(int) > 40 {
- // 输出很贵
- fmt.Println(key, "is expensive")
- return true
- }
- // 默认都是输出很便宜
- fmt.Println(key, "is cheap")
- return true
- })
- }
值设置和获取
字典内部拥有一个 data 字段,其类型为 map。这个 map 的键和值都是 interface{} 类型,也就是实现任意类型关联任意类型。字典的值设置和获取通过 Set() 和 Get() 两个方法来完成,参数都是 interface{}。详细实现代码如下:
- // 字典结构
- type Dictionary struct {
- data map[interface{}]interface{} // 键值都为interface{}类型
- }
- // 根据键获取值
- func (d *Dictionary) Get(key interface{}) interface{} {
- return d.data[key]
- }
- // 设置键值
- func (d *Dictionary) Set(key interface{}, value interface{}) {
- d.data[key] = value
- }
代码说明如下:
- 第 3 行,Dictionary 的内部实现是一个键值均为 interface{} 类型的 map,map 也具备与 Dictionary 一致的功能。
- 第 8 行,通过 map 直接获取值,如果键不存在,将返回 nil。
- 第 13 行,通过 map 设置键值。
遍历字段的所有键值关联数据
每个容器都有遍历操作。遍历时,需要提供一个回调返回需要遍历的数据。为了方便在必要时终止遍历操作,可以将回调的返回值设置为 bool 类型,外部逻辑在回调中不需要遍历时直接返回 false 即可终止遍历。
Dictionary 的 Visit() 方法需要传入回调函数,回调函数的类型为 func(k,v interface{})bool。每次遍历时获得的键值关联数据通过回调函数的 k 和 v 参数返回。Visit 的详细实现请参考下面的代码:
- // 遍历所有的键值, 如果回调返回值为false, 停止遍历
- func (d *Dictionary) Visit(callback func(k, v interface{}) bool) {
- if callback == nil {
- return
- }
- for k, v := range d.data {
- if !callback(k, v) {
- return
- }
- }
- }
代码说明如下:
- 第 2 行,定义回调,类型为 func(k,v interface{})bool,意思是返回键值数据(k、v)。bool 表示遍历流程控制,返回 true 时继续遍历,返回 false 时终止遍历。
- 第 4 行,当 callback 为空时,退出遍历,避免后续代码访问空的 callback 而导致的崩溃。
- 第 8 行,遍历字典结构的 data 成员,也就是遍历 map 的所有元素。
- 第 9 行,根据 callback 的返回值,决定是否继续遍历。
初始化和清除
字典结构包含有 map,需要在创建 Dictionary 实例时初始化 map。这个过程通过 Dictionary 的 Clear() 方法完成。在 NewDictionary 中调用 Clear() 方法避免了 map 初始化过程的代码重复问题。请参考下面的代码:
- // 清空所有的数据
- func (d *Dictionary) Clear() {
- d.data = make(map[interface{}]interface{})
- }
- // 创建一个字典
- func NewDictionary() *Dictionary {
- d := &Dictionary{}
- // 初始化map
- d.Clear()
- return d
- }
代码说明如下:
- 第 3 行,map 没有独立的复位内部元素的操作,需要复位元素时,使用 make 创建新的实例。Go语言的垃圾回收是并行的,不用担心 map 清除的效率问题。
- 第 7 行,实例化一个 Dictionary。
- 第 11 行,在初始化时调用 Clear 进行 map 初始化操作。
使用字典
字典实现完成后,需要经过一个测试过程,查看这个字典是否存在问题。
将一些字符串和数值组合放入到字典中,然后再从字典中根据键查询出对应的值,接着再遍历一个字典中所有的元素。详细实现过程请参考下面的代码:
- func main() {
- // 创建字典实例
- dict := NewDictionary()
- // 添加游戏数据
- dict.Set("My Factory", 60)
- dict.Set("Terra Craft", 36)
- dict.Set("Don't Hungry", 24)
- // 获取值及打印值
- favorite := dict.Get("Terra Craft")
- fmt.Println("favorite:", favorite)
- // 遍历所有的字典元素
- dict.Visit(func(key, value interface{}) bool {
- // 将值转为int类型, 并判断是否大于40
- if value.(int) > 40 {
- // 输出“很贵”
- fmt.Println(key, "is expensive")
- return true
- }
- // 默认都是输出“很便宜”
- fmt.Println(key, "is cheap")
- return true
- })
- }
代码说明如下:
- 第 4 行创建字典的实例。
- 第 7~9 行,将 3 组键值对通过字典的 Set() 方法设置到字典中。
- 第 12 行,根据字符串键查找值,将结果保存在 favorite 中。
- 第 13 行,打印 favorite 的值。
- 第 16 行,遍历字典的所有键值对。遍历的返回数据通过回调提供,key 是键,value 是值。
- 第 19 行,遍历返回的 key 和 value 的类型都是 interface{},这里确认 value 只有 int 类型,所以将 value 转换为 int 类型判断是否大于 40。
- 第 23 和 29 行,继续遍历,返回 true
- 第 23 行,打印键。
运行代码,输出如下:
favorite: 36
My Factory is expensive
Terra Craft is cheap
Don't Hungry is cheap