Go语言基础之接口
接口的定义
接口是一种由程序员来定义的类型,一个接口类型就是一组方法的集合,它规定了需要实现的所有方法。接口可以类比c++中的抽象类,接口中的方法可以类比为c++当中的纯虚函数
每个接口类型由任意个方法签名组成,接口的定义格式如下:
type 接口类型名 interface{方法名1( 参数列表1 ) 返回值列表1方法名2( 参数列表2 ) 返回值列表2…
}
其中:
- 接口类型名:Go语言的接口在命名时,一般会在单词后面添加
er
,如有写操作的接口叫Writer
,有关闭操作的接口叫closer
等。接口名最好要能突出该接口的类型含义。 - 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
- 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。
举个例子,定义一个包含Write
方法的Writer
接口。
type Writer interface{Write([]byte) error
}
当你看到一个Writer
接口类型的值时,你不知道它是什么,唯一知道的就是可以通过调用它的Write
方法来做一些事情。
实现接口的条件
接口就是规定了一个需要实现的方法列表,在 Go 语言中一个类型只要实现了接口中规定的所有方法,那么我们就称它实现了这个接口。
我们定义的Singer
接口类型,它包含一个Sing
方法。
// Singer 接口
type Singer interface {Sing()
}
我们有一个Bird
结构体类型如下。
type Bird struct {}
因为Singer
接口只包含一个Sing
方法,所以只需要给Bird
结构体添加一个Sing
方法就可以满足Singer
接口的要求。
// Sing Bird类型的Sing方法
func (b Bird) Sing() {fmt.Println("汪汪汪")
}
这样就称为Bird
实现了Singer
接口。
为什么要使用接口
接口的使用场景类似于c++中的多态,都是实现一种定义多种实现的手段和方式
假设我们的代码世界里有很多小动物,下面的代码片段定义了猫和狗,它们饿了都会叫。
package mainimport "fmt"type Cat struct{}func (c Cat) Say() {fmt.Println("喵喵喵~")
}type Dog struct{}func (d Dog) Say() {fmt.Println("汪汪汪~")
}func main() {c := Cat{}c.Say()d := Dog{}d.Say()
}
这个时候又跑来了一只羊,羊饿了也会发出叫声。
type Sheep struct{}func (s Sheep) Say() {fmt.Println("咩咩咩~")
}
可以观察到,这组函数的函数签名是一样的,唯一不同的是他们的具体实现,据此,我们就可以把他们抽象出来
我们接下来定义一个饿肚子的场景。
// MakeCatHungry 猫饿了会喵喵喵~
func MakeCatHungry(c Cat) {c.Say()
}// MakeSheepHungry 羊饿了会咩咩咩~
func MakeSheepHungry(s Sheep) {s.Say()
}
接下来会有越来越多的小动物跑过来,我们的代码世界该怎么拓展呢?
在饿肚子这个场景下,我们可不可以把所有动物都当成一个“会叫的类型”来处理呢?当然可以!使用接口类型就可以实现这个目标。 我们的代码其实并不关心究竟是什么动物在叫,我们只是在代码中调用它的Say()
方法,这就足够了。
我们可以约定一个Sayer
类型,它必须实现一个Say()
方法,只要饿肚子了,我们就调用Say()
方法。
type Sayer interface {Say()
}
然后我们定义一个通用的MakeHungry
函数,接收Sayer
类型的参数。
// MakeHungry 饿肚子了...
func MakeHungry(s Sayer) {s.Say()
}
我们通过使用接口类型,把所有会叫的动物当成Sayer
类型来处理,只要实现了Say()
方法都能当成Sayer
类型的变量来处理。
var c cat
MakeHungry(c)
var d dog
MakeHungry(d)
那实现了接口又有什么用呢?一个接口类型的变量能够存储所有实现了该接口的类型变量。
例如在上面的示例中,Dog
和Cat
类型均实现了Sayer
接口,此时一个Sayer
类型的变量就能够接收Cat
和Dog
类型的变量。
var x Sayer // 声明一个Sayer类型的变量x
a := Cat{} // 声明一个Cat类型变量a
b := Dog{} // 声明一个Dog类型变量b
x = a // 可以把Cat类型变量直接赋值给x
x.Say() // 喵喵喵
x = b // 可以把Dog类型变量直接赋值给x
x.Say() // 汪汪汪
空接口
空接口是指没有定义任何方法的接口类型。因此任何类型都可以视为实现了空接口。也正是因为空接口类型的这个特性,空接口类型的变量可以存储任意类型的值。
package mainimport "fmt"// 空接口// Any 不包含任何方法的空接口类型
type Any interface{}// Dog 狗结构体
type Dog struct{}func main() {var x Anyx = "你好" // 字符串型fmt.Printf("type:%T value:%v\n", x, x)x = 100 // int型fmt.Printf("type:%T value:%v\n", x, x)x = true // 布尔型fmt.Printf("type:%T value:%v\n", x, x)x = Dog{} // 结构体类型fmt.Printf("type:%T value:%v\n", x, x)
}
空接口的应用
空接口作为函数的参数
使用空接口实现可以接收任意类型的函数参数。
// 空接口作为函数参数
func show(a interface{}) {fmt.Printf("type:%T value:%v\n", a, a)
}
空接口作为map的值
使用空接口实现可以保存任意值的字典。
// 空接口作为map值var studentInfo = make(map[string]interface{})studentInfo["name"] = "沙河娜扎"studentInfo["age"] = 18studentInfo["married"] = falsefmt.Println(studentInfo)
类型断言
接口值可能赋值为任意类型的值,那我们如何从接口值获取其存储的具体数据呢?
想要从接口值中获取到对应的实际值需要使用类型断言,其语法格式如下。
x.(T)
其中:
- x:表示接口类型的变量
- T:表示断言
x
可能是的类型。
该语法返回两个参数,第一个参数是x
转化为T
类型后的变量,第二个值是一个布尔值,若为true
则表示断言成功,为false
则表示断言失败。
举个例子:
func funcName(a interface{}) string {// 判断当前接收到的参数变量的类型是否是stringvalue, ok := a.(string)if !ok {fmt.Println("Type assertion failed")return ""}fmt.Println("Type assertion successful, the value is: ", value)return value
}
如果对一个接口值有多个实际类型需要判断,推荐使用switch
语句来实现。
// justifyType 对传入的空接口类型变量x进行类型断言
func justifyType(x interface{}) {switch v := x.(type) {case string:fmt.Printf("x is a string,value is %v\n", v)case int:fmt.Printf("x is a int is %v\n", v)case bool:fmt.Printf("x is a bool is %v\n", v)default:fmt.Println("unsupport type!")}
}