作者:闪电豹猫 转载注明出处
接口是 Go 语言提供的数据类型之一,它把所有具有共性的方法 (注意与函数区别开) 定义在一起,任何其它类型只要一一实现这些方法的话,我们就称这个类型实现了这个接口。Go 语言的接口与 C++ 的虚函数有共通之处,提高了语言的灵活性的同时也弥补了语言自身的一些不足。
Go 语言的接口与其它面向对象语言的接口不同,Go 的接口只是用来对方法进行一个收束,而正是这个收束,使得 Go 这个面向过程的语言拥有了面向对象的特征。
一般来说,Go 接口的主要功能有:
比如一个完整方法的接口的定义:
// 这是接口,接口内只有方法的定义,没有具体实现 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
。和变量的命名规则一样,接口名的命名也是不能以数字开头、只允许出现一种特殊字符_
,开头大写则包外可见,开头小写则方法在包外不可见等等。
对于接口内的方法名,也是一样的。只有接口名和方法名的首字母都大写,才可以在包外调用这个接口的这个方法。
一个接口只要全部实现了接口中声明的方法,那么就是实现了这个接口。换句话讲,接口就是一个需要具体实现的方法的列表。
下面给出一个示例代码
// 定义接口 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() }
这里不推荐没有把接口里的方法全部实现的做法。
一个空接口 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]]
接口 (包括空接口) 可以存储所有的值,那么自然会涉及到类型转换这个话题。我们将分两部分来讨论接口类型转换,分别是以结构体实现的接口和以指针实现的接口。
这里挖个坑,以后再填
这里挖个坑,以后再填
在 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
与变量的类型比较。
接口是个抽象数据类型,不要为了写接口而写接口,有些不需要接口的地方硬是搞成接口模式,只会带来不必要的损耗。
希望这篇文章能对你在学习接口的过程中有所帮助。码字不易,转载请注明出处。