• GoLang接口---中



    引言

    GoLang接口—上

    上一篇文章中,我们对接口的基本使用和底层实现做了简单的了解,本文对接口的一些使用技巧做相关陈述。


    接口的类型断言

    一个接口类型的变量 varI 中可以包含任何类型的值,必须有一种方式来检测它的 动态 类型,即运行时在变量中存储的值的实际类型。

    在这里插入图片描述

    func main() {
    	var shaper Shaper
    
    	if rand.Intn(10) > 5 {
    		shaper = Circle{radius: 12.1}
    	} else {
    
    		shaper = Square{side: 12.1}
    	}
    
    	println(shaper)
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    显然,上面这段程序,只有在运行时才能确定shaper接口中存储的值的类型

    那么,有没有什么方法可以来判断当前shaper接口中存储的值的类型到底是什么呢?

    通常我们可以使用 类型断言 来测试在某个时刻 varI 是否包含类型 T 的值:

    v := varI.(T)       // unchecked type assertion
    
    • 1

    varI 必须是一个接口变量,否则编译器会报错:

    invalid type assertion: varI.(T) (non-interface type (type of varI) on left)
    • 1

    类型断言可能是无效的,虽然编译器会尽力检查转换是否有效,但是它不可能预见所有的可能性。如果转换在程序运行时失败会导致错误发生。更安全的方式是使用以下形式来进行类型断言:

    if v, ok := varI.(T); ok {  // checked type assertion
        Process(v)
        return
    }
    // varI is not of type T
    
    • 1
    • 2
    • 3
    • 4
    • 5

    如果转换合法,vvarI 转换到类型 T 的值,ok 会是 true;否则 v 是类型 T 的零值,okfalse,也没有运行时错误发生。

    在这里插入图片描述
    这里 接口变量.(接口实现类的类型) 的操作可以理解为将父类类型强制转换为子类类型后返回,但是转换的前提是,实现类必须实现了当前接口的所有方法才行,否则go编译会报错


    实例演示

    package main
    
    import (
    	"fmt"
    )
    
    type Square struct {
    	side float32
    }
    
    
    func (sq Square) Area() float32 {
    	return sq.side * sq.side
    }
    
    type Shaper interface {
    	Area() float32
    }
    
    func main() {
    	var shaper Shaper
        //调用方法为结构体---此时接口中保存的值类型为结构体
    	shaper = Square{side: 250}
    	
    	testValueType(shaper)
    	testPointerType(shaper)
        //调用方为指针--此时接口中保存的值类型为指针
    	shaper = &Square{side: 520}
    
    	testValueType(shaper)
    	testPointerType(shaper)
    }
    
    func testValueType(shaper Shaper) {
    	println("Test ValueType...")
    	//判断接口中保存的值类型是否为结构体
    	if square, ok := shaper.(Square); ok {
    		fmt.Println("当前Shaper接口中保存的值类型为Square, ", square)
    	} else {
    		fmt.Println("当前Shaper接口中保存的值类型不是Square")
    	}
    
    	fmt.Println("-----------------------------------------------")
    }
    
    func testPointerType(shaper Shaper) {
    	println("Test PointerType...")
    	//判断接口中保存的值类型是否为指针
    	if square, ok := shaper.(*Square); ok {
    		fmt.Println("当前Shaper接口中保存的值类型为Square, ", square)
    	} else {
    		fmt.Println("当前Shaper接口中保存的值类型不是Square")
    	}
    
    	fmt.Println("-----------------------------------------------")
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    在这里插入图片描述
    打印输出结果的具体原因,可以参考下面这幅图思考:
    在这里插入图片描述
    因为对于接口而言,当方法接受者为指针时,只有指针才能调用该方法,因此如果将上面的例子改一下:

    package main
    
    import (
    	"fmt"
    )
    
    type Square struct {
    	side float32
    }
    
    //方法接受者为指针,只有指针实现才能调用该方法
    func (sq *Square) Area() float32 {
    	return sq.side * sq.side
    }
    
    type Shaper interface {
    	Area() float32
    }
    
    func main() {
    	var shaper Shaper
       
    	//这里就无法结构体实现的Square就没有实现Shaper接口了
    	//shaper = Square{side: 250}
    	//
    	//testValueType(shaper)
    	//testPointerType(shaper)
    
    	shaper = &Square{side: 520}
    
    	//testValueType(shaper)
    	testPointerType(shaper)
    }
    
    //func testValueType(shaper Shaper) {
    //	println("Test ValueType...")
    //这里Square结构体并没有实现Shaper接口,因此不能直接这样进行强制类型转换
    //	if square, ok := shaper.(Square); ok {
    //		fmt.Println("当前Shaper接口中保存的值类型为Square, ", square)
    //	} else {
    //		fmt.Println("当前Shaper接口中保存的值类型不是Square")
    //	}
    //
    //	fmt.Println("-----------------------------------------------")
    //}
    
    func testPointerType(shaper Shaper) {
    	println("Test PointerType...")
    	if square, ok := shaper.(*Square); ok {
    		fmt.Println("当前Shaper接口中保存的值类型为Square, ", square)
    	} else {
    		fmt.Println("当前Shaper接口中保存的值类型不是Square")
    	}
    
    	fmt.Println("-----------------------------------------------")
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    在这里插入图片描述


    类型判断:type-switch

    接口变量的类型也可以使用一种特殊形式的 switch 来检测:type-switch

    switch t := areaIntf.(type) {
    case *Square:
        fmt.Printf("Type Square %T with value %v\n", t, t)
    case *Circle:
        fmt.Printf("Type Circle %T with value %v\n", t, t)
    case nil:
        fmt.Printf("nil value: nothing to check?\n")
    default:
        fmt.Printf("Unexpected type %T\n", t)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    变量 t 得到了 areaIntf 的值和类型, 所有 case 语句中列举的类型(nil 除外)都必须实现对应的接口(在上例中即 Shaper),如果被检测类型没有在 case 语句列举的类型中,就会执行 default 语句。

    可以用 type-switch 进行运行时类型分析,但是在 type-switch 不允许有 fallthrough 。

    如果仅仅是测试变量的类型,不用它的值,那么就可以不需要赋值语句,比如:

    switch areaIntf.(type) {
    case *Square:
        // TODO
    case *Circle:
        // TODO
    ...
    default:
        // TODO
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    nil 和 non-nil

    我们可以通过一个例子理解『Go 语言的接口类型不是任意类型』这一句话,下面的代码在 main 函数中初始化了一个 *TestStruct 结构体指针,由于指针的零值是 nil,所以变量 s 在初始化之后也是 nil:

    package main
    
    type TestStruct struct{}
    
    func NilOrNot(v interface{}) bool {
        return v == nil
    }
    
    func main() {
        var s *TestStruct
        fmt.Println(s == nil)      // #=> true
        fmt.Println(NilOrNot(s))   // #=> false
    }
    
    $ go run main.go
    true
    false
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    我们简单总结一下上述代码执行的结果:

    1. 将上述变量与 nil 比较会返回 true;
    2. 将上述变量传入 NilOrNot 方法并与 nil 比较会返回 false;

    出现上述现象的原因是调用 NilOrNot 函数时发生了隐式的类型转换,除了向方法传入参数之外,变量的赋值也会触发隐式类型转换。在类型转换时,*TestStruct 类型会转换成 interface{} 类型,转换后的变量不仅包含转换前的变量,还包含变量的类型信息 TestStruct,所以转换后的变量与 nil 不相等。


    空接口

    空接口或者最小接口 不包含任何方法,它对实现不做任何要求:

    type Any interface {}
    
    • 1

    任何其他类型都实现了空接口(它不仅仅像 Java/C# 中 Object 引用类型),any 或 Any 是空接口一个很好的别名或缩写。

    空接口类似 Java/C# 中所有类的基类: Object 类,二者的目标也很相近。

    可以给一个空接口类型的变量 var val interface {} 赋任何类型的值。

    示例:

    package main
    
    import "fmt"
    
    var i = 5
    
    var str = "ABC"
    
    type Person struct {
        name string
        age  int
    }
    
    type Any interface{}
    
    func main() {
        var val Any
        val = 5
        fmt.Printf("val has the value: %v\n", val)
        val = str
        fmt.Printf("val has the value: %v\n", val)
        pers1 := new(Person)
        pers1.name = "Rob Pike"
        pers1.age = 55
        val = pers1
        fmt.Printf("val has the value: %v\n", val)
        switch t := val.(type) {
        case int:
            fmt.Printf("Type int %T\n", t)
        case string:
            fmt.Printf("Type string %T\n", t)
        case bool:
            fmt.Printf("Type boolean %T\n", t)
        case *Person:
            fmt.Printf("Type pointer to Person %T\n", t)
        default:
            fmt.Printf("Unexpected type %T", t)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    输出:

    val has the value: 5
    val has the value: ABC
    val has the value: &{Rob Pike 55}
    Type pointer to Person *main.Person
    
    • 1
    • 2
    • 3
    • 4

    在上面的例子中,接口变量 val 被依次赋予一个 int,string 和 Person 实例的值,然后使用 type-switch 来测试它的实际类型。每个 interface {} 变量在内存中占据两个字长:一个用来存储它包含的类型,另一个用来存储它包含的数据或者指向数据的指针。


    构建通用类型或包含不同类型变量的数组

    通过使用空接口。让我们给空接口定一个别名类型 Element:type Element interface{}

    然后定义一个容器类型的结构体 Vector,它包含一个 Element 类型元素的切片:

    type Vector struct {
        a []Element
    }
    
    • 1
    • 2
    • 3

    Vector 里能放任何类型的变量,因为任何类型都实现了空接口,实际上 Vector 里放的每个元素可以是不同类型的变量。我们为它定义一个 At() 方法用于返回第 i 个元素:

    func (p *Vector) At(i int) Element {
        return p.a[i]
    }
    
    • 1
    • 2
    • 3

    再定一个 Set() 方法用于设置第 i 个元素的值:

    func (p *Vector) Set(i int, e Element) {
        p.a[i] = e
    }
    
    • 1
    • 2
    • 3

    Vector 中存储的所有元素都是 Element 类型,要得到它们的原始类型(unboxing:拆箱)需要用到类型断言。

    TODO:The compiler rejects assertions guaranteed to fail,类型断言总是在运行时才执行,因此它会产生运行时错误。


    复制数据切片至空接口切片

    假设你有一个 myType 类型的数据切片,你想将切片中的数据复制到一个空接口切片中,类似:

    var dataSlice []myType = FuncReturnSlice()
    var interfaceSlice []interface{} = dataSlice
    
    • 1
    • 2

    可惜不能这么做,编译时会出错:cannot use dataSlice (type []myType) as type []interface { } in assignment。

    原因是它们俩在内存中的布局是不一样的。

    必须使用 for-range 语句来一个一个显式地赋值:

    var dataSlice []myType = FuncReturnSlice()
    var interfaceSlice []interface{} = make([]interface{}, len(dataSlice))
    for i, d := range dataSlice {
        interfaceSlice[i] = d
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    通用类型的节点数据结构

    诸如列表和树这样的数据结构,在它们的定义中使用了一种叫节点的递归结构体类型,节点包含一个某种类型的数据字段。现在可以使用空接口作为数据字段的类型,这样我们就能写出通用的代码。下面是实现一个二叉树的部分代码:通用定义、用于创建空节点的 NewNode 方法,及设置数据的 SetData 方法。

    package main
    import "fmt"
    type Node struct {
        le   *Node
        data interface{}
        ri   *Node
    }
    func NewNode(left, right *Node) *Node {
        return &Node{left, nil, right}
    }
    func (n *Node) SetData(data interface{}) {
        n.data = data
    }
    func main() {
        root := NewNode(nil, nil)
        root.SetData("root node")
        // make child (leaf) nodes:
        a := NewNode(nil, nil)
        a.SetData("left node")
        b := NewNode(nil, nil)
        b.SetData("right node")
        root.le = a
        root.ri = b
        fmt.Printf("%v\n", root) // Output: &{0x125275f0 root node 0x125275e0}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    接口到接口

    一个接口的值可以赋值给另一个接口变量,只要底层类型实现了必要的方法。这个转换是在运行时进行检查的,转换失败会导致一个运行时错误:这是 Go 语言动态的一面,可以拿它和 Ruby 和 Python 这些动态语言相比较。

    假定:

    var ai AbsInterface // declares method Abs()
    type SqrInterface interface {
        Sqr() float
    }
    var si SqrInterface
    pp := new(Point) // say *Point implements Abs, Sqr
    var empty interface{}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    那么下面的语句和类型断言是合法的:

    empty = pp                // everything satisfies empty
    ai = empty.(AbsInterface) // underlying value pp implements Abs()
    // (runtime failure otherwise)
    si = ai.(SqrInterface) // *Point has Sqr() even though AbsInterface doesn’t
    empty = si             // *Point implements empty set
    // Note: statically checkable so type assertion not necessary.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    下面是函数调用的一个例子:

    type myPrintInterface interface {
        print()
    }
    func f3(x myInterface) {
        x.(myPrintInterface).print() // type assertion to myPrintInterface
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    x 转换为 myPrintInterface 类型是完全动态的:只要 x 的底层类型(动态类型)定义了 print 方法这个调用就可以正常运行


    参考

    • Go入门指南
    • Go语言设计与实现
  • 相关阅读:
    C++ primer 查漏补缺九:第六章 函数
    郑州轻工业大学OJ合集(C语言)【正在整理】
    Web3:数字身份与隐私保护的新篇章
    Android解析异步消息机制——Android筑基
    数据分析技能点-多元分析和应用
    深度学习之基于Yolov5人体姿态摔倒识别分析报警系统(GUI界面)
    MySQL - 访问控制
    会议纪要与需求变更申请书(软间项目管理课程)
    深入解析:AntSK 0.1.7版本的技术革新与多模型管理策略
    spring security oauth2 jwt过期时间不准原因分析以及解决办法
  • 原文地址:https://blog.csdn.net/m0_53157173/article/details/126344048