zoukankan      html  css  js  c++  java
  • Golang中正确使用接口的方式?

    Golang中接口的作用:

    1. 可以作为函数和方法的参数或者返回值的使用,可以通过类型断言和switch方法

    2.多态的使用,在程序设计中,抽象出某些对象共同拥有的方法,多种类型实现同一接口,通过接口变量指向具体对象操作这些方法。

    Golang接口的使用

    interface 是方法或行为声明的集合,其中所有的方法都没有方法体,接口中也不能有其他变量

    interface接口方式实现比较隐性,任何类型的对象必须实现interface所包含的全部方法,则表明该类型实现了该接口。

    interface还可以作为一中通用的类型,其他类型变量可以给interface声明的变量赋值。

    interface 可以作为一种数据类型,实现了该接口的任何对象都可以给对应的接口类型变量赋值。

    一个自定义的类型可以实现多个接口,可以理解为多继承可以使用多个方法,一个接口(比如A接口)可以继承多个别的接口(比如B,C接口),这时如果要实现A接口,也必须将B,C接口的方法也全部实现

    具体使用

    一个接口本身不能创建实例,但是可以指向一个实现该接口的自定义类型的实例,自定义类型既可以是函数也可以是结构体,如果自定义的类型没有实现接口,就不可以将结构体赋给接口

    自定义类型为结构体

    package main
    import (
        "fmt"
    )
    // 结构体类型
    type Struct struct {
    }
    // 实现Invoker的Call
    func (s *Struct) Call(p interface{}) {
        fmt.Println("from struct", p)
    }
    // 调用器接口
    type Invoker interface {
        // 需要实现一个Call()方法
        Call(interface{})
    }
    func main() {
        // 声明接口变量
        var invoker Invoker
        // 实例化结构体
        // s := new(Struct)
        var s Struct
        // 将实例化的结构体赋值到接口
        invoker = &s
        // 使用接口调用实例化结构体的方法Struct.Call
        invoker.Call("hello")
    }

     自定义类型为函数

    package main
    import (
        "fmt"
    )
    // 函数定义为类型
    type FuncCaller func(interface{})
    // 实现Invoker的Call
    func (f FuncCaller) Call(p interface{}) {
        // 调用f()函数本体
        f(p)
    }
    // 调用器接口
    type Invoker interface {
        // 需要实现一个Call()方法
        Call(interface{})
    }
    func main() {
        // 声明接口变量
        var invoker Invoker
        // 将匿名函数转为FuncCaller类型, 再赋值给接口
        invoker = FuncCaller(func(v interface{}) {
            fmt.Println("from function", v)
        })
        // 使用接口调用FuncCaller.Call, 内部会调用函数本体
        invoker.Call("hello")
    }

     指向接口的指针

    如果希望接口方法修改基础数据,则必须使用指针传递(将对象指针赋值给接口变量)。

    type Father interface {
        f()
    }
    type Son1 struct {
        Name string
    }
    func (s Son1) f() {
        s.Name = "C"
    }
    type Son2 struct {
        Name string
    }
    func (s *Son2) f() {
        s.Name = "C"
    }
    func main() {
        var son1 Father = Son1{Name: "A"}
        fmt.Println("son1 before name:", son1)
        son1.f()
        fmt.Println("son1 after name:", son1)
        var son2 Father = &Son2{Name: "B"}
        fmt.Println("son2 before name:", son2)
        son2.f()
        fmt.Println("son2 after name:", son2)
    }
    console:
    son1 before name: {A}
    son1 after name: {A} 
    son2 before name: &{B}
    son2 after name: &{C} //传递指针才修改成功

    接口的合理性验证

    • 将实现特定接口的导出类型作为接口API 的一部分进行检查
    • 实现同一接口的(导出和非导出)类型属于实现类型的集合
    • 任何违反接口合理性检查的场景,都会终止编译, 并通知给用户

    错误使用接口会在编译期报错. 所以可以利用这个机制让部分问题在编译期暴露。在实际的项目中可能会导致代码冗余。

    type temp interface {
        f()
    }
    type Father struct {
    }
    // 实现 eat() 方法
    func (father Father) f() {
        fmt.Println("father transfer f()")
    }
    type Son struct {
        // Father
    }
    var _ temp = Son{} func main() {}

    这段代码在编译时不会报错,但是运行时会报错(var _ temp = Son{}可以帮助我们在编译时发现错误),Son没有实现eat方法,如果去掉Father的注释可以正常运行,而且可以调用f方法

    在实际使用中,还需根据继承的具体类型判断,赋值的右边应该是断言类型的零值。对于指针类型(如*Handler)、切片和映射,这是nil;对于结构类型,这是空结构。

    Good代码

    type Handler struct {
      // ...
    }
    var _ http.Handler = (*Handler)(nil)
    type LogHandler struct {
      h   http.Handler
      log *zap.Logger
    }
    
    var _ http.Handler = LogHandler{}

    接收器 (receiver) 与接口,接口与具体方法集的匹配

    一个类型可以有值接收器方法集和指针接收器方法集

    使用值接收器的方法既可以通过值调用,也可以通过指针调用。带指针接收器的方法只能通过指针或 addressable values调用(其实和值调用类似)。

    如果方法的接收者是值类型,无论调用者是对象还是对象指针,修改的都是对象的副本,不影响调用者;如果方法的接收者是指针类型,则调用者修改的是指针指向的对象本身。

    通常我们使用指针作为方法的接收者的理由:

    • 使用指针方法能够修改接收者指向的值。
    • 可以避免在每次调用方法时复制该值,在值的类型为大型结构体时,这样做会更加高效。
    type S struct {
      data string
    }
    
    func (s S) Read() string {
      return s.data
    }
    
    func (s *S) Write(str string) {
      s.data = str
    }
    
    sVals := map[int]S{1: {"A"}}
    
    // 你只能通过值调用 Read
    sVals[1].Read()
    
    // 这不能编译通过:
    //  sVals[1].Write("test")
    
    sPtrs := map[int]*S{1: {"A"}}
    
    // 通过指针既可以调用 Read,也可以调用 Write 方法
    sPtrs[1].Read()
    sPtrs[1].Write("test")

    map中的值是无法取地址的,因为它不是一个变量。这里的值对象仅仅指一些拿不到地址的情况。

    sVals := S{"A"}
    sVals.Write("test")

    这种情况编译是可以通过的,这就是通过addressable values调用的

    接口的匹配

    • 值方法集和接口匹配
      • 给接口变量赋值的不管是值还是指针对象,都ok,因为都包含值方法集
    • 指针方法集和接口匹配
      • 只能将指针对象赋值给接口变量,因为只有指针方法集和接口匹配
      • 如果将值对象赋值给接口变量,会在编译期报错(会触发接口合理性检查机制)
    type F interface {
      f()
    }
    
    type S1 struct{}
    
    func (s S1) f() {}
    
    type S2 struct{}
    
    func (s *S2) f() {}
    
    s1Val := S1{}
    s1Ptr := &S1{}
    s2Val := S2{}
    s2Ptr := &S2{}
    
    var i F
    i = s1Val
    i = s1Ptr
    i = s2Ptr
    
    //  下面代码无法通过编译。因为 s2Val 是一个值,而 S2 的 f 方法中没有使用值接收器,值方法集与接口不匹配
    //   i = s2Val

    关于interface关键字

    interface有两种表现形式,空接口eface、非空接口iface

    其底层都是由不同的struct表示

    eface底层结构

    type eface struct {       // 16 字节
        _type *_type          //类型信息
        data  unsafe.Pointer  //指向数据的指针
    }

     它就是代码中经常用到的interface{},

    Case 1 :

     interface{}内部除了指针还有类型,当做图中的赋值,它还携带着类型就不再是nil了

    Case 2:

     因为interface{}本身就可以接收任意类型的参数,而*interface{}只能接收*interface{}的参数

    iface就是带有方法声明的接口,其底层结构

    type iface struct {     // 16 字节
        tab  *itab          // 这里面包含了接口自身信息,具体类型信息等
        data unsafe.Pointer
    }

    Case 1:

     在赋值的时候就以及带着结构体的各种类型信息了

  • 相关阅读:
    深度学习三巨头Hinton,Bengio,LeCunn共摘本年度ACM图灵奖(ACM A.M. Turing Award)
    【我为之而活的三种激情】by 伯特兰·罗素
    遥感高光谱分类文献阅读:Going Deeper with Contextual CNN for Hyperspectral Image Classification
    遥感高光谱分类文献阅读:Exploring Hierarchical Convolutional Features for Hyperspectral Image Classification
    大乘百法明门论笔记
    太宰治【人间失格】
    论文笔记:Accurate Causal Inference on Discrete Data
    因果推断(Causal Inference)概要
    阿毗达摩基本概念
    我们必须知道,我们终将知道。
  • 原文地址:https://www.cnblogs.com/peterleee/p/13886270.html
Copyright © 2011-2022 走看看