19.3 类型断言

前面我们可以把实现了某个接口的类型值保存在接口变量中,但反过来某个接口变量属于哪个类型呢?如何检测接口变量的类型呢?这就是类型断言(Type Assertion)的作用。

接口类型I的变量 varI 中可以包含任何实现了这个接口的类型的值,如果多个类型都实现了这个接口,所以有时我们需要用一种动态方式来检测它的真实类型,即在运行时确定变量的实际类型。

通常我们可以使用类型断言(value, ok := element.(T))来测试在某个时刻接口变量 varI 是否包含类型 T 的值:

value, ok := varI.(T)       // 类型断言

varI 必须是一个接口变量 ,否则编译器会报错:invalid type assertion: varI.(T) (non-interface type (type of I) on left) 。

类型断言可能是无效的,虽然编译器会尽力检查转换是否有效,但是它不可能预见所有的可能性。如果转换在程序运行时失败会导致错误发生。更安全的方式是使用以下形式来进行类型断言:

var varI I
varI = T("Tstring")
if v, ok := varI.(T); ok { // 类型断言
	fmt.Println("varI类型断言结果为:", v) // varI已经转为T类型
	varI.f()
}

如果断言成功,v 是 varI 转换到类型 T 的值,ok 会是 true;否则 v 是类型 T 的零值,ok 是 false,也没有运行时错误发生。

接口类型向普通类型转换有两种方式:Comma-ok断言和Type-switch测试。

通过Type-switch做类型判断

接口变量的类型可以使用一种特殊形式的 switch 做类型断言:

// Type-switch做类型判断
var value interface{}
switch str := value.(type) {
case string:
	fmt.Println("value类型断言结果为string:", str)
case Stringer:
	fmt.Println("value类型断言结果为Stringer:", str)
default:
	fmt.Println("value类型不在上述类型之中")
}

可以用 Type-switch 进行运行时类型分析,但是在 type-switch 时不允许有 fallthrough 。Type-switch让我们在处理未知类型的数据时,比如解析 JSON 等编码的数据,会非常方便。

测试一个值是否实现了某个接口(Comma-ok断言)

我们想测试它是否实现了 I 接口,可以这样做类型断言:

// Comma-ok断言
var varI I
varI = T("Tstring")
if v, ok := varI.(T); ok { // 类型断言
	fmt.Println("varI类型断言结果为:", v) // varI已经转为T类型
	varI.f()
}

接口描述了一系列的行为,规定可以做什么行为,“当一个东西,走起来像鸭子,叫起来也像鸭子,游泳也像鸭子,那么我们可以认为他就是一只鸭子”。类型实现不同的接口将拥有不同的行为方法集合,这就是多态的本质。

下面是上面几个代码片段的完整代码文件:

package main
import (
	"fmt"
)
type I interface {
	f()
}
type T string
func (t T) f() {
	fmt.Println("T Method")
}
type Stringer interface {
	String() string
}
func main() {
	// 类型断言
	var varI I
	varI = T("Tstring")
	if v, ok := varI.(T); ok { // 类型断言
		fmt.Println("varI类型断言结果为:", v) // varI已经转为T类型
		varI.f()
	}
	// Type-switch做类型判断
	var value interface{} // 默认为零值
	switch str := value.(type) {
	case string:
		fmt.Println("value类型断言结果为string:", str)
	case Stringer:
		fmt.Println("value类型断言结果为Stringer:", str)
	default:
		fmt.Println("value类型不在上述类型之中")
	}
	// Comma-ok断言
	value = "类型断言检查"
	str, ok := value.(string)
	if ok {
		fmt.Printf("value类型断言结果为:%T\n", str) // str已经转为string类型
	} else {
		fmt.Printf("value不是string类型 \n")
	}
}
程序输出:
varI类型断言结果为: Tstring
T Method
value类型不在上述类型之中
value类型断言结果为:string

使用接口使代码更具有普适性,例如函数的参数为接口变量。标准库中遵循了这个原则,但如果对接口概念没有良好的把握,是不能很好理解它是如何构建的。

那么为什么在Go语言中我们可以进行类型断言呢?我们可以在上面代码中看到,断言后的值 v, ok := varI.(T),v值对应的是一个类型名:Tstring 。 因为在Go语言中,一个接口值(Interface Value)其实是由两部分组成:type :value。所以在做类型断言时,变量只能是接口类型变量,断言得到的值其实是接口值中对应的类型名。这在后面讨论reflect反射包时将会有更深入的说明。