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)
View Code

 

接口排序

使用 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}
Hero

 

接口嵌套组合

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 ~

  

 

posted @ 2019-06-10 14:16  kaichenkai  阅读(533)  评论(0编辑  收藏  举报