Go语言基础之接口断言

Go语言基础之接口断言

空接口可以存储任意类型的值,那我们如何获取其存储的具体数据呢?

一、接口类型的值

对于像Go语言这种静态类型的语言,一个变量的类型与具体的值是分开的概念,每个变量都有且只有一个类型,在编译时就已经确定不会发生改变,因此Go语言的变量的类型也称之为静态类型,比如 int、string、float32、*MyType、[]byte

type MyInt int
var i int
var j MyInt

上面的代码中,变量 i 的类型是 int,j 的类型是 MyInt。 所以,尽管变量 i 和 j 具有共同的底层类型 int,但它们的静态类型并不一样。不经过类型转换直接相互赋值时,编译器会报错。

特殊的是,接口类型,通常被人称之为动态类型,何意

1、首先须知一个接口类型的变量就是专门用于接收具体实现了本接口的变量值的,然后我们展开讨论。在底层,接口类型的变量中存放的内容是一对pair,初始状态下,接口变量中的pair为(nil,nil)

var w io.Writer // io包内的Writer接口,初始零值/pair是(nil,nil)

// 注意:接口值(一个接口类型的值,简称接口值)的类型是type存放的内容,即它所存放的具体变量的类型
fmt.Printf("%T %v\n", w, w)  // <nil> <nil>

image-20211102080906749

在被赋值了一个具体的变量后,pair中会记录下具体变量的类型和值

w = os.Stdout

// 注意:接口值(一个接口类型的值,简称接口值)的类型是type存放的内容,即它所存放的具体变量的类型
fmt.Printf("%T %v\n", w, w)  // *os.File &{0xc0000520c0}

image-20211102081145279

上述赋值过程发生了一个隐式转换,将具体类型转换到接口类型,这和显式的调用接口io.Writer(os.Stdout)的转换是等价的。但这类转换不管是显式的还是隐式的,都会将其对应的具体变量的类型和值存入接口type与value中

这个接口值的type被设为*os.Stdout指针的类型描述符,它的value持有os.Stdout的拷贝(是一个代表处理标准输出的os.File类型变量的指针)

但凡是实现了io.Writer接口的类型都可以赋值给接口变量w,所以我们可以继续为w赋值

w = new(bytes.Buffer)  //给接口值赋了一个*bytes.Buffer类型的值,bytes.Buffer也实现了接口io.Writer

// 注意:接口值(一个接口类型的值,简称接口值)的类型是type存放的内容,即它所存放的具体变量的类型
fmt.Printf("%T %v\n", w, w)  // *bytes.Buffer 

现在接口值w存放的type*bytes.Buffer并且value是一个指向新分配的缓冲区的指针

继续为w赋值

w = nil  // 将nil赋给了接口值

// 注意:接口值(一个接口类型的值,简称接口值)的类型是type存放的内容,即它所存放的具体变量的类型
fmt.Printf("%T %v\n", w, w)  // <nil> <nil>

此时无论w原来是否存放有内容,这个重置将它所有的部分(动态类型与动态值)都设为nil值,把变量w恢复到和它之前定义时相同的状态

image-20211102081614255

我们把上述讨论所有执行的代码汇总到一起看一下

var w io.Writer
fmt.Printf("%T %v\n", w, w)  // <nil> <nil>
w = os.Stdout
fmt.Printf("%T %v\n", w, w)  // *os.File &{0xc000052060}
w = new(bytes.Buffer)
fmt.Printf("%T %v\n", w, w)  // *bytes.Buffer
w = nil
fmt.Printf("%T %v\n", w, w)  // <nil> <nil>

会得出一个结论,接口值w的类型type是动态的,会随着其被赋予的具体值的类型变化而变化,并且接口存放的value也是动态的,所以才有人会说接口是动态类型,并将接口的pair中的type和value描述为动态类型与动态值

  • 1、动态类型type与动态值value指的是赋值给接口的那些具体的变量的类型与值,即便我们把一个被赋予具体值的接口值赋值给当前接口变量,当前接口变量中存放的也是对应的具体值的类型与值。
  • 2、接口值(一个接口类型的值,简称接口值)的类型是type存放的内容,即它所存放的具体变量的类型
  • 3、type类型部分存放的是与之相关类型的描述符,类型描述符指的是每个类型的详细信息,比如类型的名称和方法。

另外从概念上讲,不论接口值多大,动态值总是可以容下它。(这只是一个概念上的模型;具体的实现可能会非常不同)

d := time.Now()
var x interface{} = d

image-20211102082715883

二、接口值是否为空的判定依据是type

一个接口类型中的值是基于它的动态类型type被描述为空或非空的,与其动态value无关

var w io.Writer  // 动态类型与动态值分别为:<nil> <nil>
fmt.Println(w == nil)  // 动态类型为nil,所以结果为:true

