接口:为什么nil接口不等于nil?|青训营笔记

117 阅读3分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记

接口的静态特性与动态特性接口的静态特性体现在接口类型变量具有静态类型,比如var err error中变量 err 的静态类型为 error。拥有静态类型,那就意味着编译器会在编译阶段对所有接口类型变量的赋值操作进行类型检查,编译器会检查右值的类型是否实现了该接口方法集合中的所有方法。如果不满足,就会报错 var err error = 1 // cannot use 1 (type int) as type error in assignment: int does not implement error (missing Error method)

而接口的动态特性,就体现在接口类型变量在运行时还存储了右值的真实类型信息,这个右值的真实类型被称为接口类型变量的动态类型。你看一下下面示例代码: var err error err = errors.New("error1") fmt.Printf("%T\n", err) // *errors.errorString

我们可以看到,这个示例通过 errros.New 构造了一个错误值,赋值给了 error 接口类型变量 err,并通过 fmt.Printf 函数输出接口类型变量 err 的动态类型为 *errors.errorString。那接口的这种“动静皆备”的特性,又带来了什么好处呢?首先,接口类型变量在程序运行时可以被赋值为不同的动态类型变量,每次赋值后,接口类型变量中存储的动态类型信息都会发生变化,这让 Go 语言可以像动态语言(比如 Python)那样拥有使用Duck Typing(鸭子类型)的灵活性。所谓鸭子类型,就是指某类型所表现出的特性(比如是否可以作为某接口类型的右值),不是由其基因(比如 C++ 中的父类)决定的,而是由类型所表现出来的行为(比如类型拥有的方法)决定的。 比如下面的例子` type QuackableAnimal interface { Quack() }

type Duck struct{}

func (Duck) Quack() { println("duck quack!") }

type Dog struct{}

func (Dog) Quack() { println("dog quack!") }

type Bird struct{}

func (Bird) Quack() { println("bird quack!") }

func AnimalQuackInForest(a QuackableAnimal) { a.Quack()
}

func main() {
animals := []QuackableAnimal{new(Duck), new(Dog), new(Bird)} for _, animal := range animals { AnimalQuackInForest(animal) }
}:` 这个例子中,我们用接口类型 QuackableAnimal 来代表具有“会叫”这一特征的动物,而 Duck、Bird 和 Dog 类型各自都具有这样的特征,于是我们可以将这三个类型的变量赋值给 QuackableAnimal 接口类型变量 a。每次赋值,变量 a 中存储的动态类型信息都不同,Quack 方法的执行结果将根据变量 a 中存储的动态类型信息而定。这里的 Duck、Bird、Dog 都是“鸭子类型”,但它们之间并没有什么联系,之所以能作为右值赋值给 QuackableAnimal 类型变量,只是因为他们表现出了 QuackableAnimal 所要求的特征罢了。不过,与动态语言不同的是,Go 接口还可以保证“动态特性”使用时的安全性。比如,编译器在编译期就可以捕捉到将 int 类型变量传给 QuackableAnimal 接口类型变量这样的明显错误,决不会让这样的错误遗漏到运行时才被发现。接口类型的动静特性让我们看到了接口类型的强大,但在日常使用过程中,很多人都会产生各种困惑,其中最经典的一个困惑莫过于“nil 的 error 值不等于 nil”了。下面我们来详细看一下。nil error 值 != nil