• 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
    ", 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("
    after:")
        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
    ", d)
        return 0, nil
    }
    
    func (d Device) Close() error {
        fmt.Printf("%v call Close method
    ", 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 ~

      

    每天都要遇到更好的自己.
  • 相关阅读:
    如何将自己的镜像上传到私库
    基于spring-cloud的微服务(1) 服务注册中心eureka
    关于对象池技术的一些记录
    为Docker容器中运行的gitlab添加ssh的一些问题记录
    使用java实现的socket代理(支持socket4和socket5)
    ConfluenceRemoteUserAuth
    JiraRemoteUserAuth
    Apache 的mod_auth_cas模块的介绍和使用
    基于乌班图的标准镜像添加中文支持
    apache反向代解决绝对路径可能出现的问题
  • 原文地址:https://www.cnblogs.com/kaichenkai/p/10997414.html
Copyright © 2020-2023  润新知