w = os.Stdout  // 动态类型与动态值分别为:*os.File &{0xc000052060}
fmt.Println(w == nil)  // 动态类型不为nil,所以结果为:false

w = new(bytes.Buffer)  // 动态类型与动态值分别为:*bytes.Buffer
fmt.Println(w == nil)  // 动态类型不为nil,所以结果为:false

w = nil  // 动态类型与动态值分别为:<nil> <nil>
fmt.Println(w == nil)  // 动态类型为nil,所以结果为:true

三、nil接口与动态值为nil指针的接口

nil接口指的是动态类型与动态值均为nil的接口值,此时,该接口值与nil相等,如

var w io.Writer  // 动态类型与动态值分别为:<nil> <nil>
fmt.Println(w == nil) // true

而一个接口值有可能动态类型不为nil,而动态值为一个指向nil的指针,因为接口值是否为空的判定依据是其动态类型,所以此时该接口值不等于nil

先准备好一个指针类型的变量x和一个接口类型的变量out

// x是一个指针类型,该指针类型指向接口,注意日常开发不要使用指针指向接口类型
var x *bytes.Buffer  // x采用其零值,未开辟内存空间
fmt.Printf("%T %v\n", x, x)  // *bytes.Buffer <nil>,变量x是一个空指针,类型为指针

// 因为x不是一个接口类型,对于非接口类型,判定是否与nil相等的依据就是它的值,所以下述结果为true
fmt.Println(x == nil)  // true

// out是一个接口类型,再次强调,针对接口这种类型的值,判断是否为nil的依据是其存放的动态类型而非动态值
var out io.Writer  
fmt.Printf("%T %v\n", out,out)  // 动态值与动态类型:<nil> <nil>
fmt.Println(out == nil)  // true,因为接口类型out的动态类型为nil

然后再把x赋值给接口类型out,out存放的动态类型为x的类型即指针类型,而out存放的动态值为nil,如下

image-20211102083601921

再次强调,对于接口类型来说,判定其是否为空的依据是其动态类型而非动态值,也就是此时的out,虽然存的动态值为nil,但因为其存放的动态类型不为空,所以此时接口类型的变量out不为空

out=x  // 
fmt.Printf("%T %v\n", out,out)  // *bytes.Buffer <nil>
fmt.Println(out != nil)  // true

至此,得出一个重要结论,nil接口与动态值为nil的接口不是一回事

四、 接口值的相等性判断

接口值可以使用==!=来进行比较。两个接口值相等仅当它们都是nil值或者它们的动态类型相同并且动态值也相同(前提是动态值是可以比较的)。因为接口值是可比较的,所以它们可以用在map的键或者作为switch语句的操作数。

如果两个接口值的动态类型相同,但是这个动态类型是不可比较的(比如切片),将它们进行比较就会失败并且panic:

var x interface{} = []int{1, 2, 3}
fmt.Println(x == x) // panic: comparing uncomparable type []int

五、接口类型断言

一个接口的值(简称接口值)是由一个具体类型具体类型的值两部分组成的。这两部分分别称为接口的动态类型动态值

我们来看一个具体的例子:

var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil

请看下图分解:

接口值图解

想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:

x.(T)

其中:

  • x:表示类型为interface{}的变量
  • T:表示断言x可能是的类型。

该语法返回两个参数,第一个参数是x转化为T类型后的变量,第二个值是一个布尔值,若为true则表示断言成功,为false则表示断言失败。

举个例子:

func main() {
    var x interface{}
    x = "Hello word"
    v, ok := x.(string)
    if ok {
        fmt.Println(v)
    } else {
        fmt.Println("类型断言失败")
    }
}

上面的示例中如果要断言多次就需要写多个if判断,这个时候我们可以使用switch语句来实现:

func justifyType(x interface{}) {
    switch v := x.(type) {
    case string:
        fmt.Printf("x is a string,value is %v\n", v)
    case int:
        fmt.Printf("x is a int is %v\n", v)
    case bool:
        fmt.Printf("x is a bool is %v\n", v)
    default:
        fmt.Println("unsupport type!")
    }
}

因为空接口可以存储任意类型值的特点,所以空接口在Go语言中的使用十分广泛。

关于接口需要注意的是,只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。

七、总结

  1. 接口类型底层存放的是两个值,一个是类型type,一个是值value

  2. 两个接口值相等仅当它们都是nil值或者它们的动态类型相同并且动态值也相同(前提是动态值是可以比较的)

  3. 判断接口类型: v, ok := x.(string)

  4. 单纯断言判断接口值类型

    switch x.(type) {
    case *Square:
        // TODO
    case *Circle:
        // TODO
    ...
    default:
        // TODO
    }
    
posted @ 2021-11-02 09:02  RandySun  阅读(291)  评论(0编辑  收藏  举报