【新人系列】Golang 入门(十五):类型断言

✍ 个人博客:https://blog.csdn.net/Newin2020?type=blog

📝 专栏地址:https://blog.csdn.net/newin2020/category_12898955.html

📣 专栏定位:为 0 基础刚入门 Golang 的小伙伴提供详细的讲解,也欢迎大佬们一起交流~

📚 专栏简介:在这个专栏,我将带着大家从 0 开始入门 Golang 的学习。在这个 Golang 的新人系列专栏下,将会总结 Golang 入门基础的一些知识点,并由浅入深的学习这些知识点,方便大家快速入门学习~

❤️ 如果有收获的话,欢迎点赞 👍 收藏 📁 关注,您的支持就是我创作的最大动力 💪

1. 快速了解

在 Go 语言(Golang)中,断言(Type Assertion)是一种用于在运行时检查接口值实际保存的具体类型,并获取对应类型值的机制。

例如,下面 add 函数中入参被定义成了空接口的类型,因此我们可以针对入参进行类型断言,通过 a.(int) 的方式来判断入参的类型是否为 int 类型。

go 复制代码
func add(a, b interface{}) interface{} {
    ai, ok := a.(int)
    if !ok {
        panic("not int type")
    }
    bi, _ := b.(int)
    return ai + bi
}

func main() {
    a := 1
    b := 2
    fmt.Println(add(a,b))
}

当然,我们可以利用 switch 语法来适应不同类型的断言:

go 复制代码
func add(a, b interface{}) interface{} {
    switch a.(type) {
    case int :
        ai, _ := a.(int)
        bi, _ := b.(int)
        return ai + bi
    case int32:
        ai, _ := a.(int32)
        bi, _ := b.(int32)
        return ai + bi
    case float64:
        af, _ := a.(float64)
        bf, _ := b.(float64)
        return af + bf
    default:
        panic("not supported type")
    }
}

func main() {
    fmt.Println(add(1,2))
    fmt.Println(add(1.2,2.3))
}

而从前面的接口篇章可以得知,接口可以分为「空接口」和「非空接口」两类。

相对于接口这种「抽象类型」,int、string、slice 等等都可以被称为「具体类型」。

  • 类型断言作用于接口值之上,可以是空接口或非空接口
  • 断言的目标类型,可以是具体类型或非空接口类型

这样,就组合出了四种类型断言,接下来我们就逐一看看它们究竟是怎样 "断言" 的。

2 空接口.(具体类型)

go 复制代码
var e interface {}

r, ok := e.(*os.File)

上面的代码中 e.(*os.File) 是要判断 e 的动态类型是否为 *os.File,而这只需要确定 _type 是否指向 *os.File 的类型元数据即可。

在 go 语言里每种类型的类型元数据都是全局唯一的,如果像下面这样给 e 赋值,那么 e 的动态值就是 f,动态类型就是 *os.File。

所以下面代码会断言成功,ok 为 true,r 被赋值为 e 的动态值。

go 复制代码
var e interface {}

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

r, ok := e.(*os.File)

而如果像下面这样赋值,e 的动态类型就是 string 类型,那么类型断言就会失败。

此时 ok 就为 false,r 会被赋值为 *os.File 的类型零值 nil。

go 复制代码
var e interface {}

f  := "output"
e = f

r, ok := e.(*os.File)

3 非空接口.(具体类型)

go 复制代码
var rw io.ReadWriter

r, ok := rw.(*os.File)

上面代码中 rw.(*os.File) 是要判断 rw 的动态类型是否为 *os.File。

之前的篇章中,我们也介绍过,程序中用到的 itab 结构体都会缓存起来,可以通过接口类型和动态类型组合起来的 key,查找到对应的 itab 指针。

所以这里的类型断言只需要一次比较就能完成,只要看 tab = &itab 是否指向这个 itab 结构体即可

如果 rw 像下面代码这样赋值,那么它的动态值就是 f,动态类型就是 *os.File。

go 复制代码
var rw io.ReadWriter

f, _ := os.Open("output.txt")
rw = f

r, ok := rw.(*os.File)

所以 tab 指向下面图中第二块部分的这个 itab 结构体,因此类型断言成功,ok 为 true 且 r 被赋值为 rw 的动态值。

然而,如果 rw 动态类型不是 *os.File。

go 复制代码
var rw io.ReadWriter

f, _ := output{name: "output"}
rw = f

r, ok := rw.(*os.File)

那么此时 tab 就指向下面图中第一块部分,而不是图中第二块部分的地方。所以类型断言就会失败,ok 为 false 且 r 会被置为 *os.File 的类型零值 nil。

4. 空接口.(非空接口)

go 复制代码
var e interface {}

rw, ok := e.(ioReadWriter)

