mirror of
https://github.com/unknwon/the-way-to-go_ZH_CN.git
synced 2025-08-11 19:41:43 +08:00
@@ -1,4 +1,4 @@
|
||||
# 11.0 接口(interface)与反射(reflection)
|
||||
# 11.0 接口 (interface)与反射 (reflection)
|
||||
|
||||
本章介绍 Go 语言中接口和反射的相关内容。
|
||||
|
||||
|
@@ -96,7 +96,7 @@ func main() {
|
||||
cannot use sq1 (type *Square) as type Shaper in assignment:
|
||||
*Square does not implement Shaper (missing Area method)
|
||||
|
||||
如果 `Shaper` 有另外一个方法 `Perimeter()`,但是`Square` 没有实现它,即使没有人在 `Square` 实例上调用这个方法,编译器也会给出上面同样的错误。
|
||||
如果 `Shaper` 有另外一个方法 `Perimeter()`,但是 `Square` 没有实现它,即使没有人在 `Square` 实例上调用这个方法,编译器也会给出上面同样的错误。
|
||||
|
||||
扩展一下上面的例子,类型 `Rectangle` 也实现了 `Shaper` 接口。接着创建一个 `Shaper` 类型的数组,迭代它的每一个元素并在上面调用 `Area()` 方法,以此来展示多态行为:
|
||||
|
||||
@@ -150,7 +150,7 @@ func main() {
|
||||
Shape details: &{5}
|
||||
Area of this shape is: 25
|
||||
|
||||
在调用 `shapes[n].Area() ` 这个时,只知道 `shapes[n]` 是一个 `Shaper` 对象,最后它摇身一变成为了一个 `Square` 或 `Rectangle` 对象,并且表现出了相对应的行为。
|
||||
在调用 `shapes[n].Area()` 这个时,只知道 `shapes[n]` 是一个 `Shaper` 对象,最后它摇身一变成为了一个 `Square` 或 `Rectangle` 对象,并且表现出了相对应的行为。
|
||||
|
||||
也许从现在开始你将看到通过接口如何产生 **更干净**、**更简单** 及 **更具有扩展性** 的代码。在 11.12.3 中将看到在开发中为类型添加新的接口是多么的容易。
|
||||
|
||||
@@ -236,21 +236,20 @@ type Reader interface {
|
||||
|
||||
有的时候,也会以一种稍微不同的方式来使用接口这个词:从某个类型的角度来看,它的接口指的是:它的所有导出方法,只不过没有显式地为这些导出方法额外定一个接口而已。
|
||||
|
||||
**练习 11.1** simple_interface.go:
|
||||
**练习 11.1** [simple_interface.go](exercises\chapter_11\simple_interface.go):
|
||||
|
||||
定义一个接口 `Simpler`,它有一个 `Get()` 方法和一个 `Set()`,`Get()`返回一个整型值,`Set()` 有一个整型参数。创建一个结构体类型 `Simple` 实现这个接口。
|
||||
定义一个接口 `Simpler`,它有一个 `Get()` 方法和一个 `Set()`,`Get()` 返回一个整型值,`Set()` 有一个整型参数。创建一个结构体类型 `Simple` 实现这个接口。
|
||||
|
||||
接着定一个函数,它有一个 `Simpler` 类型的参数,调用参数的 `Get()` 和 `Set()` 方法。在 `main` 函数里调用这个函数,看看它是否可以正确运行。
|
||||
|
||||
**练习 11.2** interfaces_poly2.go:
|
||||
**练习 11.2** [interfaces_poly2.go](exercises\chapter_11\interfaces_poly2.go):
|
||||
|
||||
a) 扩展 interfaces_poly.go 中的例子,添加一个 `Circle` 类型
|
||||
a) 扩展 [interfaces_poly.go](exercises\chapter_11\interfaces_poly.go) 中的例子,添加一个 `Circle` 类型
|
||||
|
||||
b) 使用一个抽象类型 `Shape`(没有字段) 实现同样的功能,它实现接口 `Shaper`,然后在其他类型里内嵌此类型。扩展 10.6.5 中的例子来说明覆写。
|
||||
b) 使用一个抽象类型 `Shape`(没有字段) 实现同样的功能,它实现接口 `Shaper`,然后在其他类型里内嵌此类型。扩展 [10.6.5](10.6.md) 中的例子来说明覆写。
|
||||
|
||||
## 链接
|
||||
|
||||
- [目录](directory.md)
|
||||
- 上一节:[接口(Interfaces)与反射(reflection)](11.0.md)
|
||||
- 上一节:[接口 (Interfaces) 与反射 (reflection)](11.0.md)
|
||||
- 下一节:[接口嵌套接口](11.2.md)
|
||||
|
||||
|
@@ -2,8 +2,7 @@
|
||||
|
||||
## 11.10.1 方法和类型的反射
|
||||
|
||||
在 10.4 节我们看到可以通过反射来分析一个结构体。本节我们进一步探讨强大的反射功能。反射是用程序检查其所拥有的结构,尤其是类型的一种能力;这是元编程的一种形式。反射可以在运行时检查类型和变量,例如它的大小、方法和 `动态`
|
||||
的调用这些方法。这对于没有源代码的包尤其有用。这是一个强大的工具,除非真得有必要,否则应当避免使用或小心使用。
|
||||
在 [10.4](10.4.md) 节我们看到可以通过反射来分析一个结构体。本节我们进一步探讨强大的反射功能。反射是用程序检查其所拥有的结构,尤其是类型的一种能力;这是元编程的一种形式。反射可以在运行时检查类型和变量,例如:它的大小、它的方法以及它能“动态地”调用这些方法。这对于没有源代码的包尤其有用。这是一个强大的工具,除非真得有必要,否则应当避免使用或小心使用。
|
||||
|
||||
变量的最基本信息就是类型和值:反射包的 `Type` 用来表示一个 Go 类型,反射包的 `Value` 为 Go 值提供了反射接口。
|
||||
|
||||
@@ -20,7 +19,7 @@ func ValueOf(i interface{}) Value
|
||||
|
||||
反射可以从接口值反射到对象,也可以从对象反射回接口值。
|
||||
|
||||
reflect.Type 和 reflect.Value 都有许多方法用于检查和操作它们。一个重要的例子是 Value 有一个 Type 方法返回 reflect.Value 的 Type。另一个是 Type 和 Value 都有 Kind 方法返回一个常量来表示类型:Uint、Float64、Slice 等等。同样 Value 有叫做 Int 和 Float 的方法可以获取存储在内部的值(跟 int64 和 float64 一样)
|
||||
`reflect.Type` 和 `reflect.Value` 都有许多方法用于检查和操作它们。一个重要的例子是 `Value` 有一个 `Type()` 方法返回 `reflect.Value` 的 `Type` 类型。另一个是 `Type` 和 `Value` 都有 `Kind()` 方法返回一个常量来表示类型:`Uint`、`Float64`、`Slice` 等等。同样 `Value` 有叫做 `Int()` 和 `Float()` 的方法可以获取存储在内部的值(跟 `int64` 和 `float64` 一样)
|
||||
|
||||
```go
|
||||
const (
|
||||
@@ -54,10 +53,9 @@ const (
|
||||
)
|
||||
```
|
||||
|
||||
对于 float64 类型的变量 x,如果 `v:=reflect.ValueOf(x)`,那么 `v.Kind()` 返回 `reflect.Float64` ,所以下面的表达式是 `true`
|
||||
`v.Kind() == reflect.Float64`
|
||||
对于 `float64` 类型的变量 `x`,如果 `v:=reflect.ValueOf(x)`,那么 `v.Kind()` 返回 `reflect.Float64` ,所以下面的表达式是 `true`:`v.Kind() == reflect.Float64`
|
||||
|
||||
Kind 总是返回底层类型:
|
||||
`Kind()` 总是返回底层类型:
|
||||
|
||||
```go
|
||||
type MyInt int
|
||||
@@ -67,7 +65,7 @@ v := reflect.ValueOf(m)
|
||||
|
||||
方法 `v.Kind()` 返回 `reflect.Int`。
|
||||
|
||||
变量 v 的 `Interface()` 方法可以得到还原(接口)值,所以可以这样打印 v 的值:`fmt.Println(v.Interface())`
|
||||
变量 `v` 的 `Interface()` 方法可以得到还原(接口)值,所以可以这样打印 `v` 的值:`fmt.Println(v.Interface())`
|
||||
|
||||
|
||||
尝试运行下面的代码:
|
||||
@@ -111,25 +109,25 @@ value is 3.40e+00
|
||||
3.4
|
||||
```
|
||||
|
||||
x 是一个 float64 类型的值,`reflect.ValueOf(x).Float()` 返回这个 float64 类型的实际值;同样的适用于 `Int(), Bool(), Complex(), String()`
|
||||
`x` 是一个 `float64` 类型的值,`reflect.ValueOf(x).Float()` 返回这个 `float64` 类型的实际值;同样的适用于 `Int(), Bool(), Complex(), String()`
|
||||
|
||||
## 11.10.2 通过反射修改(设置)值
|
||||
## 11.10.2 通过反射修改(设置)值
|
||||
|
||||
继续前面的例子(参阅 11.9 [reflect2.go](examples/chapter_11/reflect2.go)),假设我们要把 x 的值改为 3.1415。Value 有一些方法可以完成这个任务,但是必须小心使用:`v.SetFloat(3.1415)`。
|
||||
继续前面的例子(参阅 11.9 [reflect2.go](examples/chapter_11/reflect2.go)),假设我们要把 `x` 的值改为 `3.1415`。`Value` 有一些方法可以完成这个任务,但是必须小心使用:`v.SetFloat(3.1415)`。
|
||||
|
||||
这将产生一个错误:`reflect.Value.SetFloat using unaddressable value`。
|
||||
|
||||
为什么会这样呢?问题的原因是 v 不是可设置的(这里并不是说值不可寻址)。是否可设置是 Value 的一个属性,并且不是所有的反射值都有这个属性:可以使用 `CanSet()` 方法测试是否可设置。
|
||||
为什么会这样呢?问题的原因是 `v` 不是可设置的(这里并不是说值不可寻址)。是否可设置是 `Value` 的一个属性,并且不是所有的反射值都有这个属性:可以使用 `CanSet()` 方法测试是否可设置。
|
||||
|
||||
在例子中我们看到 `v.CanSet()` 返回 false: `settability of v: false`
|
||||
在例子中我们看到 `v.CanSet()` 返回 `false`: `settability of v: false`
|
||||
|
||||
当 `v := reflect.ValueOf(x)` 函数通过传递一个 x 拷贝创建了 v,那么 v 的改变并不能更改原始的 x。要想 v 的更改能作用到 x,那就必须传递 x 的地址 `v = reflect.ValueOf(&x)`。
|
||||
当 `v := reflect.ValueOf(x)` 函数通过传递一个 `x` 拷贝创建了 `v`,那么 `v` 的改变并不能更改原始的 `x`。要想 `v` 的更改能作用到 `x`,那就必须传递 x 的地址 `v = reflect.ValueOf(&x)`。
|
||||
|
||||
通过 Type() 我们看到 v 现在的类型是 `*float64` 并且仍然是不可设置的。
|
||||
通过 `Type()` 我们看到 `v` 现在的类型是 `*float64` 并且仍然是不可设置的。
|
||||
|
||||
要想让其可设置我们需要使用 `Elem()` 函数,这间接的使用指针:`v = v.Elem()`
|
||||
要想让其可设置我们需要使用 `Elem()` 函数,这间接地使用指针:`v = v.Elem()`
|
||||
|
||||
现在 `v.CanSet()` 返回 true 并且 `v.SetFloat(3.1415)` 设置成功了!
|
||||
现在 `v.CanSet()` 返回 `true` 并且 `v.SetFloat(3.1415)` 设置成功了!
|
||||
|
||||
|
||||
示例 11.12 [reflect2.go](examples/chapter_11/reflect2.go):
|
||||
@@ -176,9 +174,9 @@ settability of v: true
|
||||
|
||||
## 11.10.3 反射结构
|
||||
|
||||
有些时候需要反射一个结构类型。`NumField()` 方法返回结构内的字段数量;通过一个 for 循环用索引取得每个字段的值 `Field(i)`。
|
||||
有些时候需要反射一个结构类型。`NumField()` 方法返回结构内的字段数量;通过一个 `for` 循环用索引取得每个字段的值 `Field(i)`。
|
||||
|
||||
我们同样能够调用签名在结构上的方法,例如,使用索引 n 来调用:`Method(n).Call(nil)`。
|
||||
我们同样能够调用签名在结构上的方法,例如,使用索引 `n` 来调用:`Method(n).Call(nil)`。
|
||||
|
||||
示例 11.13 [reflect_struct.go](examples/chapter_11/reflect_struct.go):
|
||||
|
||||
|
@@ -1,16 +1,16 @@
|
||||
# 11.11 Printf 和反射
|
||||
# 11.11 Printf() 和反射
|
||||
|
||||
在 Go 语言的标准库中,前几节所述的反射的功能被大量地使用。举个例子,fmt 包中的 Printf(以及其他格式化输出函数)都会使用反射来分析它的 `...` 参数。
|
||||
在 Go 语言的标准库中,前几节所述的反射的功能被大量地使用。举个例子,`fmt` 包中的 `Printf()`(以及其他格式化输出函数)都会使用反射来分析它的 `...` 参数。
|
||||
|
||||
Printf 的函数声明为:
|
||||
`Printf()` 的函数声明为:
|
||||
|
||||
```go
|
||||
func Printf(format string, args ... interface{}) (n int, err error)
|
||||
```
|
||||
|
||||
Printf 中的 `...` 参数为空接口类型。Printf 使用反射包来解析这个参数列表。所以,Printf 能够知道它每个参数的类型。因此格式化字符串中只有 %d 而没有 %u 和 %ld,因为它知道这个参数是 unsigned 还是 long。这也是为什么 Print 和 Println 在没有格式字符串的情况下还能如此漂亮地输出。
|
||||
`Printf()` 中的 `...` 参数为空接口类型。`Printf()` 使用反射包来解析这个参数列表。所以,`Printf()` 能够知道它每个参数的类型。因此格式化字符串中只有 `%d` 而没有 `%u` 和 `%ld`,因为它知道这个参数是 unsigned 还是 long。这也是为什么 `Print()` 和 `Println()` 在没有格式字符串的情况下还能如此漂亮地输出。
|
||||
|
||||
为了让大家更加具体地了解 Printf 中的反射,我们实现了一个简单的通用输出函数。其中使用了 type-switch 来推导参数类型,并根据类型来输出每个参数的值(这里用了 10.7 节中练习 10.13 的部分代码)
|
||||
为了让大家更加具体地了解 `Printf()` 中的反射,我们实现了一个简单的通用输出函数。其中使用了 type-switch 来推导参数类型,并根据类型来输出每个参数的值(这里用了 [10.7](10.7.md) 节中练习 10.13 的部分代码)
|
||||
|
||||
示例 11.15 [print.go](examples/chapter_11/print.go):
|
||||
|
||||
@@ -58,7 +58,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
在 12.8 节中我们将阐释 `fmt.Fprintf()` 是怎么运用同样的反射原则的。
|
||||
在 [12.8](12.8.md) 节中我们将阐释 `fmt.Fprintf()` 是怎么运用同样的反射原则的。
|
||||
|
||||
## 链接
|
||||
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
## 11.12.1 Go 的动态类型
|
||||
|
||||
在经典的面向对象语言(像 C++,Java 和 C#)中数据和方法被封装为 `类` 的概念:类包含它们两者,并且不能剥离。
|
||||
在经典的面向对象语言(像 C++,Java 和 C#)中数据和方法被封装为*类*的概念:类包含它们两者,并且不能剥离。
|
||||
|
||||
Go 没有类:数据(结构体或更一般的类型)和方法是一种松耦合的正交关系。
|
||||
|
||||
@@ -10,11 +10,11 @@ Go 中的接口跟 Java/C# 类似:都是必须提供一个指定方法集的
|
||||
|
||||
和其它语言相比,Go 是唯一结合了接口值,静态类型检查(是否该类型实现了某个接口),运行时动态转换的语言,并且不需要显式地声明类型是否满足某个接口。该特性允许我们在不改变已有的代码的情况下定义和使用新接口。
|
||||
|
||||
接收一个(或多个)接口类型作为参数的函数,其**实参**可以是任何实现了该接口的类型的变量。 `实现了某个接口的类型可以被传给任何以此接口为参数的函数` 。
|
||||
接收一个(或多个)接口类型作为参数的函数,其**实参**可以是任何实现了该接口的类型的变量。 *实现了某个接口的类型可以被传给任何以此接口为参数的函数*。
|
||||
|
||||
类似于 Python 和 Ruby 这类动态语言中的 `动态类型(duck typing)`;这意味着对象可以根据提供的方法被处理(例如,作为参数传递给函数),而忽略它们的实际类型:它们能做什么比它们是什么更重要。
|
||||
类似于 Python 和 Ruby 这类动态语言中的动态类型 (duck typing);这意味着对象可以根据提供的方法被处理(例如,作为参数传递给函数),而忽略它们的实际类型:它们能做什么比它们是什么更重要。
|
||||
|
||||
这在程序 [duck_dance.go](examples/chapter_11/duck_dance.go) 中得以阐明,函数 DuckDance 接受一个 IDuck 接口类型变量。仅当 DuckDance 被实现了 IDuck 接口的类型调用时程序才能编译通过。
|
||||
这在程序 [duck_dance.go](examples/chapter_11/duck_dance.go) 中得以阐明,函数 `DuckDance()` 接受一个 `IDuck` 接口类型变量。仅当 `DuckDance()` 被实现了 `IDuck` 接口的类型调用时程序才能编译通过。
|
||||
|
||||
示例 11.16 [duck_dance.go](examples/chapter_11/duck_dance.go):
|
||||
|
||||
@@ -77,7 +77,7 @@ cannot use b (type *Bird) as type IDuck in function argument:
|
||||
|
||||
像 Python,Ruby 这类语言,动态类型是延迟绑定的(在运行时进行):方法只是用参数和变量简单地调用,然后在运行时才解析(它们很可能有像 `responds_to` 这样的方法来检查对象是否可以响应某个方法,但是这也意味着更大的编码量和更多的测试工作)
|
||||
|
||||
Go 的实现与此相反,通常需要编译器静态检查的支持:当变量被赋值给一个接口类型的变量时,编译器会检查其是否实现了该接口的所有函数。如果方法调用作用于像 `interface{}` 这样的“泛型”上,你可以通过类型断言(参见 11.3 节)来检查变量是否实现了相应接口。
|
||||
Go 的实现与此相反,通常需要编译器静态检查的支持:当变量被赋值给一个接口类型的变量时,编译器会检查其是否实现了该接口的所有函数。如果方法调用作用于像 `interface{}` 这样的“泛型”上,你可以通过类型断言(参见 [11.3](11.3.md) 节)来检查变量是否实现了相应接口。
|
||||
|
||||
例如,你用不同的类型表示 XML 输出流中的不同实体。然后我们为 XML 定义一个如下的“写”接口(甚至可以把它定义为私有接口):
|
||||
|
||||
@@ -87,7 +87,7 @@ type xmlWriter interface {
|
||||
}
|
||||
```
|
||||
|
||||
现在我们可以实现适用于该流类型的任何变量的 `StreamXML` 函数,并用类型断言检查传入的变量是否实现了该接口;如果没有,我们就调用内建的 `encodeToXML` 来完成相应工作:
|
||||
现在我们可以实现适用于该流类型的任何变量的 `StreamXML()` 函数,并用类型断言检查传入的变量是否实现了该接口;如果没有,我们就调用内建的 `encodeToXML()` 来完成相应工作:
|
||||
|
||||
```go
|
||||
// Exported XML streaming function.
|
||||
@@ -112,15 +112,15 @@ Go 在这里用了和 `gob` 相同的机制:定义了两个接口 `GobEncoder`
|
||||
|
||||
对于动态语言非常重要的单元测试来说,这样即可以减少单元测试的部分需求,又可以发挥相当大的作用。
|
||||
|
||||
Go 的接口提高了代码的分离度,改善了代码的复用性,使得代码开发过程中的设计模式更容易实现。用 Go 接口还能实现 `依赖注入模式`。
|
||||
Go 的接口提高了代码的分离度,改善了代码的复用性,使得代码开发过程中的设计模式更容易实现。用 Go 接口还能实现“依赖注入模式”。
|
||||
|
||||
## 11.12.3 接口的提取
|
||||
|
||||
`提取接口` 是非常有用的设计模式,可以减少需要的类型和方法数量,而且不需要像传统的基于类的面向对象语言那样维护整个的类层次结构。
|
||||
*提取接口*是非常有用的设计模式,可以减少需要的类型和方法数量,而且不需要像传统的基于类的面向对象语言那样维护整个的类层次结构。
|
||||
|
||||
Go 接口可以让开发者找出自己写的程序中的类型。假设有一些拥有共同行为的对象,并且开发者想要抽象出这些行为,这时就可以创建一个接口来使用。
|
||||
|
||||
我们来扩展 11.1 节的示例 11.2 [interfaces_poly.go](examples/chapter_11/interfaces_poly.go),假设我们需要一个新的接口 `TopologicalGenus`,用来给 shape 排序(这里简单地实现为返回 int)。我们需要做的是给想要满足接口的类型实现 `Rank()` 方法:
|
||||
我们来扩展 11.1 节的示例 11.2 [interfaces_poly.go](examples/chapter_11/interfaces_poly.go),假设我们需要一个新的接口 `TopologicalGenus`,用来给 `shape` 排序(这里简单地实现为返回 `int`)。我们需要做的是给想要满足接口的类型实现 `Rank()` 方法:
|
||||
|
||||
示例 11.17 [multi_interfaces_poly.go](examples/chapter_11/multi_interfaces_poly.go):
|
||||
|
||||
@@ -195,7 +195,7 @@ Shape details: &{5}
|
||||
Topological Genus of this shape is: 1
|
||||
```
|
||||
|
||||
所以你不用提前设计出所有的接口;`整个设计可以持续演进,而不用废弃之前的决定`。类型要实现某个接口,它本身不用改变,你只需要在这个类型上实现新的方法。
|
||||
所以你不用提前设计出所有的接口;*整个设计可以持续演进,而不用废弃之前的决定*。类型要实现某个接口,它本身不用改变,你只需要在这个类型上实现新的方法。
|
||||
|
||||
## 11.12.4 显式地指明类型实现了某个接口
|
||||
|
||||
@@ -222,7 +222,7 @@ func (b Bar) Foo() {}
|
||||
|
||||
## 11.12.5 空接口和函数重载
|
||||
|
||||
在 6.1 节中, 我们看到函数重载是不被允许的。在 Go 语言中函数重载可以用可变参数 `...T` 作为函数最后一个参数来实现(参见 6.3 节)。如果我们把 T 换为空接口,那么可以知道任何类型的变量都是满足 T (空接口)类型的,这样就允许我们传递任何数量任何类型的参数给函数,即重载的实际含义。
|
||||
在 [6.1](06.1.md) 节中, 我们看到函数重载是不被允许的。在 Go 语言中函数重载可以用可变参数 `...T` 作为函数最后一个参数来实现(参见 [6.3](06.3.md) 节)。如果我们把 `T` 换为空接口,那么可以知道任何类型的变量都是满足 `T` (空接口)类型的,这样就允许我们传递任何数量任何类型的参数给函数,即重载的实际含义。
|
||||
|
||||
函数 `fmt.Printf` 就是这样做的:
|
||||
|
||||
@@ -230,7 +230,7 @@ func (b Bar) Foo() {}
|
||||
fmt.Printf(format string, a ...interface{}) (n int, errno error)
|
||||
```
|
||||
|
||||
这个函数通过枚举 `slice` 类型的实参动态确定所有参数的类型,并查看每个类型是否实现了 `String()` 方法,如果是就用于产生输出信息。我们可以回到 11.10 节查看这些细节。
|
||||
这个函数通过枚举 slice 类型的实参动态确定所有参数的类型,并查看每个类型是否实现了 `String()` 方法,如果是就用于产生输出信息。我们可以回到 [11.10](11.10.md) 节查看这些细节。
|
||||
|
||||
## 11.12.6 接口的继承
|
||||
|
||||
@@ -253,13 +253,13 @@ func NewTask(command string, logger *log.Logger) *Task {
|
||||
}
|
||||
```
|
||||
|
||||
当 `log.Logger` 实现了 `Log()` 方法后,Task 的实例 task 就可以调用该方法:
|
||||
当 `log.Logger` 实现了 `Log()` 方法后,`Task` 的实例 `task` 就可以调用该方法:
|
||||
|
||||
```go
|
||||
task.Log()
|
||||
```
|
||||
|
||||
类型可以通过继承多个接口来提供像 `多重继承` 一样的特性:
|
||||
类型可以通过继承多个接口来提供像*多重继承*一样的特性:
|
||||
|
||||
```go
|
||||
type ReaderWriter struct {
|
||||
@@ -268,25 +268,25 @@ type ReaderWriter struct {
|
||||
}
|
||||
```
|
||||
|
||||
上面概述的原理被应用于整个 Go 包,多态用得越多,代码就相对越少(参见 12.8 节)。这被认为是 Go 编程中的重要的最佳实践。
|
||||
上面概述的原理被应用于整个 Go 包,多态用得越多,代码就相对越少(参见 [12.8 节](12.8.md))。这被认为是 Go 编程中的重要的最佳实践。
|
||||
|
||||
有用的接口可以在开发的过程中被归纳出来。添加新接口非常容易,因为已有的类型不用变动(仅仅需要实现新接口的方法)。已有的函数可以扩展为使用接口类型的约束性参数:通常只有函数签名需要改变。对比基于类的 OO 类型的语言在这种情况下则需要适应整个类层次结构的变化。
|
||||
|
||||
**练习 11.11**:[map_function_interface.go](exercises/chapter_11/map_function_interface.go):
|
||||
|
||||
在练习 7.13 中我们定义了一个 map 函数来使用 int 切片 (map_function.go)。
|
||||
在练习 7.13 中我们定义了一个 `map()` 函数来使用 `int` 切片 ([map_function.go](exercises\chapter_7\map_function.go))。
|
||||
|
||||
通过空接口和类型断言,现在我们可以写一个可以应用于许多类型的 `泛型` 的 map 函数,为 int 和 string 构建一个把 int 值加倍和将字符串值与其自身连接(译者注:即 `"abc"` 变成 `"abcabc"` )的 map 函数 `mapFunc`。
|
||||
|
||||
提示:为了可读性可以定义一个 interface{} 的别名,比如:type obj interface{}
|
||||
通过空接口和类型断言,现在我们可以写一个可以应用于许多类型的*泛型*的 `map()` 函数,为 `int` 和 `string` 构建一个把 `int` 值加倍和将字符串值与其自身连接(译者注:即 `"abc"` 变成 `"abcabc"` )的 `map()` 函数 `mapFunc()`。
|
||||
|
||||
提示:为了可读性可以定义一个 `interface{}` 的别名,比如:`type obj interface{}`。
|
||||
|
||||
**练习 11.12**:[map_function_interface_var.go](exercises/chapter_11/map_function_interface_var.go):
|
||||
|
||||
稍微改变练习 11.11,允许 `mapFunc` 接收不定数量的 items。
|
||||
稍微改变练习 11.11,允许 `mapFunc()` 接收不定数量的 `items`。
|
||||
|
||||
**练习 11.13**:[main_stack.go](exercises/chapter_11/main_stack.go)—[stack/stack_general.go](exercises/chapter_11/stack/stack_general.go):
|
||||
|
||||
在练习 10.16 和 10.17 中我们开发了一些栈结构类型。但是它们被限制为某种固定的内建类型。现在用一个元素类型是 interface{}(空接口)的切片开发一个通用的栈类型。
|
||||
在练习 10.16 和 10.17 中我们开发了一些栈结构类型。但是它们被限制为某种固定的内建类型。现在用一个元素类型是 `interface{}`(空接口)的切片开发一个通用的栈类型。
|
||||
|
||||
实现下面的栈方法:
|
||||
|
||||
|
@@ -2,13 +2,13 @@
|
||||
|
||||
我们总结一下前面看到的:Go 没有类,而是松耦合的类型、方法对接口的实现。
|
||||
|
||||
OO 语言最重要的三个方面分别是:封装,继承和多态,在 Go 中它们是怎样表现的呢?
|
||||
OO 语言最重要的三个方面分别是:封装、继承和多态,在 Go 中它们是怎样表现的呢?
|
||||
|
||||
- 封装(数据隐藏):和别的 OO 语言有 4 个或更多的访问层次相比,Go 把它简化为了 2 层(参见 4.2 节的可见性规则):
|
||||
- 封装(数据隐藏):和别的 OO 语言有 4 个或更多的访问层次相比,Go 把它简化为了 2 层(参见 [4.2 节](04.2.md)的可见性规则):
|
||||
|
||||
1)包范围内的:通过标识符首字母小写,`对象` 只在它所在的包内可见
|
||||
1)包范围内的:通过标识符首字母小写,*对象*只在它所在的包内可见
|
||||
|
||||
2)可导出的:通过标识符首字母大写,`对象` 对所在包以外也可见
|
||||
2)可导出的:通过标识符首字母大写,*对象*对所在包以外也可见
|
||||
|
||||
类型只拥有自己所在包中定义的方法。
|
||||
|
||||
|
@@ -43,7 +43,7 @@ func (cs Cars) FindAll(f func(car *Car) bool) Cars {
|
||||
}
|
||||
```
|
||||
|
||||
3)实现 Map 功能,产出除 car 对象以外的东西:
|
||||
3)实现对应作用的功效 (Map-functionality),从每个 `car` 对象当中产出某些东西:
|
||||
|
||||
```go
|
||||
// Process cars and create new data.
|
||||
@@ -66,7 +66,7 @@ allNewBMWs := allCars.FindAll(func(car *Car) bool {
|
||||
})
|
||||
```
|
||||
|
||||
4)我们也可以根据参数返回不同的函数。也许我们想根据不同的厂商添加汽车到不同的集合,但是这(这种映射关系)可能会是会改变的。所以我们可以定义一个函数来产生特定的添加函数和 map 集:
|
||||
4)我们也可以根据参数返回不同的函数。也许我们想根据不同的厂商添加汽车到不同的集合,但是这(这种映射关系)可能会是会改变的。所以我们可以定义一个函数来产生特定的添加函数和 `map` 集:
|
||||
|
||||
```go
|
||||
func MakeSortedAppender(manufacturers []string)(func(car *Car),map[string]Cars) {
|
||||
|
@@ -6,7 +6,7 @@
|
||||
v := varI.(T) // unchecked type assertion
|
||||
```
|
||||
|
||||
**varI 必须是一个接口变量**,否则编译器会报错:`invalid type assertion: varI.(T) (non-interface type (type of varI) on left)` 。
|
||||
**`varI` 必须是一个接口变量**,否则编译器会报错:`invalid type assertion: varI.(T) (non-interface type (type of varI) on left)` 。
|
||||
|
||||
类型断言可能是无效的,虽然编译器会尽力检查转换是否有效,但是它不可能预见所有的可能性。如果转换在程序运行时失败会导致错误发生。更安全的方式是使用以下形式来进行类型断言:
|
||||
|
||||
@@ -83,7 +83,7 @@ func (ci *Circle) Area() float32 {
|
||||
The type of areaIntf is: *main.Square
|
||||
areaIntf does not contain a variable of type Circle
|
||||
|
||||
程序中定义了一个新类型 `Circle`,它也实现了 `Shaper` 接口。 `if t, ok := areaIntf.(*Square); ok ` 测试 `areaIntf` 里是否有一个包含 `*Square` 类型的变量,结果是确定的;然后我们测试它是否包含一个 `*Circle` 类型的变量,结果是否定的。
|
||||
程序中定义了一个新类型 `Circle`,它也实现了 `Shaper` 接口。 `if t, ok := areaIntf.(*Square); ok` 测试 `areaIntf` 里是否有一个包含 `*Square` 类型的变量,结果是确定的;然后我们测试它是否包含一个 `*Circle` 类型的变量,结果是否定的。
|
||||
|
||||
**备注**
|
||||
|
||||
|
@@ -64,11 +64,11 @@ func classifier(items ...interface{}) {
|
||||
|
||||
在处理来自于外部的、类型未知的数据时,比如解析诸如 JSON 或 XML 编码的数据,类型测试和转换会非常有用。
|
||||
|
||||
在示例 12.17([xml.go](examples/chapter_12/xml.go))中解析 XML 文档时,我们就会用到 `type-switch`。
|
||||
在示例 12.17 ([xml.go](examples/chapter_12/xml.go)) 中解析 XML 文档时,我们就会用到 `type-switch`。
|
||||
|
||||
**练习 11.4** simple_interface2.go:
|
||||
**练习 11.4** [simple_interface2.go](exercises\chapter_11\simple_interface2.go):
|
||||
|
||||
接着练习 11.1 中的内容,创建第二个类型 `RSimple`,它也实现了接口 `Simpler`,写一个函数 `fi`,使它可以区分 `Simple` 和 `RSimple` 类型的变量。
|
||||
接着练习 11.1 中的内容,创建第二个类型 `RSimple`,它也实现了接口 `Simpler`,写一个函数 `fi()`,使它可以区分 `Simple` 和 `RSimple` 类型的变量。
|
||||
|
||||
## 链接
|
||||
|
||||
|
@@ -12,7 +12,7 @@ if sv, ok := v.(Stringer); ok {
|
||||
}
|
||||
```
|
||||
|
||||
`Print` 函数就是如此检测类型是否可以打印自身的。
|
||||
`Print()` 函数就是如此检测类型是否可以打印自身的。
|
||||
|
||||
接口是一种契约,实现类型必须满足它,它描述了类型的行为,规定类型可以做什么。接口彻底将类型能做什么,以及如何做分离开来,使得相同接口的变量在不同的时刻表现出不同的行为,这就是多态的本质。
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# 11.6 使用方法集与接口
|
||||
|
||||
在第 10.6.3 节及例子 methodset1.go 中我们看到,作用于变量上的方法实际上是不区分变量到底是指针还是值的。当碰到接口类型值时,这会变得有点复杂,原因是接口变量中存储的具体值是不可寻址的,幸运的是,如果使用不当编译器会给出错误。考虑下面的程序:
|
||||
在[第 10.6.3 节](10.6.md)及例子 [methodset1.go](examples\chapter_10\methodset1.go) 中我们看到,作用于变量上的方法实际上是不区分变量到底是指针还是值的。当碰到接口类型值时,这会变得有点复杂,原因是接口变量中存储的具体值是不可寻址的,幸运的是,如果使用不当编译器会给出错误。考虑下面的程序:
|
||||
|
||||
示例 11.5 [methodset2.go](examples/chapter_11/methodset2.go):
|
||||
|
||||
@@ -81,9 +81,9 @@ func main() {
|
||||
|
||||
Go 语言规范定义了接口方法集的调用规则:
|
||||
|
||||
- 类型 *T 的可调用方法集包含接受者为 *T 或 T 的所有方法集
|
||||
- 类型 T 的可调用方法集包含接受者为 T 的所有方法
|
||||
- 类型 T 的可调用方法集不包含接受者为 *T 的方法
|
||||
- 类型 `*T` 的可调用方法集包含接受者为 `*T` 或 `T` 的所有方法集
|
||||
- 类型 `T` 的可调用方法集包含接受者为 `T` 的所有方法
|
||||
- 类型 `T` 的可调用方法集**不**包含接受者为 `*T` 的方法
|
||||
|
||||
## 链接
|
||||
|
||||
|
@@ -45,7 +45,7 @@ a := sort.IntArray(data) //conversion to type IntArray from package sort
|
||||
sort.Sort(a)
|
||||
```
|
||||
|
||||
完整的、可运行的代码可以在 `sort.go` 和 `sortmain.go` 里找到。
|
||||
完整的、可运行的代码可以在 [sort.go](examples\chapter_11\sort\sort.go) 和 [sortmain.go](examples\chapter_11\sortmain.go) 里找到。
|
||||
|
||||
同样的原理,排序函数可以用于一个浮点型数组,一个字符串数组,或者一个表示每周各天的结构体 `dayArray`。
|
||||
|
||||
@@ -180,7 +180,7 @@ func main() {
|
||||
|
||||
**备注**:
|
||||
|
||||
`panic("fail")` 用于停止处于在非正常情况下的程序(详细请参考 第13章),当然也可以先打印一条信息,然后调用 `os.Exit(1)` 来停止程序。
|
||||
`panic("fail")` 用于停止处于在非正常情况下的程序(详细请参考[第 13 章](13.0.md)),当然也可以先打印一条信息,然后调用 `os.Exit(1)` 来停止程序。
|
||||
|
||||
上面的例子帮助我们进一步了解了接口的意义和使用方式。对于基本类型的排序,标准库已经提供了相关的排序函数,所以不需要我们再重复造轮子了。对于一般性的排序,`sort` 包定义了一个接口:
|
||||
|
||||
@@ -194,29 +194,29 @@ type Interface interface {
|
||||
|
||||
这个接口总结了需要用于排序的抽象方法,函数 `Sort(data Interface)` 用来对此类对象进行排序,可以用它们来实现对其他类型的数据(非基本类型)进行排序。在上面的例子中,我们也是这么做的,不仅可以对 `int` 和 `string` 序列进行排序,也可以对用户自定义类型 `dayArray` 进行排序。
|
||||
|
||||
**练习 11.5** interfaces_ext.go:
|
||||
**练习 11.5** [interfaces_ext.go](exercises\chapter_11\interfaces_ext.go):
|
||||
|
||||
a). 继续扩展程序,定义类型 `Triangle`,让它实现 `AreaInterface` 接口。通过计算一个特定三角形的面积来进行测试(三角形面积=0.5 * (底 * 高))
|
||||
|
||||
b). 定义一个新接口 `PeriInterface`,它有一个 `Perimeter` 方法。让 `Square` 实现这个接口,并通过一个 `Square` 示例来测试它。
|
||||
|
||||
**练习 11.6** point_interfaces.go:
|
||||
**练习 11.6** [point_interfaces.go](exercises\chapter_11\point_interfaces.go):
|
||||
|
||||
继续 10.3 中的练习 point_methods.go,定义接口 `Magnitude`,它有一个方法 `Abs()`。让 `Point`、`Point3` 及`Polar` 实现此接口。通过接口类型变量使用方法做 point.go 中同样的事情。
|
||||
继续 [10.3](10.3.md) 中的练习 [point_methods.go](exercises\chapter_10\point_methods.go),定义接口 `Magnitude`,它有一个方法 `Abs()`。让 `Point`、`Point3` 及 `Polar` 实现此接口。通过接口类型变量使用方法做 point.go 中同样的事情。
|
||||
|
||||
**练习 11.7** float_sort.go/float_sortmain.go:
|
||||
**练习 11.7** [float_sort.go](exercises\chapter_11\float_sort.go) / [float_sortmain.go](exercises\chapter_11\float_sortmain.go):
|
||||
|
||||
类似 11.7 和示例 11.3/4,定义一个包 `float64`,并在包里定义类型 `Float64Array`,然后让它实现 `Sorter` 接口用来对 `float64` 数组进行排序。
|
||||
|
||||
另外提供如下方法:
|
||||
|
||||
- `NewFloat64Array()`:创建一个包含 25 个元素的数组变量(参考10.2)
|
||||
- `List()`:返回数组格式化后的字符串,并在 `String()` 方法中调用它,这样就不用显式地调用 `List()` 来打印数组(参考10.7)
|
||||
- `Fill()`:创建一个包含 10 个随机浮点数的数组(参考4.5.2.6)
|
||||
- `NewFloat64Array()`:创建一个包含 25 个元素的数组变量(参考 [10.2](10.2.md) )
|
||||
- `List()`:返回数组格式化后的字符串,并在 `String()` 方法中调用它,这样就不用显式地调用 `List()` 来打印数组(参考 [10.7](10.7.md))
|
||||
- `Fill()`:创建一个包含 10 个随机浮点数的数组(参考 [4.5.2.6](04.5.md))
|
||||
|
||||
在主程序中新建一个此类型的变量,然后对它排序并进行测试。
|
||||
|
||||
**练习 11.8** sort.go/sort_persons.go:
|
||||
**练习 11.8** [sort.go](exercises\chapter_11\sort\sort.go) / [sort_persons.go](exercises\chapter_11\sort_persons.go):
|
||||
|
||||
定义一个结构体 `Person`,它有两个字段:`firstName` 和 `lastName`,为 `[]Person` 定义类型 `Persons` 。让 `Persons` 实现 `Sorter` 接口并进行测试。
|
||||
|
||||
|
@@ -16,7 +16,7 @@ type Writer interface {
|
||||
|
||||
只要类型实现了读写接口,提供 `Read` 和 `Write` 方法,就可以从它读取数据,或向它写入数据。一个对象要是可读的,它必须实现 `io.Reader` 接口,这个接口只有一个签名是 `Read(p []byte) (n int, err error)` 的方法,它从调用它的对象上读取数据,并把读到的数据放入参数中的字节切片中,然后返回读取的字节数和一个 `error` 对象,如果没有错误发生返回 `nil`,如果已经到达输入的尾端,会返回 `io.EOF("EOF")`,如果读取的过程中发生了错误,就会返回具体的错误信息。类似地,一个对象要是可写的,它必须实现 `io.Writer` 接口,这个接口也只有一个签名是 `Write(p []byte) (n int, err error)` 的方法,它将指定字节切片中的数据写入调用它的对象里,然后返回实际写入的字节数和一个 `error` 对象(如果没有错误发生就是 `nil`)。
|
||||
|
||||
`io` 包里的 `Readers` 和 `Writers` 都是不带缓冲的,`bufio` 包里提供了对应的带缓冲的操作,在读写 `UTF-8` 编码的文本文件时它们尤其有用。在 第12章 我们会看到很多在实战中使用它们的例子。
|
||||
`io` 包里的 `Readers` 和 `Writers` 都是不带缓冲的,`bufio` 包里提供了对应的带缓冲的操作,在读写 `UTF-8` 编码的文本文件时它们尤其有用。在[第 12 章](12.0.md)我们会看到很多在实战中使用它们的例子。
|
||||
|
||||
在实际编程中尽可能的使用这些接口,会使程序变得更通用,可以在任何实现了这些接口的类型上使用读写方法。
|
||||
|
||||
|
@@ -105,13 +105,13 @@ func main() {
|
||||
|
||||
any hello is a special String!
|
||||
|
||||
**练习 11.9** simple_interface3.go:
|
||||
**练习 11.9** [simple_interface3.go](exercises\chapter_11\simple_interface3.go):
|
||||
|
||||
继续练习 11.2,在它中添加一个 `gI` 函数,它不再接受 `Simpler` 类型的参数,而是接受一个空接口参数。然后通过类型断言判断参数是否是 `Simpler` 类型。最后在 `main` 使用 `gI` 取代 `fI` 函数并调用它。确保你的代码足够安全。
|
||||
继续练习 11.2,在它中添加一个 `gI()` 函数,它不再接受 `Simpler` 类型的参数,而是接受一个空接口参数。然后通过类型断言判断参数是否是 `Simpler` 类型。最后在 `main` 使用 `gI()` 取代 `fI()` 函数并调用它。确保你的代码足够安全。
|
||||
|
||||
## 11.9.2 构建通用类型或包含不同类型变量的数组
|
||||
|
||||
在 7.6.6 中我们看到了能被搜索和排序的 `int` 数组、`float` 数组以及 `string` 数组,那么对于其他类型的数组呢,是不是我们必须得自己编程实现它们?
|
||||
在 [7.6.6](07.6.md) 中我们看到了能被搜索和排序的 `int` 数组、`float` 数组以及 `string` 数组,那么对于其他类型的数组呢,是不是我们必须得自己编程实现它们?
|
||||
|
||||
现在我们知道该怎么做了,就是通过使用空接口。让我们给空接口定一个别名类型 `Element`:`type Element interface{}`
|
||||
|
||||
@@ -141,9 +141,9 @@ func (p *Vector) Set(i int, e Element) {
|
||||
|
||||
`Vector` 中存储的所有元素都是 `Element` 类型,要得到它们的原始类型(unboxing:拆箱)需要用到类型断言。TODO:The compiler rejects assertions guaranteed to fail,类型断言总是在运行时才执行,因此它会产生运行时错误。
|
||||
|
||||
**练习 11.10** min_interface.go / minmain.go:
|
||||
**练习 11.10** [min_interface.go](exercises\chapter_11\min_interface.go) / [minmain.go](exercises\chapter_11\minmain.go):
|
||||
|
||||
仿照 11.7 中开发的 `Sorter` 接口,创建一个 `Miner` 接口并实现一些必要的操作。函数 `Min` 接受一个 `Miner` 类型变量的集合,然后计算并返回集合中最小的元素。
|
||||
仿照 11.7 中开发的 `Sorter` 接口,创建一个 `Miner` 接口并实现一些必要的操作。函数 `Min()` 接受一个 `Miner` 类型变量的集合,然后计算并返回集合中最小的元素。
|
||||
|
||||
## 11.9.3 复制数据切片至空接口切片
|
||||
|
||||
@@ -170,7 +170,7 @@ for i, d := range dataSlice {
|
||||
|
||||
## 11.9.4 通用类型的节点数据结构
|
||||
|
||||
在 10.1 中我们遇到了诸如列表和树这样的数据结构,在它们的定义中使用了一种叫节点的递归结构体类型,节点包含一个某种类型的数据字段。现在可以使用空接口作为数据字段的类型,这样我们就能写出通用的代码。下面是实现一个二叉树的部分代码:通用定义、用于创建空节点的 `NewNode` 方法,及设置数据的 `SetData` 方法。
|
||||
在 [10.1](10.1.md) 中我们遇到了诸如列表和树这样的数据结构,在它们的定义中使用了一种叫节点的递归结构体类型,节点包含一个某种类型的数据字段。现在可以使用空接口作为数据字段的类型,这样我们就能写出通用的代码。下面是实现一个二叉树的部分代码:通用定义、用于创建空节点的 `NewNode` 方法,及设置数据的 `SetData` 方法。
|
||||
|
||||
示例 11.10 [node_structures.go](examples/chapter_11/node_structures.go):
|
||||
|
||||
@@ -246,7 +246,7 @@ func f3(x myInterface) {
|
||||
}
|
||||
```
|
||||
|
||||
`x` 转换为 `myPrintInterface` 类型是完全动态的:只要 `x` 的底层类型(动态类型)定义了 `print` 方法这个调用就可以正常运行(译注:若 `x` 的底层类型未定义 `print` 方法,此处类型断言会导致 `panic`,最佳实践应该为 `if mpi, ok := x.(myPrintInterface); ok { mpi.print() }`,参考 11.3 章节)。
|
||||
`x` 转换为 `myPrintInterface` 类型是完全动态的:只要 `x` 的底层类型(动态类型)定义了 `print` 方法这个调用就可以正常运行(译注:若 `x` 的底层类型未定义 `print` 方法,此处类型断言会导致 `panic`,最佳实践应该为 `if mpi, ok := x.(myPrintInterface); ok { mpi.print() }`,参考 [11.3](11.3.md) 章节)。
|
||||
|
||||
## 链接
|
||||
|
||||
|
Reference in New Issue
Block a user