Go part 6 接口,接口排序,接口嵌套组合,接口与类型转换,接口断言
接口
接口是一种协议,比如一个汽车的协议,就应该有 “行驶”,“按喇叭”,“开远光” 等功能(方法),这就是实现汽车的协议规范,完成了汽车的协议规范,就实现了汽车的接口,然后使用接口
接口的定义:本身是调用方和实现方均需要遵守的一种协议,大家按照统一的方法命名参数类型和数量来协调逻辑处理的过程
Go 语言中的接口是双方约定的一种合作协议;接口实现者不需要关心接口会被如何使用,调用者也不需要关心接口的实现细节。接口是一种类型,也是一种抽象结构,不会暴露所含数据的格式、类型及结构
Go 语言中接口的设计是非侵入式的,接口编写者无须知道接口被哪些类型实现,而接口实现者只需知道实现的是什么样子的接口,无须指明是哪一个接口。编译器知道最终编译时使用哪个类型实现哪个接口,或者接口应该由谁来实现
非侵入式设计是 Go 语言设计师经过多年的大项目经验总结出来的设计之道。只有让接口和实现者真正解耦,编译速度才能真正提高,项目之间的耦合度也会降低
接口声明的格式
接口类型名:在命名时,一般会在单词的后面添加 er,例如写操作的接口叫 Writer,关闭功能的接口叫 Closer
方法名:当方法名首字母是大写,且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包之外的代码访问
参数列表、返回值列表:
type 接口类型名 interface{ 方法名1(参数列表) 返回值列表 方法名2(参数列表) 返回值列表 ... }
实现接口
接口定义后,需要实现接口,调用方才能正确编译通过并使用接口
接口的实现需要遵循两条规则才能使接口可用
1)接口的方法与实现接口的类型方法格式一致
在类型中添加与接口签名一致的方法就可以实现该方法。签名包括方法中的名称、参数列表、返回参数列表。也就是说,只要实现接口类型中的方法的名称、参数列表、返回参数列表中的任意一项与接口要实现的方法不一致,那么接口的这个方法就不会被实现
模拟数据写入的 Demo:
// 实现一个写入器接口 type Writer interface{ WriteData(data interface{}) error } // 文件类型结构体 type file struct {} // 实现 Writer 接口的 WriteData 方法 func (f *file) WriteData(data interface{}) error { // 模拟写入数据 fmt.Println(data) return nil } func main(){ // 实例化 file var f *file = new(file) // 声明 Writer 接口 var W Writer // 将文件类型结构体赋值给接口 W = f // 使用接口调用数据写入 W.WriteData("hello, world~") } 运行结果: hello, world~
2)接口中所有方法均被实现
当一个接口中有多个方法时,只有这些方法都被实现了,接口才能正确编译并使用
类型与接口的关系
类型与接口之间有一对多和多对一的关系
1)一个类型实现多个接口
把 Socket 能够写入数据和需要关闭的特性使用接口来描述,Demo:
// 实现一个写入接口 type Writer interface{ Write(b []byte)(n int, err error) } // 实现一个关闭接口 type Closer interface{ Close() (err error) } // 套接字结构体 type Socket struct {} // 实现 Writer 接口的 Write 方法 func (s *Socket) Write(b []byte) (n int, err error) { fmt.Println("write data") return 0, nil } // 实现 Closer 接口的 Close 方法 func (s *Socket) Close() (err error) { fmt.Println("closer socket") return nil } func main(){ // 实例化 file var s *Socket = new(Socket) // 声明 Writer 和 Closer 接口 var W Writer var C Closer // 将文件类型结构体赋值给接口 W = s C = s // 使用接口调用数据写入 W.Write(make([]byte, 0)) C.Close() } 运行结果: write data closer socket
2)多个类型对应一个接口(这里多个类型本质上指的还是一个类型)
Service 接口定义了两个方法:一个开启服务的方法 Start(),一个是输出日志的方法 Log(),使用 GameService 结构体来实现 Service 接口;GameService 自己的结构只能实现 Start(),而 Service 接口中的 Log() 已经被一个输出日志的 Logging 实现了, 无须再进行 GameService 再重新实现一遍,所以,选择将 Logging 嵌入到 GameService 能最大程度的避免冗余代码,详细实现过程如下:
// 实现一个服务接口 type Service interface{ Start(args string)(err error) Log(args string)(err error) } // 日志器结构体 type Logging struct{} // 日志记录方法 func (l *Logging) Log(info string) (err error){ fmt.Println(info) return nil } // 游戏服务结构体,内嵌日志器 type GameService struct{ Logging } // 游戏服务开启方法 func (gs *GameService) Start(args string) (err error){ fmt.Println("game start", args) return nil } func main(){ // 实例化 游戏服务结构体,并将实例赋值给 Service var s Service = new(GameService) // 使用接口调用服务启动,日志记录 s.Start("come on") s.Log("this is a log info") } 运行结果: game start come on this is a log info
错误示例:
如果游戏服务结构体单独实现 Start() 方法,日志器单独实现 Log() 方法,这样并没有实现接口的所有的方法
// 实现一个服务接口 type Service interface{ Start(args string)(err error) Log(args string)(err error) } // 日志器结构体 type Logging struct{} // 日志记录方法 func (l *Logging) Log(info string) (err error){ fmt.Println(info) return nil } // 游戏服务结构体,内嵌日志器 type GameService struct{} // 游戏服务开启方法 func (gs *GameService) Start(args string) (err error){ fmt.Println("game start", args) return nil } func main(){ // 实例化 游戏服务结构体,并将实例赋值给 Service var s Service = new(GameService) // 使用接口调用服务启动,日志记录 s.Start("come on") s.Log("this is a log info") } 运行结果: ./main_04.go:31:9: cannot use new(GameService) (type *GameService) as type Service in assignment: *GameService does not implement Service (missing Log method)
接口排序
使用 sort.Interface 接口实现排序
在排序时,使用 sort.Interface 提供数据的一些特性和操作方法,这个接口定义代码如下:
type Interface interface { // 获取元素数量 Len() int // 小于比较 Less(i, j int) bool // 交换元素 Swap(i, j int) }
这个接口需要实现者实现三个方法:Len(),Less(),Swap()
对一系列字符串进行排序时,把字符串放入切片,使用 type 关键字,定义为自定义的类型,为了让 sort 包能够识别自定义类型,就必须让自定义类型实现 sort.Interface 接口
package main import ( "fmt" "sort" ) // 将[]string定义为MyStringList类型 type MyStringList []string func (m MyStringList) Len() int { return len(m) } func (m MyStringList) Less(i, j int) bool { return m[i] < m[j] } func (m MyStringList) Swap(i, j int) { m[i], m[j] = m[j], m[i] } func main() { // 准备一个内容被打乱顺序的字符串切片 var names MyStringList = MyStringList{ "3 Triple Kill", "5 Penta Kill", "2 Double Kill", "4 Quadra Kill", "1 First Blood", } // 使用sort包进行排序(Sort 接收一个Interface类型,MyStringList会被赋值给 Interface 类型) sort.Sort(names) // 遍历打印结果 for _, v := range names { fmt.Printf("%s\n", v) } } 运行结果: 1 First Blood 2 Double Kill 3 Triple Kill 4 Quadra Kill 5 Penta Kill
常见类型的便捷排序
通过 sort.Interface 接口的排序过程具有很强的可定制性
1)字符串切片的便捷排序(与在上面用自定义类型实现的逻辑一样)
sort 包中有一个 StringSlice 类型,定义如下:
type StringSlice []string func (p StringSlice) Len() int { return len(p) } func (p StringSlice) Less(i, j int) bool { return p[i] < p[j] } func (p StringSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } // Sort is a convenience method. func (p StringSlice) Sort() { Sort(p) }
使用 sort 包下的 StringSlice 类型就可以对字符串切片进行排序,简化上面的步骤:
names := sort.StringSlice{ "3 Triple Kill", "5 Penta Kill", "2 Double Kill", "4 Quadra Kill", "1 First Blood", } sort.Sort(names)
字符串排序简化版:sort.Strings(names)
2)sort 包下其它的内建排序接口
类 型 | 实现 sort.lnterface 的类型 | 直接排序方法 | 说 明 |
---|---|---|---|
字符串(String) | StringSlice | sort.Strings(a [] string) | 字符 ASCII 值升序 |
整型(int) | IntSlice | sort.Ints(a []int) | 数值升序 |
双精度浮点(float64) | Float64Slice | sort.Float64s(a []float64) | 数值升序 |
编程中经常用到的 int32、int64、float32、bool 类型没有由 sort 实现,需要开发者自己编写
对结构体数据进行排序
除了基本类型,也可以对结构体的字段进行排序,结构体的多个字段在排序中可能存在多种排序规则,如先按分类排序,然后按名称排序
demo:定义英雄结构体,有 Name 和 Kind 字段,排序时要求先按照分类排序,相同分类则按名称排序,实现如下(排序的对象是英雄结构体):
// 定义int常量, 类似于枚举 const ( None int = iota // 0 Tank // 1 Assassin // 2 Mage // 3 ) // Hero 结构体 type Hero struct{ Name string Kind int } // 自定义 Hero 的切片的类型 type Heros []Hero // 实现 sort.Interface 接口方法 func (hs Heros) Len() int { return len(hs) } func (hs Heros) Less(i, j int) bool { // 优先对分类进行排序 if hs[i].Kind != hs[j].Kind { return hs[i].Kind < hs[j].Kind } else { // secondary: Name return hs[i].Name < hs[j].Name } } func (hs Heros) Swap (i,j int) { hs[i], hs[j] = hs[j], hs[i] } func main(){ var heros Heros = Heros{ Hero{"吕布", Tank}, Hero{"李白", Assassin}, Hero{"妲己", Mage}, Hero{"貂蝉", Assassin}, Hero{"关羽", Tank}, Hero{"诸葛亮", Mage}, } fmt.Println("before:") for _, v := range(heros){ fmt.Println(v) } sort.Sort(heros) fmt.Println("\nafter:") for _, v := range(heros){ fmt.Println(v) } } 运行结果: before: {吕布 1} {李白 2} {妲己 3} {貂蝉 2} {关羽 1} {诸葛亮 3} after: {关羽 1} {吕布 1} {李白 2} {貂蝉 2} {妲己 3} {诸葛亮 3}
接口嵌套组合
Go 语言中,不仅结构体之间可以嵌套,接口之间也可以通过嵌套组合形成新的接口
1)io 包中的接口嵌套组合
io 包中定义了写入接口(Writer)、关闭接口(Closer)、写入关闭组合接口(WriterCloser),代码如下:
type Writer interface { Write(p []byte) (n int, err error) } type Closer interface { Close() error } type WriteCloser interface { Writer Closer }
2)在代码中使用接口嵌套组合
我们实现一下上面 io 包中的三个接口(一个类型对应多个接口)
type Device struct { Name string } func (d Device) Write(p []byte) (n int, err error) { fmt.Printf("%v call Write methodi\n", d) return 0, nil } func (d Device) Close() error { fmt.Printf("%v call Close method\n", d) return nil } func main(){ var device1 io.Writer = Device{"device1"} device1.Write(make([]byte, 0)) var device2 io.Closer = Device{"device2"} device2.Close() var device3 io.WriteCloser = Device{"device3"} device3.Write(make([]byte, 0)) device3.Close() } 运行结果: {device1} call Write methodi {device2} call Close method {device3} call Write methodi {device3} call Close method
接口与类型之间的转换
使用接口断言 type assertions 将接口转换成另外一个接口,也可以将接口准换为另外的类型,接口的转换在开发中非常常见,使用也非常频繁
空接口
interface{},空接口没有任何方法,所有类型都实现了空接口(所以类型都可以赋值给空接口),下面会详细说
类型判断的格式
t := i.(T) 其中,i 是接口变量,T 是要转换的目标类型,t 是准换后的变量
如果 i 没有实现 T 接口的所有方法,即断言失败,会触发 panic,所以有一种友好的写法
断言失败时,将会把 ok 置为 false, t 置为 T 类型的 0 值,断言成功时,ok 置为 true,t 置为断言后的结果
t, ok := i.(T)
将接口转换成其它接口
一种类型实现了多个接口,就可以在多个接口之间进行切换
demo:鸟 和 猪具有不同的特性,鸟可以飞,可以行走,猪只能行走,现在有飞行动物接口 Flyer 和 行走动物接口 Walker,如果用结构体分别实现鸟和猪,鸟实现 Fly() 和 Walk(),猪实现 Walk(),那么鸟类型实现了飞行动物接口和行走动物接口,猪实现了行走动物接口
下面的demo中,猪类型实现了从空接口转换到行走接口,鸟类型实现了从空接口转换到行走接口,然后转换到飞行接口
//飞行接口 type Flyer interface { Fly() } //行走接口 type Walker interface { Walk() } //猪结构体,实现行走接口 type Pig struct{} func (p Pig) Walk() { fmt.Println("pig walk") } //鸟结构体,实现行走和飞行接口 type Bird struct{} func (b Bird) Walk() { fmt.Println("bird walk") } func (b Bird) Fly() { fmt.Println("bird fly") } func main() { //空接口接收猪类型 var pig interface{} = new(Pig) var bird interface{} = new(Bird) //判断对象类型是否实现行走接口(转换到行走接口) pigWalk, pigWalker := pig.(Walker) birdWalk, birdWalker := bird.(Walker) birdFly, isFlyer := bird.(Flyer) //如果实现行走接口,则调用行走接口方法 if pigWalker { pigWalk.Walk() } if birdWalker { birdWalk.Walk() } if isFlyer { birdFly.Fly() } } 运行结果: pig walk bird walk bird fly
将接口转换为类型
在上面的代码中,可以将普通的指针类型 new(Pig),转换成接口 Walker,那么将 Walker 接口转换成 *Pig 类型也是可以的
var walk Walker = new(Pig) //接口转换为类型 p, ok := walk.(*Pig) if ok {fmt.Printf("%T", p)}
但是如果把普通指针类型 new(*Pig),转换成接口,然后将接口转换成 *Bird,这样会触发 panic: interface conversion: main.Walker is *main.Pig, not *main.Bird
var walk Walker = new(Pig) p := walk.(*Bird) fmt.Printf("%T", p) 运行结果: panic: interface conversion: main.Walker is *main.Pig, not *main.Bird
报错意思是:接口转换类型时,main.Walker 接口的内部保存的是 *main.pig,而不是 *main.bird
因此,接口在转换为类型时,接口内保存的类型指针,必须是要转换的类型指针
空接口类型
空接口是接口类型的特殊形式,空接口没有任何方法,从实现的角度看,任何类型都实现了空接口,因此空接口类型可以保存任何值,也可以从空接口中取出原值
空接口的内部实现保存了对象的类型与指针,使用空接口保存一个数据的过程会比直接用变量保存稍慢,因此在开发中,应在需要的地方使用空接口,而不是所有地方都使用空接口
1)将值保存到空接口(从类型转换成接口)
func main(){ var any interface{} any = 666 any = "hello, world~" fmt.Println(any) } 运行结果: hello, world~
2)从空接口中获取值(从接口转换成类型)
func main(){ var any interface{} any = "hello, world~" var value string = any.(string) fmt.Println(value) } 运行结果: hello, world~
使用空接口实现可以保存任意值的字典(实现 python 中的字典)
空接口可以保存任何类型,这个特性可以方便的用于容器的设计,下面的例子中使用 map 和 insterface{} 实现了 python 中的字典,包含有 设置值、取值、清空的方法
package main import "fmt" //定义key,value 可为任意值的字典结构体 type Dict struct { data map[interface{}]interface{} } //设置值 func (d Dict) Set(key, value interface{}) { d.data[key] = value } //根据键获取值 func (d Dict) Get(key interface{}) interface{} { return d.data[key] } //清空Dict func (d *Dict) Clear(){ d.data = make(map[interface{}]interface{}) } func main(){ //字典结构包含有 map,需要在创建 Dictionary 实例时初始化 map var dict Dict = Dict{} dict.data = make(map[interface{}]interface{}) //var dict Dict = Dict{map[interface{}]interface{}{}} //可以写成这种 dict.Set("name", "johny") dict.Set("age", 12) dict.Set(666, 666) // 根据键获取值(这里拿到的是 interface{},需要根据空接口中的值类型进行断言取值,不好用) fmt.Println(dict.Get("name").(string)) fmt.Println(dict.Get("age").(int)) fmt.Println(dict.Get(666).(int)) // 清空字典 dict.Clear() fmt.Println(dict) } 运行结果: johny 12 666 {map[]}
问题:在空接口转换成类型的时候,需要进行类型的断言,如果你不知道空接口中的类型,则需要做判断,有点麻烦
接口类型断言
在从接口转换成类型的时候,往往会不清楚要转换的目标类型是什么,所以需要判断空接口中的类型,if 的语句代码太繁杂,这里使用 switch 实现
1)类型断言 switch 格式(接口转换成类型)
package main import "fmt" func assertions(element interface{}) { switch element.(type){ case int: fmt.Println(element.(int)) case string: fmt.Println(element.(string)) case float64: fmt.Println(element.(float64)) default: fmt.Println("unsupported types") } } func main(){ assertions("666") assertions("hello, world") assertions(true) } 运行结果: 666 hello, world unsupported types
2)接口断言 switch 格式(接口转换成接口)
多个接口进行断言时,也可以使用 switch 分支简化判断过程
demo:现在移动支付逐渐成为人们普遍使用的支付方式,移动支付可以使用 faceID,而现金支付容易被偷(Stolen),使用 switch 接口断言可以方便判断是哪种支付接口,进行方法调用
现有两个支付接口 CantainCanUseFaceID 和 ContainStolen,分别实现了 UseFaceID() 和 Stolen() 方法,在支付函数 Payment() 中进行接口断言,然后调用相应的方法
package main import "fmt" // 移动支付接口 type CantainCanUseFaceID interface { UseFaceID() } //现金支付接口 type ContainStolen interface { Stolen() } //alipay 结构体,实现移动支付接口 type Alipay struct {} func (a *Alipay) UseFaceID() { fmt.Println("alipay payment") } //现金支付结构体,实现现金支付接口 type Cash struct {} func (c *Cash) Stolen() { fmt.Println("cash payment") } func Payment(patternPayment interface{}) { switch patternPayment.(type) { // 可以使用移动支付 case CantainCanUseFaceID: faceIDPayment := patternPayment.(CantainCanUseFaceID) faceIDPayment.UseFaceID() // 可以使用现金支付 case ContainStolen: cashPayment := patternPayment.(ContainStolen) cashPayment.Stolen() } } func main() { //使用 alipay 支付 Payment(new(Alipay)) //使用现金支付 Payment(new(Cash)) } 运行结果: alipay payment cash payment
end ~