开心一刻
小学时,因为淘气,再加上成绩不好,爸妈经常打我,有次爷爷奶奶来家里,看到我挨打,心疼啊,就把我接过去住!没一个月,我就被送回父母家了,爷爷奶奶进门第一句话就是: “你们先把这臭小子狠狠地打一顿,我们再回去!”……
写在前面
golang 的类型断言(Type Assertion)是一个使用在接口值上的操作,用于检查接口类型变量所持有的值是否实现了期望的接口或者具体的类型。
golang 断言语法
断言的语法格式如下:
value, ok := x.(T)
其中,x 表示一个接口的类型,这里又可以分为空接口和非空接口两种,T 表示断言的目标类型,可以是具体类型,也可为非空接口类型(不能是空接口的原因应该是断言成空接口没有意义),因此共有四种组合。该断言表达式会返回 x 断言后的值(也就是 value,如果断言失败,value 就是 T 的默认值)和一个布尔值(也就是 ok),可根据该布尔值判断 x 是否为 T 类型。
接下来针对这四种组合进行一一解析。
空接口.(具体类型)
该断言比较简单,会判断空接口的动态类型(是一个_type类型的指针,_type就是某种具体类型的类型原数据)是否指向该具体类型 T的类型元数据,如果是,则断言成功,ok 为 true,value 的值为空接口中动态类型的值,否则为 false,value 为类型 T 的默认值。需要注意的一点是:空接口的动态类型和具体类型 T 的类型原数据一致才算断言成功,T 和*T 拥有不同的类型原数据,所以如果空接口的动态类型为*T,具体类型为 T,那么一定断言失败。
举例:
package main
import (
"fmt"
"os"
)
func main() {
var a interface{}
f, _ := os.Open("test.txt")
a = f
value, ok := a.(*os.File)
fmt.Printf("value type %T, assert result: %t\n", value, ok)
}
*os.File是一种具体类型,f 恰好是*os.File类型的变量,f 赋值给 a 之后,a 的动态类型为*os.File,动态值为 nil,因此该断言是成功的。注意如果括号中的*os.File改成 os.File,那么断言一定失败。
空接口.(非空接口)
这种断言会判断空接口的动态类型是否实现了该非空接口 T 要求实现的方法,如果实现了 T 的方法,那么断言成功。这时会将空接口的动态类型和动态值包装为 T 的动态值和动态方法,赋值给 value,这时 value 就可以使用非空接口的方法了。
举例:
package main
import (
"fmt"
"io"
"os"
)
func main() {
var a interface{}
f, _ := os.Open("test.txt")
a = f
value, ok := a.(io.ReadWriter)
fmt.Printf("value type %T, assert result: %t\n", value, ok)
}
具体判断的方法如下:
- 当目标类型为非空接口时,首先去根据 <io.ReadWriter,*os.File> 去 itabTable 里面查找对应的 itab 指针。
- 如果找到了,也要进一步确认 itab.fun[0] 是否等于 0,如果不等于 0那么皆大欢喜这个类型实现了接口的方法,等于 0 则没有实现,断言就失败了。
- 没有找到,再去检查动态类型的方法列表进行一对一的比较。
为什么会需要去进一步确认 itab.fun[0] 是否等于 0?
因为通过方法列表确定某个具体类型没有实现指定接口,就会把 itab 这里的 fun[0] 置为 0,然后同样会把这个 itab 结构体缓存起来,和那些断言成功的 itab 缓存一样。
这样子的目的就是避免再遇到同种类型断言时重复检查方法列表的操作。
非空接口.(具体类型)
程序会根据非空接口的接口类型和具体类型的组合去检查 itab 缓存,如果发现该组合与非空接口的 itab 类型的指针一致,则断言成功,否则则失败。
举例:
package main
import (
"fmt"
"io"
"os"
)
func main() {
var a io.ReadWriter
f, _ := os.Open("test.txt")
a = f
value, ok := a.(*os.File)
fmt.Printf("value type %T, assert result: %t\n", value, ok)
}
这里判断方式比较简单:判断 iface.tab 是否等于 <io.ReadWriter,*os.File> 这个组合对应的 itab 指针。
不过这里我觉得比较非空接口的动态类型的类型元数据和该具体类型的类型元数据是否一致即可,
非空接口.(非空接口)
这里就需要去查找 itab 缓存了,以 x.(T) 说明,按照<T的接口类型,x 的动态类型>组成的 itab 指针查找 itab 缓存,如果存在,则去判断 fun[0] == 0是否成立,如果成立,表明x 的动态类型没有实现 T 的接口类型要求实现的方法,也就是断言失败,如果不成立表示 x 的动态类型实现了 T 的接口类型要求实现的方法,那么断言成功。
举例:
package main
import (
"fmt"
"io"
"os"
)
func main() {
var a io.ReadWriter
f, _ := os.Open("test.txt")
a = f
value, ok := a.(io.Read)
fmt.Printf("value type %T, assert result: %t\n", value, ok)
}
总结
如果 T 是具体某个类型,类型断言会检查 x 的动态类型是否等于具体类型 T。如果检查成功,类型断言返回的结果是 x 的动态值,其类型是 T。
如果 T 是非空接口类型,类型断言会检查 x 的动态类型是否满足 T。如果检查成功,x 的动态值不会被提取,返回值是一个类型为 T 的接口值。
无论 T 是什么类型,如果 x 是 nil 接口值,类型断言都会失败。
这四种的判断断言的方式可以分成两种:
查看当前接口的动态类似是否满足目标对象。断言目标类型是具体类型,无论是空接口还是非空接口,其实都是看的接口的动态类型是否就是目标类型。
先查表找到 itab 结构体,找不到比较方法,缓存起来。当目标类型是非空接口,其实判断的方法就是先去根据 <接口类型,动态类型> 组合 去查表,如果找到了,那就是满足了要求,如果没有找到,那么就得比较方法列表,然后缓存起来。