我们已经知道接口可以分为空接口与非空接口两类。相对于接口这种“抽象类型”,像int,slice,string,map,struct等类型被称为“具体类型”。
类型断言是Go语言中应用在接口值上的一个神奇特性,而类型断言的目标类型可以是某种具体类型,也可以是某种非空接口类型。这样我们就组合出四种类型断言,接下来就逐一看看它们究竟是如何断言的!
var e interface{}
//......
r,ok := e.(*os.File)
e.(os.File)就是要判断e存储的_type是否指向os.File的类型元数据。反正我们介绍过Go语言里每种类型的类型元数据都是全局唯一的。
f,_ := os.Open("hello.txt")
e = f
如果e像上面这样赋值,e的动态类型就是*os.File,所以e.(*os.File)类型断言成功,ok为true,r为e的动态值。
f := "eggo"
e = f
请看以下代码的演示:
package main
import (
"fmt"
"os"
)
func main() {
var e interface{} //e为空接口类型
r, ok := e.(*os.File) //将e与*os.File类型进行类型断言
fmt.Println(r)
fmt.Println(ok)
fmt.Println()
f, _ := os.Open("hello.txt")
e = f //将*os.File类型的f赋值给e
r, ok = e.(*os.File)
fmt.Println(r)
fmt.Println(ok)
fmt.Println()
f1 := "hello"
e = f1 //将string类型的f1赋值给e
r, ok = e.(*os.File)
fmt.Println(r)
fmt.Println(ok)
fmt.Println()
}
执行go run
以后的结果:
<nil>
false
&{0xc000088780}
true
<nil>
false
var rw io.ReadWriter
//......
r,ok := rw.(*os.File)
rw.(os.File)是要判断rw的动态类型是否为os.File。前面我们介绍过,程序中用到的itab结构体都会缓存起来,可以通过<接口类型, 动态类型>组合起来的key,查找到对应的itab指针。
所以这里的类型断言只需要一次比较就能完成,就是看iface.tab是否等于
如果把一个os.File类型的变量f赋给rw,它的动态值就是f,动态类型就是os.File。
f,_ := os.Open("hello.txt")
rw = f
rw这里存储的itab指针就指向
下面我们定义一个eggo类型,并且由*eggo类型实现io.ReadWriter接口。
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类型的变量赋值给rw,rw的动态类型就是eggo,rw持有的itab指针就不指向
f := eggo{name: "eggo"}
rw = &f
请看以下代码的演示:
package main
import (
"fmt"
"io"
"os"
)
// 定义一个eggo类型
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
}
func main() {
var rw io.ReadWriter
r, ok := rw.(*os.File)
fmt.Println(r)
fmt.Println(ok)
fmt.Println()
f, _ := os.Open("hello.txt")
rw = f
r, ok = rw.(*os.File)
fmt.Println(r)
fmt.Println(ok)
fmt.Println()
f1 := eggo{name: "eggo"}
rw = &f1
r, ok = rw.(*os.File)
fmt.Println(r)
fmt.Println(ok)
fmt.Println()
}
执行go run
以后的运行结果:
<nil>
false
<nil>
true
<nil>
false
var e interface{}
//......
rw,ok := e.(io.ReadWriter)
e.(io.ReadWriter)就是要判断e的动态类型是否实现了io.ReadWriter接口。
如果e像这样赋值:
f,_ := os.Open("hello.txt")
e = f
e的动态类型就是os.File,我们知道os.File类型元数据的后面可以找到该类型实现的方法列表描述信息。
其实并不需要每次都检查动态类型的方法列表,还记得itab缓存吗? 实际上,当类型断言的目标类型为非空接口时,会首先去itabTable里查找对应的itab指针,若没有找到,再去检查动态类型的方法列表。
此处注意,就算从itabTable中找到了itab指针,也要进一步确认itab.fun[0]是否等于0。这是因为一旦通过方法列表确定某个具体类型没有实现指定接口,就会把itab这里的fun[0]置为0,然后同样会把这个itab结构体缓存起来,和那些断言成功的itab缓存一样。这样做的目的是避免再遇到同种类型断言时重复检查方法列表。
回到例子中,这里会断言成功,ok为true,rw就是一个io.ReadWriter类型的变量,其动态值与e相同。tab指向
f := "eggo"
e = f
然而如果把一个字符串赋值给e,它的动态类型就是string,
断言失败,ok为false,rw为io.ReadWriter的类型零值,即tab和data均为nil。
示例请看以下代码:
package main
import (
"fmt"
"io"
"os"
)
func main() {
var e interface{}
rw, ok := e.(io.ReadWriter)
fmt.Println(rw)
fmt.Println(ok)
fmt.Println()
f, _ := os.Open("hello.txt")
e = f
rw, ok = e.(io.ReadWriter)
fmt.Println(rw)
fmt.Println(ok)
fmt.Println()
f1 := "eggo"
e = f1
rw, ok = e.(io.ReadWriter)
fmt.Println(rw)
fmt.Println(ok)
fmt.Println()
}
执行```go run``以后的运行结果:
<nil>
false
&{0xc000108780}
true
<nil>
false
var w io.Writer
//......
rw,ok := w.(io.ReadWriter)
w.(io.ReadWriter)是要判断w的动态类型是否实现了io.ReadWriter接口。
下面同样把一个os.File类型的变量f赋值给w,它的动态值就是f,动态类型就是os.File。
f,_ := os.Open(“hello.txt”)
w = f
要确定*os.File是否实现了io.ReadWriter接口,同样会先去itab缓存里查找
这里断言成功,ok为true,rw为io.ReadWriter类型的变量,动态值与w相同,而itab是
下面我们自定义一个eggo类型,且eggo类型只实现io.Writer要求的Write方法,并没有实现io.ReadWriter额外要求的Read方法。如果把一个eggo类型的变量赋给w:
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
此时,w的动态类型为eggo,而eggo的方法列表里缺少一个Read方法,所以类型断言失败,下面这个itab被缓存起来。
断言失败后,ok为false,rw的data和tab均为nil。
示例请看以下代码:
package main
import (
"fmt"
"io"
"os"
)
type eggo struct {
name string
}
func (e *eggo) Write(b []byte) (n int, err error) {
return len(e.name), nil
}
func main() {
var w io.Writer
rw, ok := w.(io.ReadWriter)
fmt.Println(rw)
fmt.Println(ok)
fmt.Println()
f, _ := os.Open("hello.txt")
w = f
rw, ok = w.(io.ReadWriter)
fmt.Println(rw)
fmt.Println(ok)
fmt.Println()
f1 := eggo{name: "eggo"}
w = &f1
rw, ok = w.(io.ReadWriter)
fmt.Println(rw)
fmt.Println(ok)
fmt.Println()
}
执行go run以后的结果:
<nil>
false
&{0xc000108780}
true
<nil>
false
综上,类型断言的关键是明确接口的动态类型,以及对应的类型实现了哪些方法。而明确这些的关键,还是类型元数据,以及空接口与非空接口的数据结构。接下来的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分支。
有时会遇到多个类型放在一个分支的情况,这时b的类型是interface{}。
switch b := e.(type) {
case *os.File:
{
fmt.Println("这里b的类型为*os.File", b)
}
case string:
{
fmt.Println("这里b的类型为string", b)
}
case int, int32, int64:
{
fmt.Println("多类型分支里b的类型为interface{}", b)
}
default:
fmt.Println("default")
}
参考资料:
https://mp.weixin.qq.com/s/i0vmHjF7faDo0hvOlVfJcA