gopl-zh.github.com/ch7/ch7-05.md
2022-08-04 14:58:52 +08:00

165 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 7.5. 接口值
概念上讲一个接口的值接口值由两个部分组成一个具体的类型和那个类型的值。它们被称为接口的动态类型和动态值。对于像Go语言这种静态类型的语言类型是编译期的概念因此一个类型不是一个值。在我们的概念模型中一些提供每个类型信息的值被称为类型描述符比如类型的名称和方法。在一个接口值中类型部分代表与之相关类型的描述符。
下面4个语句中变量w得到了3个不同的值。开始和最后的值是相同的
```go
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil
```
让我们进一步观察在每一个语句后的w变量的值和动态行为。第一个语句定义了变量w:
```go
var w io.Writer
```
在Go语言中变量总是被一个定义明确的值初始化即使接口类型也不例外。对于一个接口的零值就是它的类型和值的部分都是nil图7.1)。
![](../images/ch7-01.png)
一个接口值基于它的动态类型被描述为空或非空所以这是一个空的接口值。你可以通过使用w==nil或者w!=nil来判断接口值是否为空。调用一个空接口值上的任意方法都会产生panic:
```go
w.Write([]byte("hello")) // panic: nil pointer dereference
```
第二个语句将一个`*os.File`类型的值赋给变量w:
```go
w = os.Stdout
```
这个赋值过程调用了一个具体类型到接口类型的隐式转换这和显式的使用io.Writer(os.Stdout)是等价的。这类转换不管是显式的还是隐式的,都会刻画出操作到的类型和值。这个接口值的动态类型被设为`*os.File`指针的类型描述符它的动态值持有os.Stdout的拷贝这是一个代表处理标准输出的os.File类型变量的指针图7.2)。
![](../images/ch7-02.png)
调用一个包含`*os.File`类型指针的接口值的Write方法使得`(*os.File).Write`方法被调用。这个调用输出“hello”。
```go
w.Write([]byte("hello")) // "hello"
```
通常在编译期我们不知道接口值的动态类型是什么所以一个接口上的调用必须使用动态分配。因为不是直接进行调用所以编译器必须把代码生成在类型描述符的方法Write上然后间接调用那个地址。这个调用的接收者是一个接口动态值的拷贝os.Stdout。效果和下面这个直接调用一样
```go
os.Stdout.Write([]byte("hello")) // "hello"
```
第三个语句给接口值赋了一个*bytes.Buffer类型的值
```go
w = new(bytes.Buffer)
```
现在动态类型是*bytes.Buffer并且动态值是一个指向新分配的缓冲区的指针图7.3)。
![](../images/ch7-03.png)
Write方法的调用也使用了和之前一样的机制
```go
w.Write([]byte("hello")) // writes "hello" to the bytes.Buffers
```
这次类型描述符是\*bytes.Buffer所以调用了(\*bytes.Buffer).Write方法并且接收者是该缓冲区的地址。这个调用把字符串“hello”添加到缓冲区中。
最后第四个语句将nil赋给了接口值
```go
w = nil
```
这个重置将它所有的部分都设为nil值把变量w恢复到和它之前定义时相同的状态在图7.1中可以看到。
一个接口值可以持有任意大的动态值。例如表示时间实例的time.Time类型这个类型有几个对外不公开的字段。我们从它上面创建一个接口值
```go
var x interface{} = time.Now()
```
结果可能和图7.4相似。从概念上讲,不论接口值多大,动态值总是可以容下它。(这只是一个概念上的模型;具体的实现可能会非常不同)
![](../images/ch7-04.png)
接口值可以使用==和!来进行比较。两个接口值相等仅当它们都是nil值或者它们的动态类型相同并且动态值也根据这个动态类型的==操作相等。因为接口值是可比较的所以它们可以用在map的键或者作为switch语句的操作数。
然而如果两个接口值的动态类型相同但是这个动态类型是不可比较的比如切片将它们进行比较就会失败并且panic:
```go
var x interface{} = []int{1, 2, 3}
fmt.Println(x == x) // panic: comparing uncomparable type []int
```
考虑到这点接口类型是非常与众不同的。其它类型要么是安全的可比较类型如基本类型和指针要么是完全不可比较的类型如切片映射类型和函数但是在比较接口值或者包含了接口值的聚合类型时我们必须要意识到潜在的panic。同样的风险也存在于使用接口作为map的键或者switch的操作数。只能比较你非常确定它们的动态值是可比较类型的接口值。
当我们处理错误或者调试的过程中得知接口值的动态类型是非常有帮助的。所以我们使用fmt包的%T动作:
```go
var w io.Writer
fmt.Printf("%T\n", w) // "<nil>"
w = os.Stdout
fmt.Printf("%T\n", w) // "*os.File"
w = new(bytes.Buffer)
fmt.Printf("%T\n", w) // "*bytes.Buffer"
```
在fmt包内部使用反射来获取接口动态类型的名称。我们会在第12章中学到反射相关的知识。
### 7.5.1. 警告一个包含nil指针的接口不是nil接口
一个不包含任何值的nil接口值和一个刚好包含nil指针的接口值是不同的。这个细微区别产生了一个容易绊倒每个Go程序员的陷阱。
思考下面的程序。当debug变量设置为true时main函数会将f函数的输出收集到一个bytes.Buffer类型中。
```go
const debug = true
func main() {
var buf *bytes.Buffer
if debug {
buf = new(bytes.Buffer) // enable collection of output
}
f(buf) // NOTE: subtly incorrect!
if debug {
// ...use buf...
}
}
// If out is non-nil, output will be written to it.
func f(out io.Writer) {
// ...do something...
if out != nil {
out.Write([]byte("done!\n"))
}
}
```
我们可能会预计当把变量debug设置为false时可以禁止对输出的收集但是实际上在out.Write方法调用时程序发生了panic
```go
if out != nil {
out.Write([]byte("done!\n")) // panic: nil pointer dereference
}
```
当main函数调用函数f时它给f函数的out参数赋了一个\*bytes.Buffer的空指针所以out的动态值是nil。然而它的动态类型是\*bytes.Buffer意思就是out变量是一个包含空指针值的非空接口如图7.5所以防御性检查out!=nil的结果依然是true。
![](../images/ch7-05.png)
动态分配机制依然决定(\*bytes.Buffer).Write的方法会被调用但是这次的接收者的值是nil。对于一些如\*os.File的类型nil是一个有效的接收者§6.2.1),但是\*bytes.Buffer类型不在这些种类中。这个方法会被调用但是当它尝试去获取缓冲区时会发生panic。
问题在于尽管一个nil的\*bytes.Buffer指针有实现这个接口的方法它也不满足这个接口具体的行为上的要求。特别是这个调用违反了(\*bytes.Buffer).Write方法的接收者非空的隐含先觉条件所以将nil指针赋给这个接口是错误的。解决方案就是将main函数中的变量buf的类型改为io.Writer因此可以避免一开始就将一个不完整的值赋值给这个接口
```go
var buf io.Writer
if debug {
buf = new(bytes.Buffer) // enable collection of output
}
f(buf) // OK
```
现在我们已经把接口值的技巧都讲完了让我们来看更多的一些在Go标准库中的重要接口类型。在下面的三章中我们会看到接口类型是怎样用在排序web服务错误处理中的。