Go 学习笔记(二十一)接口类型断言和 type-switch

本文原创地址:博客园骏马金龙Go 基础系列:接口类型断言和 type-switch

接口类型探测:类型断言

接口实例中存储了实现接口的类型实例,类型的实例有两种:值类型实例和指针类型实例。在程序运行过程中,接口实例存储的实例类型可能会动态改变。例如:

// ins是接口实例
var ins Shaper

// ins存储值类型的实例
ins = c1

// 一段时间后...
...

// ins存储指针类型的实例,存储的类型发生改变
ins = c2

// 一段时间后...

// ins可能存储另一个类型实例
ins = s1

所以,需要一种探测接口实例所存储的是值类型还是指针类型。

探测的方法是:ins.(Type)ins.(*Type)。它们有两个返回值,第二个返回值是 ok 返回值,布尔类型,第一个返回值是探测出的类型。也可以只有一个返回值:探测出的类型。

// 如果ins保存的是值类型的Type,则输出
if t, ok := ins.(Type); ok {
	fmt.Printf("%T\n", v)
}

// 如果ins保存的是指针类型的*Type,则输出
if t, ok := ins.(*Type); ok {
	fmt.Printf("%T\n", v)
}

// 一个返回值的探测
t := ins.(Type)
t := ins.(*Type)

以下是一个例子:

package main

import "fmt"

// Shaper 接口类型
type Shaper interface {
	Area() float64
}

// Square struct类型
type Square struct {
	length float64
}

// Square类型实现Shaper中的方法Area()
func (s Square) Area() float64 {
	return s.length * s.length
}

func main() {
	var ins1, ins2 Shaper

	// 指针类型的实例
	s1 := new(Square)
	s1.length = 3.0
	ins1 = s1
	if v, ok := ins1.(*Square); ok {
		fmt.Printf("ins1: %T\n", v)
	}

	// 值类型的实例
	s2 := Square{4.0}
	ins2 = s2
	if v, ok := ins2.(Square); ok {
		fmt.Printf("ins2: %T\n", v)
	}
}

上面两个 Printf 都会输出,因为它们的类型判断都返回 true。如果将ins2.(Square)改为ins2.(*Square),第二个 Printf 将不会输出,因为 ins2 它保存的是值类型的实例。

以下是输出结果:

ins1: *main.Square
ins2: main.Square

特别需要注意的是, ins 必须明确是接口实例 。例如,以下前两种声明是有效的,第三种推断类型是错误的,因为它可能是接口实例,也可能是类型的实例副本。

var ins Shaper     // 正确
ins := Shaper(s1)  // 正确
ins := s1          // 错误

当 ins 不能确定是接口实例时,用它来进行测试,例如ins.(Square)将会报错:

invalid type assertion:ins.(Square) (non-interface type (type of ins) on left)

它说明了左边的 ins 是非接口类型 (non-interface type)。

另一方面,通过接口类型断言 (ins.(Type)), 如果 Type 是一个接口类型,就可以判断接口实例 ins 中所保存的类型是否也实现了 Type 接口 。例如:

var r io.Read
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
	return nil, err
}
r = tty

var w io.Writer
w = r.(io.Writer)

上面的 r 是 io.Read 接口的一个实例变量,它里面保存的是 tty 和它的类型,即(tty, *os.File),然后断言 r 的类型,探测它里面的类型*File是否也实现了 io.Writer 接口,如果实现了,则保存到 io.Writer 接口的实例变量 w 中,这样 w 实例也将保存(tty,*os.File)

由于任意内容都实现了空接口,所以,总是可以把一个接口实例无需通过任何断言地赋值给一个空接口实例:

var empty interface{}
empty = w

现在 empty 也保存了(tty,*os.File)

type Switch 结构

switch 流程控制结构还可以用来探测接口实例保存的类型。 这种结构称为 type-switch

用法如下:

switch v := ins.(type) {
case *Square:
	fmt.Printf("Type Square %T\n", v)
case *Circle:
	fmt.Printf("Type Circle %T\n", v)
case nil:
	fmt.Println("nil value: nothing to check?")
default:
	fmt.Printf("Unexpected type %T", v)
}

其中ins.(type)中的小写 type 是固定的词语。

以下是一个使用示例:

package main

import (
	"fmt"
)

// Shaper 接口类型
type Shaper interface {
	Area() float64
}

// Circle struct类型
type Circle struct {
	radius float64
}

// Circle类型实现Shaper中的方法Area()
func (c *Circle) Area() float64 {
	return 3.14 * c.radius * c.radius
}

// Square struct类型
type Square struct {
	length float64
}

// Square类型实现Shaper中的方法Area()
func (s Square) Area() float64 {
	return s.length * s.length
}

func main() {
	s1 := &Square{3.3}
	whichType(s1)

	s2 := Square{3.4}
	whichType(s2)

	c1 := new(Circle)
	c1.radius = 2.3
	whichType(c1)
}

func whichType(n Shaper) {
	switch v := n.(type) {
	case *Square:
		fmt.Printf("Type Square %T\n", v)
	case Square:
		fmt.Printf("Type Square %T\n", v)
	case *Circle:
		fmt.Printf("Type Circle %T\n", v)
	case nil:
		fmt.Println("nil value: nothing to check?")
	default:
		fmt.Printf("Unexpected type %T", v)
	}
}

上面的 type-switch 中,之所以没有加上case Circle,是因为 Circle 只实现了指针类型的 receiver,根据 Method Set 对接口的实现规则,只有指针类型的 Circle 示例才算是实现了接口 Shaper,所以将值类型的示例case Circle放进 type-switch 是错误的。

上一篇 Go 学习笔记(二十)空接口
Go 学习笔记(目录)
下一篇 Go 学习笔记(二十二)读取标准输入