Go语言中的接口作用类似于C++中的虚函数机制,可以提供一个统一调用的方式。
接口是双方约定的一种合作协议。接口实现者不需要关心接口会被怎样使用,调用者也不需要关心接口的实现细节。接口是一种类型,也是一种抽象结构,不会暴露所包含数据的格式、类型及结构。
1.接口的定义
每个接口类型由多个方法组成。
type 接口类型名 interface {
方法名1(参数列表1) 返回值列表
方法名2(参数列表2) 返回值列表
......
}
一般地:
接口类型名:Go语言接口在命名时,一般会在单词后面添加er,如有字符串功能的接口叫Stringer
方法名:当方法名首字母大写时,且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包之外的代码访问
参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以被忽略
2.接口实现的条件
(1)接口的方法与实现接口的类型方法格式一致
在类型中添加与接口签名一致的方法就可以实现该方法。签名包括该方法中方法名、参数列表和返回参数列表。也就是说,只要实现接口类型中的方法名称、参数列表、返回参数列表中的任意一项与接口要实现的方法不一致,那么接口的这个方法就不会被实现。
package main
import (
"fmt"
)
// 定义一个数据写入器
type DataWriter interface {
WriteData(data interface{}) error
}
// 定义文件结构,用于实现DataWriter
type file struct {
}
// 实现DataWriter接口的WriteData方法
func (d *file) WriteData(data interface{}) error {
// 模拟写入数据
fmt.Println("WriteData:", data)
return nil
}
func main() {
// 实例化file
f := new(file)
// 声明一个DataWriter的接口
var writer DataWriter
// 将接口赋值f,也就是*file类型
writer = f //在这里会检查*file类型是否实现了DataWriter的接口函数
// 使用DataWriter接口进行数据写入
writer.WriteData("data")
}
(2)接口中所有方法均被实现
当一个接口中有多个方法时,只有这些方法都被实现了,接口才能被正确编译并使用。
package main
import (
"fmt"
)
type DataWriter interface {
WriteData(data interface{}) error
foo() //新增接口函数,但是没有实现
}
type file struct {
}
func (d *file) WriteData(data interface{}) error {
fmt.Println("WriteData:", data)
return nil
}
func main() {
f := new(file)
var writer DataWriter
writer = f
writer.WriteData("data")
}
上面代码运行时,会提示如下错误:
: cannot use f (type *file) as type DataWriter in assignment:
*file does not implement DataWriter (missing foo method)
3.接口的嵌套组合
在Go语言中,不仅结构体与结构体之间可以嵌套,接口与接口间也可以通过嵌套创造出新的接口。
接口与接口嵌套组合而成了新接口,只要接口的所有方法被实现,则这个接口中的所有嵌套接口的方法可以被调用。
package main
import (
"io"
)
// 声明一个设备结构
type device struct {
}
// 实现io.Writer的Write()方法
func (d *device) Write(p []byte) (n int, err error) {
return 0, nil
}
// 实现io.Closer的Close()方法
func (d *device) Close() error {
return nil
}
func main() {
// 声明写入关闭器, 并赋予device的实例
var wc io.WriteCloser = new(device)
// 写入数据
wc.Write(nil)
// 关闭设备
wc.Close()
// 声明写入器, 并赋予device的新实例
var writeOnly io.Writer = new(device)
// 写入数据
writeOnly.Write(nil)
}
4.接口和类型之间的转换
Go语言中使用接口断言将接口转换成另外一个接口,也可以将接口转换为另外的类型。接口的类型在开发中非常常见,使用也非常频繁。
(1)类型断言的格式
t := i.(T)
i为接口变量,T为转换的目标类型,t代表转换后的变量
如果i没有完全实现T接口的方法这个语句将会触发宕机。触发宕机不是很友好,所以有另一种写法:
t, ok := i.(T)
可以通过判断ok的值查看接口是否实现。
(2)将接口转换为其他接口
package main
import "fmt"
// 定义飞行动物接口
type Flyer interface {
Fly()
}
// 定义行走动物接口
type Walker interface {
Walk()
}
// 定义鸟类
type bird struct {
}
// 实现飞行动物接口
func (b *bird) Fly() {
fmt.Println("bird: fly")
}
// 为鸟添加Walk()方法, 实现行走动物接口
func (b *bird) Walk() {
fmt.Println("bird: walk")
}
// 定义猪
type pig struct {
}
// 为猪添加Walk()方法, 实现行走动物接口
func (p *pig) Walk() {
fmt.Println("pig: walk")
}
func main() {
// 创建动物的名字到实例的映射
animals := map[string]interface{}{
"bird": new(bird),
"pig": new(pig),
}
// 遍历映射
for name, obj := range animals {
// 判断对象是否为飞行动物
f, isFlyer := obj.(Flyer)
// 判断对象是否为行走动物
w, isWalker := obj.(Walker)
fmt.Printf("name: %s isFlyer: %v isWalker: %v\n", name, isFlyer, isWalker)
// 如果是飞行动物则调用飞行动物接口
if isFlyer {
f.Fly()
}
// 如果是行走动物则调用行走动物接口
if isWalker {
w.Walk()
}
}
}
(3)将接口转换为其他类型
接口在转换在转换为其他类型时,接口内保存的实例对应的类型指针,必须是要准换的对应的类型指针。
p1 := new(pig)
var a Walker = p1
p2 := a.(*pig) //正确
p2 := a.(*bird) //错误
5.空接口类型
空接口是接口类型的特殊形式,空接口没有任何方法,因此任何类型都无需实现空接口。从实现的角度看,任何值都满足这个接口的需求。因此空接口类型可以保存任何值,也可以从空接口中取出原值。
空接口的内部实现保存了对象的类型和指针。使用空接口保存以数据的过程会比直接用数据对应类型的变量保存稍慢。因此在开发中,应在需要的地方使用空接口,而不是在所有地方使用空接口。
空接口类型类似于C语言中的void*。
(1)将值保存到空接口
var any interface{}
any = 1 //将整型保存到空接口
fmt.Println(any)
any = "hello" //将字符串保存到空接口
fmt.Println(any)
any = false //将bool型保存到空接口
fmt.Println(any)
(2)从空接口获取值
保存到空接口的值,如果直接取出指定类型的值时,会发生编译错误:
var a int = 1
var i interface{} = a
//错误
var b int = i
//正确
b, ok := i.(int)
if ok {
fmt.Println(b)
}
(3)空接口的值比较
a).类型不同的空接口间比较
var a interface{} = 12
var b interface{} = "hello,hust"
fmt.Println(a == b) //false
b).不能比较空接口中的动态值
当接口中保存有动态类型的值时,运行时将触发错误。
类型的可比较性如下:
类型 | 说明 |
map | 宕机错误,不可比较 |
切片([]T) | 宕机错误,不可比较 |
通道 | 可比较,必须由同一个make生成,也就是同一个通道才会为true,否则为false |
数组 | 可比较,编译器知道两个数组是否一致 |
结构体 | 可比较,可以逐个比较结构体的值 |
函数 | 可比较 |
var a interface{} = []int{12}
var b interface{} = []int{12}
fmt.Println(a == b)
panic: runtime error: comparing uncomparable type []int
6.类型分支
Go语言的switch不仅可以像其他语言一样实现数值、字符串的判断,还有一种特殊的用途----判断一个接口内保存或实现的类型。
(1)类型断言的书写格式
switch 接口变量.(type) {
case 类型1:
//变量是类型1时的处理
case 类型2:
//变量是类型2时的处理
...
default:
//变量不是所有case类型时的处理
}
(2)使用类型分支判断基本类型
package main
import (
"fmt"
)
func printType(v interface{}) {
switch v.(type) {
case int:
fmt.Println(v, " is int")
case string:
fmt.Println(v, " is string")
case bool:
fmt.Println(v, " is bool")
default:
fmt.Println(v, " undefined type")
case []int:
fmt.Println(v, " is int slice")
}
}
func main() {
printType(1)
printType("hello")
printType(false)
printType([]int{1, 2})
}
//运行结果:
1 is int
hello is string
false is bool
[1 2] is int slice
(3)使用类型分支判断接口类型
多个接口进行类型断言时,可以使用类型分支简化判断过程。
package main
import "fmt"
type CantainCanUseFaceID interface {
CanUseFaceID()
}
type CantainStolen interface {
Stolen()
}
type Alipay struct {
}
func (a *Alipay) CanUseFaceID() {
}
type Cash struct {
}
func (c *Cash) Stolen() {
}
func print(payMethod interface{}) {
switch payMethod.(type) {
case CantainCanUseFaceID:
fmt.Printf("%T can use faceid\n", payMethod)
case CantainStolen:
fmt.Printf("%T may be stolen\n", payMethod)
}
}
func main() {
print(new(Alipay))
print(new(Cash))
}
//运行结果:
*main.Alipay can use faceid
*main.Cash may be stolen