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 {
//方法实现
}
WriterStringReader_
对于接口内的方法名,也是一样的。只有接口名和方法名的首字母都大写,才可以在包外调用这个接口的这个方法。
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{} // 接受指针,传入的也是指针,可以通过编译
adb&ZhuYuan{}var b Canteen = &ZhuYuan{}
总而言之,当我们用指针实现方法时,只有指针类型的变量才可以实现接口;当我们用结构体实现方法时,结构体类型和指针类型都可以实现接口。不过,在实际开发中,这个性质没那么重要,这里讲开了是为了解释现象背后的原理。
补充
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{}
interface{}
void *char *int *interface{}interface{}interface{}[]intstringinterface{}
interface{}interface{}
runtime.ifaceruntime.efaceinterface{}interfaceinterface{}
- 用空接口可以让函数和方法接受任意类型、任意数量的函数参数:
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了
Hashitab.Hash
- 对于空接口而言
// 只是换一行代码
var aaa interface{} = &Meat{Name:"Porkchop",}
runtime._typeeface._typeHash
4. 总结
接口是个抽象数据类型,不要为了写接口而写接口,有些不需要接口的地方硬是搞成接口模式,只会带来不必要的损耗。