很多面向对象的语言都有接口这个概念。Go 语言的接口的独特之处在于它是隐式实现。换句话说,对于一个具体的类型,无序声明它实现了哪些接口,只要提供接口所必需的方法即可。
接口类型
一个接口类型定义了一套方法,如果一个具体类型要实现该接口,那么必须实现接口类型定义的所有方法。
实现接口
如果一个类型实现了一个接口所要求的所有方法,那么这个类型就实现了这个接口。
var w io.Writer
w = os.Stdout // OK: *os.File 有 Write 方法
w = new(bytes.Buffer) // OK: *bytes.Buffer 有 Write 方法
w = time.Second // 编译错误: time.Duration 缺少 write 方法
var rwc io.ReadWriteCloser
rwc = os.Stdout // OK: *os.File 有 Read、Write、Close 方法
rwc = new(bytes.Buffer) // 编译错误: *bytes.Buffer 缺少 Close 方法
// 当右侧表达式也是一个接口时,该规则也有效:
w = rwc
rwc = w // 编译错误:io.Writer 缺少 Close 方法
接口值
从概念上来讲,一个接口类型的值(简称接口值)其实有两个部分:一个具体类型和该类型的一个值。二者称为接口的动态类型和动态值。
接口值可以用 == 和 != 操作符来做比较。如果两个接口值都是 nil 或者二者的动态类型完全一致且二者动态值相等(使用动态类型的 == 操作符来做比较),那么两个接口值相等。因为接口值是可以比较的,所以它们可以作为 map 的键,也可以作为 switch 语句的操作数。
需要注意的是,在比较两个接口值时,如果两个接口值的动态类型一致,但对应的动态值是不可比较的(比如 slice),那么这个比较会以崩溃的方式失败:
var x interface{} = []int{1,2,3}
fmt.Println(x == x) // 宕机:试图比较不可比较的类型 []int
类型断言
类型断言是一个作用在接口值上的操作,写出来类似于 x.(T),其中 x 是一个接口类型的表达式,而 T 是一个类型(称为断言类型)。类型断言会检查作为操作数的动态类型是否满足指定的断言类型。
这儿有两个可能。首先,如果断言类型T是一个具体类型,那么类型断言会检查 x 的动态类型是否就是 T。如果检查成功,类型断言的结果就是 x 的动态值,类型当然就是T。换句话说,类型断言就是用来从它的操作数中把具体的类型提取出来的操作。
var w io.Writer
w = os.Stdout
f := w.(*os.File) // 成功: f == os.Stdout
c := w.(*bytes.Buffer) // 崩溃:接口持有的是 *os.File, 不是 *bytes.Buffer
其次,如果断言类型T 是一个接口类型,那么类型断言检查 x 的动态类型是否满足 T。如果检查成功,动态值并没有提取出来,结果仍然是一个接口值。
使用类型断言来识别错误
考虑一下os 包中的文件操作返回的错误集合, I/O 会因为很多原因失败,但有三类原因通常必须单独处理:文件已存储(创建操作),文件没找到(读取操作)以及权限不足。os 包提供了三个帮助函数用来对错误进行分类:
package os
func IsExist(err error) bool
func IsNotExist(err error) bool
func IsPermission(err error) bool
func IsNotExist(err error) bool {
// 注意:不健壮
return strings.Contains(err.Error(), "file dose not exist")
}
但由于处理 I/O 错误的逻辑会随着平台的变化而变化,因此这种方式很不健壮,同样的错误可能会用完全不同的错误消息来报告。
一个更可靠的方法是用专门的类型来表示结构化的错误值。os 包定义了一个 PathError 类型来表示在与一个文件路径相关的操作上发生错误(比如 Open 或者 Delete),一个类似的 LinkError 用来表述在于两个文件路径相关的操作上发生的错误(比如 Symlink 和 Rename).
var ErrNotExist = errors.New("file dose not exist")
// IsNotExist 返回一个布尔值,该值表明错误是否代表文件或目录不存在
func IsNotExist(err error) bool {
if pe, ok := err.(*os.PathError); ok {
err = pe.Err
}
return err == syscall.ENOENT || err == ErrNotExist
}
类型分支
类型分支的最简单形式与普通分支语句类似,两个的差别是操作数改为 x.(type)
var x interface{}
switch x.(type){
case nil:
case int, uint:
case bool:
case string:
default:
}
与普通的 switch 语句类似,分支是按顺序来判定的,当一个分支符合时,对应的代码会执行。分支的顺序在一个或多个接口类型时变得很重要,因为有可能两个分支都能满足。default 分支的位置无关紧要。另外,类型分支不允许使用 fallthrough。