类型转换和类型断言
概述
由于go语言不允许隐式类型转换,不同的类型之间的转换必须做显示的类型转换。而类型转换和类型断言的本质,就是把一个类型转换到另一个类型。
不过go语言必须做显示的类型转换的要求也有例外的情况:
- 当普通 t 类型变量向
i
接口类型转换时,是隐式转换的(编译时转换);(t->i) - 当
ix
接口变量向
i
接口类型转,是隐式转换的(编译完成时转换的);(i->i)
类型之间转换的例子
下面是go语言规范给出的部分例子:
*point(p) // same as *(point(p))
(*point)(p) // p is converted to *point
<-chan int(c) // same as <-(chan int(c))
(<-chan int)(c) // c is converted to <-chan int
func()(x) // function signature func() x
(func())(x) // x is converted to func()
(func() int)(x) // x is converted to func() int
func() int(x) // x is converted to func() int (unambiguous)
简单的说, x
需要转换为t
类型的语法是t(x)
.
如果对于某些地方的优先级拿不准可以自己加()
约束.
最后一个转换就是一个容易混淆的语句, 因此需要用括号(func() int)(x)
提高优先级.
还有一个容易混淆的地方是 只读和只写的通道类型 <-chan int
/chan<- int
.
变量类型转换方式
go语言的转换分两种:类型转换 和 类型断言
1. 类型转换
go里面的类型转换写法:
t(x)
(1)、语法:<结果类型> := <目标类型> ( <表达式> )
(2)、类型转换是用来在不同但相互兼容的类型之间的相互转换的方式,所以,当类型不兼容的时候,是无法转换的。如下:
var var1 int = 7
fmt.printf("%t->%v\n", var1, var1) //int->7
var2 := float32(var1)
fmt.printf("%t->%v\n", var2, var2) //float32->7
值得注意的是,如果某些类型可能引起误会,应该用括号括起来转换,如下:
//创建一个int变量,并获得它的指针
var1 := new(int32)
fmt.printf("%t->%v\n", var1, var1)
var2 := *int32(var1)
fmt.printf("%t->%v\n", var2, var2)
*int32(var1)相当于*(int32(var1)),一个指针,当然不能直接转换成一个int32类型,所以该表达式直接编译错误。将该表达式改为 (*int32)(var1)就可以正常输出了。
2. 类型断言(type assertion)
类型断言用于提取接口的基础值
go里面的类型断言写法:
x.(t)
其中x为interface{}类型,t是要断言的类型。
(1)语法:
<目标类型的值>,<布尔参数> := <表达式>.( 目标类型 ) // 安全类型断言
<目标类型的值> := <表达式>.( 目标类型 ) //非安全类型断言
(2)类型断言的本质,跟类型转换类似,都是类型之间进行转换,不同之处在于,类型断言实在接口之间进行,相当于java中,对于一个对象,把一种接口的引用转换成另一种。
x.(t) 检查x的动态类型是否是t,其中x必须是接口值。
我们先来看一个最简单的错误的类型断言:
func test() {
var i interface{} = "kk"
j := i.(int)
fmt.printf("%t->%d\n", j, j)
}
var i interface{} = "kk" 某种程度上相当于java中的,object i = "kk";
现在把这个 i 转换成 int 类型,系统内部检测到这种不匹配,就会调用内置的panic()函数,抛出一个异常。
改一下,把 i 的定义改为:var i interface{} = 99,就没问题了。输出为:
int->99
以上是不安全的类型断言。我们来看一下安全的类型断言:
func test() {
var i interface{} = "tt"
j, b := i.(int)
if b {
fmt.printf("%t->%d\n", j, j)
} else {
fmt.println("类型不匹配")
}
}
输出“类型不匹配”。
在理解有关接口的相关转换前,我们先要理解go语言中的接口类型:interface{}
定义格式:
type ia interface {} //声明了一个接口(没有函数集合时,是空接口类型)
申明一个空接口类型时,我们在定义“空接口类型”的变量时,可以赋值任意类型的值,如
package main
import (
"fmt"
)
type ia interface {} //空接口类型
func main() {
var a ia = 1 //int
var b ia = 1.1 //float
var c ia = false //bool
fmt.println(a,b,c)
}
接口类型(interface{})作为函数形式参数时,则该函数可以接受任意类型的变量,但对于函数内部,该变量仍然为interface{}类型(空接口类型)
package main
import (
"fmt"
)
func test(a interface{}) {
fmt.printf("%t->%v\n", a, a)
}
func main() {
test(1)
test(1.1)
test(true)
}
输出结果:
int->1
float64->1.1
bool->true
什么叫在在 函数内部,该变量任然为interface{}类型,如:
package main
import "fmt"
//用于输出数组元素
func echoarray(a interface{}){
for _,v:=range a {
fmt.print(v," ")
}
fmt.println()
return
}
func main(){
a:=[]int{2,1,3,5,4}
echoarray(a)
}
以上代码将会报错,因为对于echoarray()而言,a是interface{}类型,而不是[]int类型
所以前面代码中,将echoarray()做如下修改即可:
func echoarray(a interface{}){
b,_:=a.([]int)//通过断言实现类型转换
for _,v:=range b{
fmt.print(v," ")
}
fmt.println()
return
}
注意:在使用断言时最好用 (安全类型断言)
b,ok:=a.([]int)
if ok{
...
}
或
b,ok:=a.([]int) if ok{ //断言成功处理
...
}
的形式,这样能根据ok的值判断断言是否成功。
因为断言失败在编译阶段不会报错,所以很可能出现断言失败导致运行错误,而你却迟迟找不到原因(亲身经历)。
总结:“空接口类型”是动态类型的,即空接口类型的变量是任意类型的变量
通过上面的例子,我们对“空接口类型”有了了解,下面我们就对“接口之间的转换” 和 “接口和普通类型直接的转换”作讲解,来理解和接口之间的转换到底是“类型转换”还是“类型断言”
接口之间转换
go语言中接口的类型转换有很多奇怪的特性: 有时候是隐式转换, 有时候需要类型断言。
如:2个接口类型:
type ia interface {} //空接口类型
type ib interface {foo()} //非空接口类型
1. 空接口类型转非空接口类型
ia
要向ib
转换如何操作呢?
这个操作无法在编译期确定, 因此必然不是类型转换.。由于2者都是接口类型, 因此肯定是类型断言:
为什么说“在编译期确定”呢?通过上面的知识,我们知道 ia 是空接口类型,既然是空接口类型,说明ia对应的变量可以是任意类型,所以类型就无法确定(动态类型),所以在编译的时候,都无法确定
var a a
var b = a.(b)
当然因为上面的代码中的a
是nil,
会导致a.(b)
错误。但是请注意: 这只是运行错误, 并不是编译错误!
结论:空接口类型转非空接口类型时,是"类型断言"转换
2. 非空接口类型转空接口类型
ib
要向ia
转换如何操作呢? 因为ib是确定的类型,所以这个操作可以在编译期确定(类型), 因此必然是类型转换.
var b b
var a = a(b)
前面我们说过, go语言的接口是隐式转换的, 因此还可以省略强制转换的语句:
var b b
var a = b
接口和类型之间的转换(t(x))
虽然前面看到接口之间偶尔也会有类似普通类型之间的强制强制转换语法,
但从本意上来说接口是一个特殊的类型(和普通的类型区别).
我们先定义2个和前面的ia
/ib
匹配的普通类型(底层类型一样):
type ia interface {} //空接口类型
type ib interface {foo()} //非空接口类型
type ta int
type tb int
func (tb) foo(
) {}
如果是ta
和tb
之间的转换, 可以参考前面的类型之间转换的例子.
我们这里重点关注 ta
/tb
和 ia
/ib
之间的转换.
普通类型向接口类型转换是隐式的(可以编译期确定, 接口的隐式转换特权):
var ta ta
var ia = ta
var tb tb
var ib = tb
接口类型向普通类型是类型断言(运行期确定):
var ia ia
var ta = ia.(ta)
var ib ib
var tb = ib.(tb)
类型断言在编译期是没有任何保障的, 错误的代码也可以编译通过:
var ta = ib.(ta)
var tb = ia.(tb)
总结:
x.(t)
检查x的动态类型是否是t,其中x必须是接口值。
- 如果t是具体类型,类型断言检查x的动态类型是否等于具体类型t。如果检查成功,类型断言返回的结果是x的动态值,其类型是t。换句话说,对接口值x断言其动态类型是具体类型t,若成功则提取出x的具体值。如果检查失败则panic。
- 如果t是接口类型,类型断言检查x的动态类型是否满足t。如果检查成功,x的动态值不会被提取,返回值是一个类型为t的接口值。换句话说,到接口类型的类型断言,改变了表达式的类型,改变了(通常是扩大了)可以访问的方法,且保护了接口值内部的动态类型和值。
- 无论t是什么类型,如果x是nil接口值,则类型断言失败。
- 类型断言到一个较少限制(较少方法)的接口类型基本是不需要的,因为这个行为和赋值一样(除了nil的情况)
- 如果我们想知道类型断言是否失败,而不是失败时触发panic,可以使用返回两个值的版本:
y, ok := x.(t)