Golang 接口 (interface) 用法介绍
Golang interface 用法
作者:闪电豹猫 转载注明出处
1. 接口 (interface) 介绍
接口是 Go 语言提供的数据类型之一,它把所有具有共性的方法 (注意与函数区别开) 定义在一起,任何其它类型只要一一实现这些方法的话,我们就称这个类型实现了这个接口。Go 语言的接口与 C++ 的虚函数有共通之处,提高了语言的灵活性的同时也弥补了语言自身的一些不足。
Go 语言的接口与其它面向对象语言的接口不同,Go 的接口只是用来对方法进行一个收束,而正是这个收束,使得 Go 这个面向过程的语言拥有了面向对象的特征。
一般来说,Go 接口的主要功能有:
- 作为方法的收束器,进行具有 “面向对象程序设计特色” 的程序设计。
- 作为各种数据的承载者,可以用来为函数接收各类不同数量的函数参数,这也是 Go 提倡的接口编程。
2. 接口的定义和使用
2.1 定义
比如一个完整方法的接口的定义:
// 这是接口,接口内只有方法的定义,没有具体实现
type 接口类型名 interface {
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2
...
}
// 定义结构体
type 结构体名 struct {
变量名1 类型1
变量名2 类型2
...
}
// 实现接口方法
func ( 结构体变量1 结构体名 ) 方法名1( 参数列表1 ) 返回值列表1 {
//方法实现
}
func ( 结构体变量2 结构体名 ) 方法名2( 参数列表2 ) 返回值列表2 {
//方法实现
}
func ( 结构体变量n 结构体名 ) 方法名n( 参数列表n ) 返回值列表n {
//方法实现
}
在实践中,我们一般将接口命名为 “什么什么er”,比如写操作的接口可以叫Writer
,读取字符串的接口可以叫做StringReader
。和变量的命名规则一样,接口名的命名也是不能以数字开头、只允许出现一种特殊字符_
,开头大写则包外可见,开头小写则方法在包外不可见等等。
对于接口内的方法名,也是一样的。只有接口名和方法名的首字母都大写,才可以在包外调用这个接口的这个方法。
2.2 使用
一个接口只要全部实现了接口中声明的方法,那么就是实现了这个接口。换句话讲,接口就是一个需要具体实现的方法的列表。
下面给出一个示例代码
// 定义接口
type Canteen interface {
MakeRice()
MakeNoodles()
}
// 定义结构体
type ZhuYuan struct {}
type HaiTang struct {}
type DingXiang struct {}
// 挨个实现接口里所声明的方法
func (a ZhuYuan) MakeRice() {
fmt.Println("竹园餐厅的米饭")
}
func (a ZhuYuan) MakeNoodles() {
fmt.Println("竹园餐厅的面条")
}
func (a HaiTang) MakeRice() {
fmt.Println("海棠餐厅的米饭")
}
func (a HaiTang) MakeNoodles() {
fmt.Println("海棠餐厅的面条")
}
func (pa *DingXIang) MakeRice() {
fmt.Println("丁香餐厅的米饭")
}
func (pa *DingXiang) MakeNoodles() {
fmt.Println("丁香餐厅的面条")
}
该示例中,我们将竹园和海棠用结构体对象实现,而丁香是用指向结构体的指针实现的。这样,我们在接口实例化时:
var a Canteen = ZhuYuan{} // 接受结构体,且传入的也是结构体,可以通过编译
var b Canteen = &ZhuYaun{} // 接受结构体,传入的是指针,可以通过编译,这很重要
var c Canteen = DingXiang{} // 接受指针,传入的却是结构体,编译当然会失败
var d Canteen = &DingXiang{} // 接受指针,传入的也是指针,可以通过编译
记住,Go 中的所有东西都是按值传递的。每次调用函数时,传入的数据都会被复制。对于具有值接收者的方法,在调用该方法时将复制该值。对于上面四行代码中的 a
和 d
,没啥好解释的,接受啥类型就给它啥类型嘛。对于 b
,编译器会对指针 &ZhuYuan{}
进行拷贝,相关方法调用时,会对拷贝后的指针进行隐式解引用获取指针指向的结构体。这也就能解释为啥上面的var b Canteen = &ZhuYuan{}
为什么能通过编译。打个比方,只给你一个 int8 类型的值 123 ,你无法知道这个 123 的内存地址;而给你一个指针 *int8 ,你既能获知该 int8 数的内存地址,又能知道该数是多少。
总而言之,当我们用指针实现方法时,只有指针类型的变量才可以实现接口;当我们用结构体实现方法时,结构体类型和指针类型都可以实现接口。不过,在实际开发中,这个性质没那么重要,这里讲开了是为了解释现象背后的原理。
补充
Go 的接口是隐式实现的,也就是说,在接口的定义里的一条条方法只是声明,具体有没有方法的实现,Go 不在乎。
因为是隐式实现的,所以不实现方法也是可以通过编译的,只要程序别遇到需要对未实现的方法进行传参、返参和变量赋值,编译器就不会检查,程序就不会嗝屁。
如下代码完全可以编译运行,控制台输出 12
并退出:
package main
import (
"fmt"
)
type hhher interface {
AAA(int, int)
PrintAge()
CCCC(string, map[int]string) (int, int)
}
type People struct {
Age int
}
func (human People) PrintAge() {
fmt.Println(human.Age)
}
func main() {
fmt.Println("Hello, playground")
alex := &People{Age:12,} // 这里就是接收结构体而传入指针,是可行的
alex.PrintAge()
}
这里不推荐没有把接口里的方法全部实现的做法。
2.3 数据承担者
一个空接口 interface{}
什么方法 (method) 也没有实现,是一个能装入任意数量、任意数据类型的数据容器。
为什么这样说呢?是这样的。空接口 interface{}
也是接口,不过是没有实现方法的接口罢了。回顾接口的定义:接口是一组方法的集合,是一种数据类型,任何其他类型如果能实现接口内所有的方法的话,我们称那个数据类型实现了这个接口。咱们再来看空接口:里面连一个方法也没有,不也就是任意数据类型都能实现这个接口了嘛。这就和 “空集能被任意集合包含” 一样嘛,空接口能被任意数据类型实现。
与 C 语言的 void *
可以转换成任意其它类型的指针 (如 char *
、int *
等) 不同的是,Go 语言的 interface{}
不是一个任意数据类型,interface{}
的类型就是 interface{}
类型,不能转换成其他接口类型,更不能转换成其他什么类型 (比如[]int
、string
等等) ,只不过是 interface{}
能装入任意数据罢了。
把其它类型的变量转换成interface{}
类型后,在程序运行时 (runtime) 内,该变量的数据类型将会发生变化,但是如果这时候要求获取该变量的数据类型,我们会得到interface{}
类型。这是为啥子呢?
在 Golang 的源代码中,用runtime.iface
表示非空接口类型,用runtime.eface
表示空接口类型interface{}
。虽然它们俩都用一个interface
声明,但是后者在 Golang 的源代码更加常见,所以在实现interface{}
时,使用了特殊的类型。具体的你得看 Golang 源代码和 Go 手册了。
- 用空接口可以让函数和方法接受任意类型、任意数量的函数参数:
func show(a interface{}) {
fmt.Printf("a的类型是%T,a的值是%v\n", a, a)
}
空接口切片还可以用于函数的可选参数,比如:
func main() {
kkk(234, "qwerty", [5]int64{1,2,4}, false, nil)
kkk(236)
}
func kkk(key int, a ...interface{}) {
// 必选参数是一个 int 类型,可选参数用空接口切片表示
// 其类型为 []interface{},这个 a 是可以下表访问的,而每个 a 的元素都是个空接口
if key == 234 {
fmt.Println((a[1])) // 这里需要保证a[1]下标不越界,我这里没有进行判断
switch ttt := a[0].(type) {
case string: fmt.Println("0th element is string interface{}")
default: fmt.Printf("idk wtf is this: %T", ttt)
}
switch ttt := a[1].(type) {
case string: fmt.Println("1st element is string interface{}")
default: fmt.Printf("idk wtf is this: %T\n", ttt)
}
} else {
fmt.Println("key wrong")
}
}
程序输出如下:
[1 2 4 0 0]
0th element is string interface{}
idk wtf is this: [5]int64
key wrong
-
空接口还可以作为函数的返回值,但是极不推荐这样干,因为代码的维护、拓展与重构将会变得极为痛苦。
-
空接口可以实现保存任意类型值的字典 (map):
var alexInfo = make( map[string]interface{} )
alexInfo["name"] = "Alex"
alexInfo["age"] = 12
alexInfo["score"] = [4]int{150, 150, 150, 300}
fmt.Println(alexInfo)
控制台输出
map[age:12 name:Alex score:[150 150 150 300]]
3. 接口类型转换
接口 (包括空接口) 可以存储所有的值,那么自然会涉及到类型转换这个话题。我们将分两部分来讨论接口类型转换,分别是以结构体实现的接口和以指针实现的接口。
3.1 指向结构体的指针实现的接口
这里挖个坑,以后再填
3.2 结构体实现的接口
这里挖个坑,以后再填
3.3 类型断言
在 2.3.1 节中,我们的代码已经用到了类型断言,下面来具体介绍一下类型断言。
如何把一个接口类型转换成具体类型 T ?
x.(T)
- 对于非空接口:
package main
import (
"fmt"
)
type OptionForMeat interface {
Boil(int)
Fry(int)
}
type Meat struct {
Name string
}
func (a Meat) Boil(minute int) {
fmt.Printf("煮了%d分钟的%s了\n", minute, a.Name)
}
func (a Meat) Fry(minute int) {
fmt.Printf("煎了%d分钟的%s了\n", minute, a.Name)
}
func main() {
var aaa OptionForMeat = &Meat{Name:"Porkchop",}
switch aaa.(type) { // 断言
case *Meat:
w := aaa.(*Meat)
w.Boil(5)
//aaa.Fry(6) // 这行会输出 “煎了6分钟的Porkchop了”
}
}
控制台输出:
煮了5分钟的Porkchop了
Go 语言的编译器对这种情况进行了优化,switch 语句生成的汇编代码会将目标类型的Hash
与接口的itab.Hash
进行比较。
- 对于空接口而言
// 只是换一行代码
var aaa interface{} = &Meat{Name:"Porkchop",}
控制台输出是一样的。上述代码在断言时并不是直接获取runtime._type
,而是从eface._type
获取类型值,汇编指令仍会使用目标类型的Hash
与变量的类型比较。
4. 总结
接口是个抽象数据类型,不要为了写接口而写接口,有些不需要接口的地方硬是搞成接口模式,只会带来不必要的损耗。
希望这篇文章能对你在学习接口的过程中有所帮助。码字不易,转载请注明出处。