首页 > 编程笔记

Go语言接口类型详解

Go 语言接口的类型包括静态类型和动态类型,接下来分开进行讲解。

1、动态类型

接口绑定的具体实例的类型称为接口的动态类型。接口可以绑定不同类型的实例,所以,接口的动态类型是随着其绑定的不同类型实例而发生变化的。

一个接口类型的变量 varI 中可以包含任何类型的值。必须有一种方式来检测它的动态类型,即运行时在变量中存储的值的实际类型。在执行过程中动态类型可能会有所不同,但它一定是可以分配给接口变量的类型。

通常可以使用类型断言(Go 语言内置的一种智能推断类型的功能)来测试在某个时刻 varI 是否包含类型 T 的值。
V := varI.(T)
//未经检查的类型断言
varI 必须是一个接口变量,否则编译器会报错。
invalid type assertion: varI.(T) (n-interface type (type of varI) on left)
类型断言可能是无效的,虽然编译器会尽力检查转换是否有效,但是它不可能预见所有的可能性。如果转换在程序运行时失败,则会导致错误发生。

更安全的方式是使用以下形式来进行类型断言:
if v, ok := varI.(T); ok { //已检查类型断言
    Process(v)
    return
}
//varI不是类型T
如果转换合法,v 是 varI 转换到类型 T 的值,ok 会是 true;否则 v 是类型 T 的零值,ok 是 false,也没有发生运行时错误。应该使用这种方式来进行类型断言。

在多数情况下,可能只是想在 if 中测试 ok 的值,此时使用以下方法会是最方便的:
if _, ok := varI.(T); ok {
}
例如:
package main
import (
    "fmt"
    "math"
)
type Square struct {
    side float32
}
type Circle struct {
    radius float32
}
type Shaper interface {
    Area() float32
}
func main() {
   var areaIntf Shaper
   sq1 := new(Square)
   sq1.side= 5
   areaIntf= sq1
   //判断areaIntf的类型是否是Square
   if t, ok := areaIntf. (*Square); ok {
       fmt.Printf("areaIntf的类型是: %T\n",t)
   }
   if u, ok := areaIntf.(*Circle); ok{
       fmt.Printf("areaIntf的类型是: %T\n", u)
   }else {
       fmt.Println("areaIntf不含类型为Circle的变量")
   }
}
func (sq *Square) Area() float32 {
    return sq.side * sq.side
}
func (ci *Circle) Area() float32 {
    return ci.radius * ci.radius * math. Pi
}
运行结果为:

areaIntf的类型是: *main.Square
areaIntf不含类型为Circle的变量

程序行中定义了一个新类型 Circle,它也实现了 Shaper 接口。第一个 if 语句测试 areaIntf 中是否包含一个 Square 类型的变量,返回的 ok 为 true,则表示包含该类型;第二个 if 语句测试它是否包含一个 Circle 类型的变量,返回的 OK 结果为 false,所以不包含该类型。

如果忽略 areaIntf.(*Square) 中的 * 号,会出现编译错误:
impossible type assertion: Square does not implement Shaper (Area method has pointer receiver)
这是因为 Go 语言编译器无法自动推断类型,Area() 方法通过指针接收器传入参数。

2、静态类型

接口被定义时,其类型就已经被确定,这个类型称为接口的静态类型。接口的静态类型在其定义时就被确定,静态类型的本质特征就是接口的方法签名集合。

两个接口如果方法签名集合相同(方法的顺序可以不同),则这两个接口在语义上完全等价,它们之间不需要强制类型转换就可以相互赋值。原因是 Go 语言编译器校验接口是否能赋值,是比较二者的方法集,而不是看具体接口类型名。

例如,a 接口的法集为 A,b 接口的法集为 B,如果 B 是 A 的子集合,则 a 的接口变量可以直接赋值给 B 的接口变量。反之,则需要用到接口类型断言。

3、类型判断

接口变量的类型可以使用 type-switch 来检测:
package main
import (
   "fmt"
   "math"
)
type Square struct {
   side float32
}
type Circle struct {
   radius float32
}
type Shaper interface {
   Area() float32
}
func main() {
   var areaIntf Shaper
   sq1 := new(Square)
   sq1.side = 5
   areaIntf = sq1
   switch t := areaIntf.(type) {
       case *Square:
           fmt.Printf("Square类型的%T值为: %v\n", t, t)
       case *Circle:
           fmt.Printf("Circle类型的%T值为: %v\n", t, t)
       case nil:
           fmt.Printf("ni1值:发生了意外.\n")
       default:
           fmt.Printf("未知类型%T\n", t)
   }
}
func (sq *Square) Area() float32 {
   return sq.side * sq.side
}
func (ci *Circle) Area() float32 {
   return ci.radius * ci.radius * math.Pi
}
运行结果如下:
Square类型的*main.Square值为: &{5}
变量 t 得到了 areaIntf 的值和类型,所有 case 语句中列举的类型(nil 除外)都必须实现对应的接口,如果被检测类型没有在 case 语句列举的类型中,就会执行 default 语句。

可以用 type-switch 进行运行时类型分析,但是 type-switch 不允许有 fallthrough。如果仅仅是测试变量的类型,不用它的值,那么就不需要赋值语句,例如:
switch areaIntf. (type) {
    case *Square:
    case *Circle:
    ...
    default:
}
以下代码展示了一个类型分类函数,它有一个可变长度参数。可以是任意类型的数组,它会根据数组元素的实际类型执行不同的动作。
func classfier(items … interface{}) {
    for i, x := range items {
       switch x.(type) {
       case bool :
           fmt.Printf("参数#%d类型是bool\n", i)
       case float64:
           fmt.Printf("参数#%d类型是float64\n", i)
       case int, int64:
           fmt.Printf("参数#%d类型是int\n", i)
       case nil:
           fmt.Printf("参数#%d类型是nil\n", i)
       case string:
           fmt.Printf("参数#%d类型是string\n", i)
       default:
           fmt.Printf("参数#%d类型未知\n", i)
       }
    }
}
可以这样调用此方法:
classifier(13, -14.3, "BELGIUM", complex(1,2), nil, false)
在处理来自外部的、类型未知的数据时,如解析诸如 JSON 或 XML 编码的数据,类型测试和转换非常有用。

推荐阅读