【Go基础】类型断言

477 阅读4分钟

类型断言

推荐阅读:【Golang】图解类型断言

我们将接口称之为 抽象类型,像 intslicestringmapstruct等内置和自定义类型称之为 具体类型

类型断言是 Go 语言在接口值上的一个神奇的特性,而类型断言的目标类型可以是抽象类型也可以是具体类型。那么我们就可以组合成四种类型断言,接下来我们逐一看看。

空接口.(具体类型)

var e interface{}
f,_ := os.Open("eggo.txt")
e = f
r,ok := e.(*os.File)

这种类型断言就是判断 e 变量存储的 _type 是否指向 *os.File 的类型元数据。

图片

f := "eggo"
e = f

e 的动态类型不是 *os.File 类型,而是 string 类型,那么断言就会失败。

图片

非空接口.(具体类型)

var rw io.ReadWriter
f,_ := os.Open("eggo.txt)
rw = f
r,ok := rw.(*os.File)

这种类型断言就是判断 rw 的动态类型是否为 *os.File 类型。

这里判断方式比较简单:判断 iface.tab 是否等于 <io.ReadWriter,*os.File> 这个组合对应的 itab 指针。

我们都知道一个 itab 靠接口类型和动态类型进行确定的并且 itab 可以进行复用,所以用上诉方式进行判断。

图片

type eggo struct {
    name string
}

func (e *eggo) Read(b []byte) (n int, err error) {
    return len(e.name), nil
}

func (e *eggo) Write(b []byte) (n int, err error) {
    return len(e.name), nil
}

在这里我们创建一个新的类型 eggo,同时也实现相关的接口方法。

f := eggo{name: "eggo"}
rw = &f

如果我们将 eggo 类型的变量赋值给 rw,再去做刚才的类型断言,此时 rwtab 与指向 <io.ReadWriter, *os.File> 组合对应的 itab 结构体指针不相等,所以断言失败。

图片

空接口.(非空接口)

var e interface{}

f,_ := os.Open("eggo.txt")
e = f

rw,ok := e.(io.ReadWriter)

这种断言就是判断 e 的动态类型是否实现了 io.ReadWriter 的接口方法。

e 的动态类型是 *os.File,我们接下来应该判断这个类型是否有 io.ReadWriter 接口的方法。

怎么判断呢?难道拿到类型列表方法信息进行一对一判断?

  • 当目标类型为非空接口时,我们会首先去根据 <io.ReadWriter,*os.File>itabTable 里面查找对应的 itab 指针。
    • 如果找到了,也要进一步确认 itab.fun[0] 是否等于 0,都满足那么皆大欢喜这个类型实现了接口的方法
    • 没有找到,再去检查动态类型的方法列表进行一对一的比较。

为什么会需要去进一步确认 itab.fun[0] 是否等于 0?

因为通过方法列表确定某个具体类型没有实现指定接口,就会把 itab 这里的 fun[0] 置为 0,然后同样会把这个 itab 结构体缓存起来,和那些断言成功的 itab 缓存一样。

这样子的目的就是避免再遇到同种类型断言时重复检查方法列表的操作

回到例子中,因为在 itabTable 找到了 <io.ReadWriter, *os.File> 对应的 itab 结构体,所以断言成功。

图片

f := "eggo"
e = f

现在 e 的类型是 string 类型。此时在 itabTable 找不到 <io.ReadWriter, string> 指向的 itab 结构体,同时 string 类型没有实现 io.ReadWriter 接口的方法,所以断言失败。

但是此时操作没有完,同时 <io.ReadWriter, string> 这个组合会对应下面这个 itab 结构体也会缓存进行哈希表中。

图片

非空接口.(非空接口)

var w io.Writer

f,_ := os.Open("eggo.txt")
w = f

rw,ok := w.(io.ReadWriter)

这种断言就是判断 w 的动态类型是否实现了 io.ReadWriter 接口的方法。

判断方法与前一种断言一致,

  • 当目标类型为非空接口时,我们会首先去根据 <io.ReadWriter,*os.File>itabTable 里面查找对应的 itab 指针。

    • 如果找到了,也要进一步确认 itab.fun[0] 是否等于 0,都满足那么皆大欢喜这个类型实现了接口的方法

    • 没有找到,再去检查动态类型的方法列表进行一对一的比较。

图片

type eggo struct {
  name string
}

func (e *eggo) Write(b []byte) (n int, err error) {
  return len(e.name), nil
}

f := eggo{name: "eggo"}
w = &f

我们创建了一个新类型 eggo,实现了 write 方法。现在 w 的动态类型是 *eggo 但是 *eggo 的方法列表缺少了一个 Read 方法,所以断言失败,但是 <io.ReadWriter,eggo> 组合对应的 itab 结构体指针会被缓存到哈希表中。

图片

总结

这四种的判断断言的方式可以分成两种:

  • 查看当前接口的动态类似是否满足目标对象。断言目标类型是具体类型,无论是空接口还是非空接口,其实都是看的接口的动态类型是否就是目标类型。
  • 先查表找到 itab 结构体,找不到比较方法,缓存起来。当目标类型是非空接口,其实判断的方法就是先去根据 <接口类型,动态类型> 组合 去查表,如果找到了,那就是满足了要求,如果没有找到,那么就得比较方法列表,然后缓存起来。

type switch

var e interface{}
str := "eggo"
e = str

switch b := e.(type) {
case *os.File:
    {
        fmt.Println("*os.File")
    }
case string:
    {
        fmt.Println(b)    //选择这个分支
    }
default:
    fmt.Println("default")
}

这里的 b 会被赋值为 e 的动态值,下面每个 case 都是把 e 的动态类型和某个具体类型作比较,相等则选择这个分支,没有匹配的则走到 default 分支。

算得上一个编程小技巧。