e.(ioReadWriter) 是要判断 e 的动态类型是否实现了 io.ReadWriter 接口,当然之前的篇章中我们已经介绍过类型关联的方法列表该去哪里找了。

如果像下面代码中 e 这样赋值,那么它的动态值就是 f,动态类型就是 *os.File。

go 复制代码
var e interface {}

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

rw, ok := e.(ioReadWriter)

虽然 *os.File 元数据后面可以找到类型关联的方法元数据数组,但也不必每次都去检查这里是否有对应接口要求的所有方法。

因为存在 itab 缓存,所以可以先去 itab 缓存中查找一下。若没有 io.ReadWriter 和 *os.File 对应的 itab 结构体,再去检查 *os.File 的方法列表也不迟。

值得强调的是,就算能够从缓存中查找到对应的 itab 指针,也要进一步判断 itab.fun[0] 是否等于 0。

这是因为断言失败的类型组合,其对应的 itab 结构体也会被缓存起来,只是会把 itab.fun[0] 置为 0,用以表示这里的动态类型并没有实现对应的接口。

这样以后再遇到同种类型断言时,就不用再去检查方法列表了,可以直接断言失败。

而我们上面这个例子中,类型断言是成功的,所以 ok 为 true,而 rw 就是一个 io.ReadWriter 类型的变量,其动态值与 e 相同,tab 就指向下面这样一个 itab 结构体。

但如果 e 被赋值为一个字符串,那么它的动态类型就是 string,并没有实现要求的 Read 和 Write 方法,所以对应的 itab 中 fun[0] 的值为 0,并且会被添加到 itab 缓存中。

go 复制代码
var e interface {}

f, _ := "output"
e = f

rw, ok := e.(ioReadWriter)

因此这里断言失败,ok 为 false 且 rw 为 io.ReadWriter 的类型零值 nil。

5. 非空接口.(非空接口)

go 复制代码
var w io.Writer

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

w.(io.ReadWriter) 是要判断 w 存储的动态类型是否实现了 io.ReadWriter 接口,w 是 io.Writer 类型,接口要求一个 Write 方法,而 io.ReadWriter 接口要求实现 Read 和 Write 两个方法。

如果 w 像下面的代码这样赋值,其动态值就是 f。

go 复制代码
var w io.Writer

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

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

tab 指向下图这样一个 itab 结构体,要确定 *os.File 是否实现了 io.ReadWriter 接口,同样会先去 itab 缓存里查找这个组合对应的 itab 指针。

若存在,且 itabfun[0] 不等于 0,则断言成功;若不存在,再去检查 *os.File 的方法列表,并缓存 itab 信息。

而在这里的代码中,断言是成功的,即 ok 为 true 且 rw 为 io.ReadWriter 类型的变量,动态值与 w 相同,而 tab 指向下面这个 itab 结构体。

如果我们自定义一个 output 类型,并且 *output 类型只实现了 io.Writer 接口,并没有实现 io.ReadWriter 接口。

go 复制代码
type output struct {
    name string
}

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

现在如果把这个 output 类型的变量赋值给 w,那么此时 w 的动态类型就为 *output,并没有实现指定接口,所以 fun[0] = 0,该 itab 结构体就会被缓存起来。

go 复制代码
var w io.Writer

f, _ := output{name: "john")
w = &f

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

因此,类型断言会失败,ok 为 false 且 rw 的 tab 和 data 均为 nil。

所以,类型断言的关键是明确接口的动态类型,以及对应的类型实现了哪些方法。而明确这些的关键还是「类型元数据」,以及空接口与非空接口的「数据结构」。

相关推荐
_一条咸鱼_22 分钟前
Android ARouter 处理器模块深度剖析(三)
android·面试·android jetpack
_一条咸鱼_28 分钟前
Android ARouter 基础库模块深度剖析(四)
android·面试·android jetpack
難釋懷29 分钟前
bash的特性-bash中的引号
开发语言·chrome·bash
_一条咸鱼_1 小时前
Android ARouter 核心路由模块原理深度剖析(一)
android·面试·android jetpack
_一条咸鱼_1 小时前
Android ARouter 编译器模块深度剖析(二)
android·面试·android jetpack
Hello eveybody1 小时前
C++按位与(&)、按位或(|)和按位异或(^)
开发语言·c++
6v6-博客1 小时前
2024年网站开发语言选择指南:PHP/Java/Node.js/Python如何选型?
java·开发语言·php
Baoing_1 小时前
Next.js项目生成sitemap.xml站点地图
xml·开发语言·javascript
被AI抢饭碗的人2 小时前
c++:c++中的输入输出(二)
开发语言·c++
lqqjuly2 小时前
C++ 面向对象关键语法详解:override、虚函数、转发调用和数组引用传参-策略模式
开发语言·c++