通向Golang的捷径【11. 接口和反射】

11.1 接口介绍

Go 语言并不是一种经典的 OO 语言, 因为它并未提供类和继承, 同时它给出了更加灵活的接口机制, 其中包含了大量的面对对象的理念,Go 接口提供了一种对象行为的定义方法.

一个接口可给出一组方法 (即方法集合), 而这些方法并未包含在代码中, 因为它们并未实现 (即抽象方法), 同时接口也不会包含变量, 而接口的定义格式如下:
在这里插入图片描述
方法名加上 er(或 r) 后缀, 将得到接口名, 比如 Printer, Reader, Writer, Logger, Converter 等, 因此接口名将标记成一类操作, 而另一种命名方式则较少使用, 即使用 able 作为名称后缀 (此时不适合添加 er 后缀), 比如Recoverable, 或是以 I 为首字符 (这与.NET 和 Java 很相似).

为了简化 Go 接口的定义, 通常会定义 0-3 个方法. 与大多数的 OO 语言不同, Go 接口可包含数值, 或是定义接口类型的变量, 以及给出一个接口值:
在这里插入图片描述
ai 是一个多字节的数据结构, 其中包含了 nil 值 (未初始化值), 这么说并不完整, 实际上它是一个指针, 但是指向接口值的指针是非法的, 因此在接口操作中无法使用指针, 并且会增加代码出错的风险, 所以接口值的存储格式如下:
在这里插入图片描述
通过运行时的反射功能, 可创建方法集合的指针.

类型 (比如结构) 也可包含接口实现的方法集合, 其中的每个方法都将基于类型变量给出实现代码, 也就是说类型实现了所需的接口, 即类型的接口给出了一组方法, 比如实现接口的类型变量将分配给 ai(即接收器), 方法集合的指针将指向已实现的接口方法, 当另一个类型的变量 (也实现了接口) 分配给 ai 时, 上述的两个参数也将发生变化.

一个类型未被要求, 显式实现一个接口, 因此接口可隐含存在, 同时多个类型可实现相同的接口, 实现一个接口的类型也可给出其他函数, 一个类型可实现多个接口, 一个接口类型中, 可包含了任意类型 (实现了接口) 的实例引用, 因此一个接口将被称为动态类型.

即使接口在类型之后定义, 也将放置在不同的包, 并进行独立编译, 如果在接口中包含方法名, 之后需实现对应的方法. 以上给出的诸多特性将包含一定的灵活性.

例 11.1 interfaces.go

在这里插入图片描述
在这里插入图片描述
上述代码定义了一个结构 Square 和一个接口 Shaper, 同时接口给出了一个方法 Area(). 在 main() 函数中, 构建一个 Square 实例, 同时在 Area() 方法中包含了一个 Square 类型的接收器, 用于计算正方形的面积, 这时可认为 Square 结构, 实现了一个接口 Shaper.

我们还可将一个 Square 类型变量, 分配给一个接口类型变量:
在这里插入图片描述
这时接口变量中, 将包含 Square 变量的引用, 并可将该引用传递给, Area() 方法的 Square 形参, 当然也可基于 Square 实例 sql, 直接调用 Area() 方法, 即sq1.Area(), 但是基于接口实例的调用, 可实现调用操作的泛型(generalize), 因为在接口变量中, 不仅包含了接收器实例的数值, 还包含了方法集合中所需方法的指针.

以上就是 Go 语言实现的多态性, 实现了一个经典的 OO 理念: 为当前类型选择一个正确的方法, 因为在链接不同实例时, 类型可提供不同的行为.

如果 Square 类型未实现 Area() 方法, 将得到一个编译器错误:
在这里插入图片描述
如果 Shaper 接口提供了另一个方法 Perimeter(), 而 Square 未给出该方法的实现, 即使 Square 实例未调用Perimeter(), 也将得到以上的相同错误.

以下将扩展上述代码,Rectangle 类型也将实现一个 Shaper 接口, 并会创建一个数组, 其中的元素为 Shaper 接口, 当调用每个元素的 Area() 方法时, 将展示出 Go 语言的多态性.

例 11.2 interfaces_poly.go

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在每次 shapes[n].Area() 调用中, 我们只知道这是一个 Shaper 对象, 并不了解是 Square 类型还是 Rectangle类型, 需要自动匹配. 因此接口可实现更清晰更简洁更易于扩展的代码, 同时 11.12.3 节将讨论如何在类型中,更简单地添加新接口.

以下是一个更复杂的示例, 首先给出了两个类型 stockPosition 和 car, 它们都包含了一个方法 getValue(), 基于该方法, 又定义了一个接口 valuable, 同时还定义了一个方法 showValue(), 它包含了一个 valuable 接口类型的形参, 用于显示当前 valuable 接口所使用的类型数值:

例 11.3 interfaces_poly.go

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

来自标准库的示例

io 包给出了一个接口类型 Reader:
在这里插入图片描述
以下将定义一个变量 r, 并实现一些功能:
在这里插入图片描述
r 的静态类型为 io.Reader, 右侧的每一种类型都实现了一个 Read() 方法. 有时可基于不同的方式来理解接口,比如接口可视为一种类型, 同时类型接口又是该类型定义的一组可导出的方法集合, 而这些方法又不需要从属于一个明确的接口.

11.2 接口的嵌套

一个接口可包含一个或多个其他接口, 这等同于将嵌入接口的方法定义, 显式传递给上层接口, 比如 File 接口包含了 ReadWrite 和 Lock 接口的所有方法, 而 Close() 方法只属于 File 接口.
在这里插入图片描述

11.3 接口变量的类型检查和类型转换-类型断言

接口变量 varI 可包含任意类型的数值, 这意味着需要对这类动态类型进行检查, 在运行状态下, 接口变量将关联一个实际类型, 但这个实际类型可发生变化 (即动态类型机制), 也就是通过持续赋值, 接口变量所关联的类型将发生变化, 一般情况下, 如果 varI 能在某一时刻, 关联到 T 类型变量, 则需要使用类型断言, 来测试类型关联的正确性,
在这里插入图片描述
类型断言也可能失败, 编译器只能保证最佳的执行过程 (即接口数值可转换成所需类型), 而无法预知所有可能的情况, 如果无法得到所需的类型, 程序将产生一个运行时错误, 因此以下代码将给出一种安全方式:
在这里插入图片描述
如果类型转换满足要求, 即 v 将包含转换成 T 类型的 varI 值,ok 将为 true, 否则 v 将包含 T 类型的零值,ok为 false, 但不会出现运行时错误. 应给出逗号后的 ok, 它可实现类型断言.

在大多数情况下, 需要使用 if 语句, 来测试 ok 值,
在这里插入图片描述
由于 varI 变量的可见性规则, 有时可让 varI 和 v 使用相同的名称.

例 11.4 type_interfaces.go

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
以上定义了一个新类型 Circle, 它也实现了 Shaper 接口, 在 t, ok := areaIntf.(*Square) 代码行中, 将测试areaIntf 中是否包含了一个 Square 变量, 如果满足条件, 那么 areaIntf 则不可能再包含一个 Circle 变量.

如果忽略 areaIntf.(*Square) 的星号 *, 将产生一个编译器错误:
在这里插入图片描述

11.4 特殊的 switch 语句

在接口变量的关联类型的测试中, 可使用一个特殊的 switch 语句:
在这里插入图片描述
在这里插入图片描述
变量 t 可接收来自于 areaIntf 的类型和数值, 也就是实现该接口的所有类型 (nil 除外), 这里的接口为 Shaper,如果当前类型未给出 case 条件, 将执行 default. 这里的 switch 语句不允许使用 fallthrough, 同时可实现类型的运行时分析, 当然所有的内建类型 ( int,bool,string) 也可在特殊的 switch 语句中进行测试.

如果在 switch 语句中, 只需进行类型测试, 则省略赋值语句, 如下:
在这里插入图片描述
在以下的类型分类函数中, 可传入一个数组, 在数组中将包含数量可变的类型值 (比如接口所关联的类型值), 同时将基于不同类型, 以执行对应的操作:
在这里插入图片描述
可使用以下方式, 调用该函数:
在这里插入图片描述
当处理外部数据源的未知类型时, 使用上述方法, 可将其转换成 Go 语言的数据类型, 比如解析 JSON 或 XML编码, 在例 12.17(xml.go) 中, 使用了特殊 switch 语句, 来解析 XML 文档.

11.5 接口的数值测试

这是类型断言的一种特殊情况, 假定 v 是一个数值, 且需要测试 Stringer 接口是否包含了该数值, 如下:
在这里插入图片描述
首先一个接口必须由类型来实现, 接口可描述类型的行为, 这可使对象定义与对象操作完全隔离, 因此同一个接口变量可描述不同的实现, 这也就是多态性. 在编写函数时, 包含一个接口变量的形参, 可实现更强大的泛型.

标准库代码的应用很广泛, 如果不通过接口概念, 无法理清构建代码的意图. 在后续小节中, 将给出两个重要的示例, 以便深入了解接口概念的应用.

11.6 接口的方法集合

在 10.6.3 节的 methodset1.go 示例中, 基于变量的方法, 并不会区分数值传递和指针传递, 而存储接口类型中保存的数值, 则更加复杂, 因为无法访问接口中存储的数值, 幸好会产生一个编译器错误, 比如以下代码:

例 11.5 methodset2.go

在这里插入图片描述
在这里插入图片描述
如果调用 CountInto 时, 传入 lst, 将产生一个编译器错误, 因为 CountInto 给出了一个 Appender 形参, 同时Append() 需要传入一个指针变量, 而 LongEnough 传入 lst, 则可完成执行, 因为 Len() 需要传入一个数值变量. 当 CountInto 传入一个指针变量 plst, 则可完成执行, 而将 plst 传入 LongEnough, 也可完成执行, 因为接收器的指针变量可实现反向引用 (dereference).

基于接口, 调用对应的方法时, 方法的接收器类型必须与, 定义接口的类型相同, 同时还必须传入一个可确认的实际类型,
• 包含指针变量的方法, 需要传入一个指针, 不能传入一个数值变量, 因为接口中保存的数值变量, 无法提
供访问地址.
• 包含数值变量的方法, 需要传入一个数值, 也可传入一个指针变量, 因为指针变量可实现反向引用.

当赋值给接口后, 编译器将为该赋值, 在所有的接口方法中, 调用一个与之类型相符的方法, 因此在编译期内,不正确的接口赋值将导致编译失败.

11.7 示例:Sorter 接口的排序

以下将给出 sort 包的一个应用示例, 为了实现一个数值和字符串集合的排序, 只需获取集合元素的个数, 并使用 Less(i, j) 进行元素 i 和 j 的比较, 以及使用 Swap(i, j), 进行索引 i 和 j 之间的元素互换.

sort 包的 Sort 函数所包含的算法, 只能使用一组基于集合数据的方法, 也就是冒泡排序, 当然也可使用其他的排序算法.
在这里插入图片描述
Sort 函数可传入一个接口类型 Sorter 的泛型形参, 而接口类型 Sorter 将声明所需的方法:
在这里插入图片描述
int 类型的 Sorter 接口, 并不意味着集合数据必须是 int 类型, 但 i,j 应当是整型索引, 集合长度也必须是整型.如果需要实现一个 int 数组的排序, 则必须定义一个类型, 并实现对应接口的所需方法:
在这里插入图片描述
之后可实现一个整型数组的排序:
在这里插入图片描述
在这里插入图片描述
在 sort.go 和 sortmain.go 文件中, 找到上述操作的完整代码. 由于接口类型的强大功能, 相同的操作也可用于float,string 类型的数组, 以及 dayArry 结构 ( 用于表示星期几) 的数组.

例 11.6 sort.go

在这里插入图片描述

例 11.7 sortmain.go

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在未出现终止条件的情况下,panic(”fail”) 提供了一种终止应用程序的方法, 当然也可以采用, 打印一条通告消息, 之后调用 os.Exit(1) 的方法.

上述示例可使我们更好地理解接口的用法, 如果是基础类型的排序, 则不能给出上述代码, 而是应当使用标准库.

在 sort 包中, 定义了泛型的排序接口:
在这里插入图片描述
以实现排序的抽象方法, 因此 Sort(data Interface) 可用于大多数类型 (基础类型除外), 同时也可实现多种数据的排序, 从上例中可知, 能够同时处理 int 数值和字符串, 如果使用自定义类型 dayArray, 可实现字符串的内部字符数组的排序.

11.8 示例: 读取和写入

在应用程序中, 读取和写入都是基本操作, 比如文件的读取和写入, 读取和写入缓冲 ( 比如字节 slice 和字符串), 标准输入端/标准输出端/标准出错端, 网络连接, 管道等, 或是其他的自定义设备, 为了实现代码的通用性,Go 语言包含了一致性的数据读取和数据写入.

io 包提供了读取和写入的接口, 即 io.Reader 和 io.Writer:
在这里插入图片描述
当基于任意类型实现读取和写入操作时, 该类型必须提供 Read() 和 Write() 方法, 以关联到读取和写入接口,对于可读取对象, 必须使用 io.Reader 接口, 该接口将指定一个匹配的方法 Read([]byte) (int, error), 该方法可从读取对象中读取数据, 并将这些数据存入给定的字节 slice 中, 同时还将返回读取的字节数, 以及一个错误对象 (如果未出现错误, 或是到达数据源的末尾 (比如文件末尾), 错误对象为 nil, 如果出现错误, 错误对象则为非nil 值), 同样一个可写入对象, 必须使用 io.Writer 接口, 该接口将指定一个匹配的方法 Write([]byte) (int, error), 该方法可将给定的字节 slice 的数据, 存入写入对象, 并返回写入的字节数, 以及一个错误对象 (如果未出现错误, 错误对象为 nil).

io 包的 Reader(读取器) 和 Writer(写入器) 都未包含缓冲功能, 而 bufio 包则可提供读写的缓冲功能, 尤其是文本文件 (UTF8 编码) 的读写, 第 12 章包含了大量的类似示例. 因此在大量的接口中, 都将给出相似的功能性方法, 这使得泛型变为可能, 每一种类型都可使用功能性方法, 实现对应的接口. 比如实现 JPEG 解码的函数中, 可包含一个 Reader 形参, 它可解码来自于磁盘, 网络连接, 压缩文件的数据.

11.9 空接口

11.9.1 概念

空 (或最小) 接口不会给出方法, 同时不会要求它, 提供所有的接口功能.
在这里插入图片描述
任意变量和类型都可实现一个空接口, 不光是引用类型 (类似于 Java,C# 的对象), any 或 Any 是一个最佳的名称, 并可视为一个假名或缩略语, 同时类似于 Java,C# 的 Object 类, 即所有类的基类.

接口变量var val interface{} 可通过赋值, 而接收任意类型的变量,

例 11.8 empty_interface.go

在这里插入图片描述
在这里插入图片描述
在以下示例中,int,string 和 Person 实例, 都将分配给了接口变量 val, 并使用了类型 switch 语句, 对上述类型进行检查, 每个interface{} 变量将包含 8 个字节的内存空间, 前 4 个字节用于类型, 后 4 个字节用于类型数值或类型指针. 因此在 lambda 函数的类型 switch 语句中, 将展示空接口的用法:

例 11.9 emptyint_switch.go

在这里插入图片描述

11.9.2 使用通用类型或不同类型的变量, 可创建一个数组

在 7.6.6 节中, 讨论了 int,float,string 数组的搜索和保存, 但并未提到其他类型的数组. 为了全面了解空接口的用法, 首先为其设定一个假名 Element, 即type Element interface {}. 之后将定义一个 Vector 结构, 其中将给出一个包含 Element 元素的 slice.
在这里插入图片描述
由于任意类型都可实现空接口, 因此等同于 Vector 可包含任意类型, 实际上每个 Element 元素都可给出不同的类型, 因此可定义一个方法 At(), 以返回特定索引值的元素.
在这里插入图片描述
而 Set() 可设置特定索引值的元素:
在这里插入图片描述
矢量的任何信息都可保存成 Element 元素, 并会使用类型断言, 获取到当前类型, 在编译器中, 将会剔除这些类型断言 (因为在编译过程中, 这些类型断言都将失败), 因此这些类型断言将在运行时执行, 并可生成运行时错误.

11.9.3 在接口 slice 中复制数据

假定有一个 myType 类型的 slice, 可将其放入一个空接口的 slice 中.
在这里插入图片描述
但上述代码无法工作, 并会给出一个编译器错误:
在这里插入图片描述
因为这两种变量的内存结构并不相同, 可参考页面 http://code.google.com/p/go-wiki/wiki/InterfaceSlice. 同时这类复制可在一个 for-range 语句中完成,
在这里插入图片描述

11.9.4 包含通用类型或不同类型的结构

在 10.1 节中, 给出了列表和树, 而这类可递归的结构, 又被称为节点 (node), 在节点的数据域中, 可包含某些类型, 当然也可给出空接口类型, 以方便写出通用代码, 比如以下的二叉树代码中, 给出了一个通用定义, 一个创建空结构的 NewNode 方法, 一个设置数值的 SetData 方法.

例 11.10 node_structures.go

在这里插入图片描述
在这里插入图片描述

11.9.5 接口间的赋值

一个接口可分配给其他接口, 但必须实现下层的赋值方法, 在运行时状态下, 将检查接口之间的类型转换, 当转换失败时, 将产生一个运行时错误, 这也是 Go 语言提供的动态属性, 并兼容于动态语言 Ruby 和 Python, 假定给出了以下定义:
在这里插入图片描述
以下语句和类型断言都有效:
在这里插入图片描述
对应的函数调用如下:
在这里插入图片描述
转换成 myPrintInterface, 完全是一个动态操作,x(动态类型) 的下层类型, 实现了一个 print 方法.

11.10 reflect 包

11.10.1 方法和类型

在 10.4 节中, 描述了如何使用反射, 进行结构的分析, 以下将深入学习反射带来的强大功能, 首先反射功能使得应用程序, 可对自身的结构进行检查, 尤其是基于类型的检查, 这类功能被称为元编程方法(metaprogramming), 所以在运行时状态下, 反射可对类型和变量进行检查, 比如它们的存储尺寸, 方法, 同时反射还可动态调用其他方法, 并且还能用于来自包 (未提供源码) 的类型, 所以反射是一种功能强大的工具, 必须谨慎使用, 同时又要避免滥用.

变量的基本信息为变量类型和变量数值, 而它们在 reflect 包中, 变量类型被描述为 Type( 可视为一种通用的Go 类型), 变量数值被描述为 Value, 而它是 Go 数值对应的反射接口. 在 reflect 包中, 还提供了两个简单函数 reflect.TypeOf 和 reflect.ValueOf, 用于获取变量类型和变量数值, 比如 x 被定义为var x float64 = 3.4, 这时 reflect.TypeOf(x) 可获取到 float64 类型, reflect.ValueOf(x) 将返回变量 x 的数值.

事实上反射需基于一个接口进行检查, 首先变量将转换成空接口, 看看上述函数的原型, 则一目了然:
在这里插入图片描述
因此在接口中, 将同时包含类型和数值, 接口中包含的信息将反射给反射对象, 从反射对象中, 我们又可再次查看变量类型和变量数值.

reflect.Type 和 reflect.Value 类型都包含了一些方法, 用于检查和操控对应的信息. 其中最重要的方法是,
reflect.Value 类型给出了一个 Type 方法, 用于返回 reflect.Value 对应的变量类型, 而 reflect.Type 和 reflect.Value 类型都给出了一个 Kind 方法, 用于返回一个常量索引值, 用于描述变量类型的排序 (因为不同的类型存在一个排序表, 比如 Uint,Float64,Slice 等), 在 reflect.Value 类型的方法中, 将出现含有 Int 和 Float的方法名, 这类方法可用于获取大值 (比如 int64 和 float64 类型值), 还可定义不同类型的 Kind 类型, 比如type Kind uint8. 而不同类型的排序表如下:
在这里插入图片描述
如果给出赋值语句v:= reflect.ValueOf(x), 且 v.Kind() 为 float64, 因此以下比较为真:
在这里插入图片描述
Kind 方法将会给出下层类型, 如果给出以下定义:
在这里插入图片描述
v.Kind() 将返回 reflect.Int, 如果基于 Value 类型, 调用 Interface() 方法, 将返回接口中包含的变量数值, 比如fmt.Println(v.Interface()), 即打印 Value 类型的变量 v, 以下代码更完整地实现了上述操作:

例 11.1 reflect1.go

在这里插入图片描述
已知 x 为 float64 类型值, reflect.ValueOf(x).Float() 可返回一个 float64 类型值, 而 Int(), Bool(), Complex()和 String() 方法, 将给出相同的返回结果.

11.10.2 通过反射修改变量数值 (字符串)

在以下示例中, 如果需要将 v 值修改为 3.1415,Value 类型包含了一些设定 (Set-) 方法, 但必须小心使用, 如下:
在这里插入图片描述
这将产生一个错误:
在这里插入图片描述
因为 v 并不是一个可访问的变量, 反射 Value 类型可提供一个设定属性, 但不是所有的反射 Value 类型都提供了设定属性, 因此必须进行该属性的测试, 即 CanSet() 方法, 而测试结果为 false, 当变量 v 创建时 (v := reflect.ValueOf(x)), 该方法将得到 x 的一个副本, 因此通过 v 无法对 x 值进行修改, 为了实现这一目的, 应传入 x 的地址, 如v = reflect.ValueOf(&x). 通过 Type() 可知,v 的当前类型为 *float64, 这也无法完成数值修改, 因此还需要使用 Elem(), 实现指针的间接引用, 即v = v.Elem(), 这时 v.CanSet() 的测试结果为真, 并且v.SetFloat(3.1415) 可实现数值修改.

例 11.12 reflect2.go

在这里插入图片描述
在这里插入图片描述
反射数值需要一个地址, 才可实现修改.

11.10.3 结构的反射

在例 11.10 中, 给出了结构的一些用法, 使用 NumField() 可获取结构中数据域的个数, 并可基于 for 语句的索引值, 对每个数据域进行访问, 比如基于索引值 n, 调用不同的方法, 对数据域 (与索引值对应)
进行处理,即Method(n).Call(nil).

例 11.13 reflect_struct.go

在这里插入图片描述
在这里插入图片描述
当修改某个数值时 (value.Field(i).SetString(”C#”)), 将产生一个运行时错误:
在这里插入图片描述
因此只有结构的可导出数据域 (以大写字母开头) 才能被修改.

例 11.14 reflect_struct2.go

在这里插入图片描述
在这里插入图片描述

11.11 Printf 和反射

之前的小节中, 讨论了 reflect(反射) 包, 它也是标准库中频繁使用的一类功能, 比如 Printf 函数在可扩展形参… 中, 使用了这类功能, 其原型如下:
在这里插入图片描述
Printf 函数中… 形参的类型为interface{}, 该函数可利用 reflect 包, 生成… 形参所需的一个实参列表, 首先Printf 需要了解实参的实际类型, 如果知道实参类型为 unsigned 或 long, 则不会使用%u 或%ld 格式化字符, 而只会使用%d, 因此 Print 和 Println 无须格式化字符, 也可实现实参的完美打印.

以下示例将实现一个通用打印函数的简单版本, 其中将使用一个类型 switch 语句, 对类型进行检查, 以实现对应类型的打印输出:

例 11.15 print.go

在这里插入图片描述
在这里插入图片描述

11.12 接口和动态类型

11.12.1 动态类型

在经典的 OO 语言 (例如 C++,Java 和 C#) 中, 会用类的概念, 来组织数据和处理数据的方法, 一个类中可保存数据和方法, 并且不可分隔.

在 Go 语言中不存在类, 数据 (结构或其他通用类型) 和方法并无太大的关联性.Go 语言的接口概念与 Java, C#很相似, 一个接口必须提供一个最小方法集合, 这使得关联性更加灵活和通用, 任意类型都能给出接口方法的代码, 这等同于隐含实现了一个接口, 同时无须显式标注.

相比于其他语言,Go 语言是唯一一个合并了接口, 静态类型检查 (用于确认类型是否实现了对应的接口), 动态的运行时转换, 并且无须给出类型和接口的显式绑定, 这使得接口的定义和使用, 无须修改之前的代码.

函数可包含一个或多个接口类型的参数, 当函数调用时, 可传入类型 (该类型实现了对应接口) 变量, 因此实现了一个接口的类型, 可传递给包含该接口形参的任意函数. 上述特性与动态语言 (Python, Ruby) 的 duck typing 很相似, 当它被调用时, 意味着对象将被处理 (比如将传递给函数), 并会使用对应的方法, 同时与实参的类型无关, 所以接口和类型的关系不再重要.

在以下示例中,DuckDance 函数将给出一个接口类型变量 IDuck, 只有当 DuckDance 调用时, 传入一个类型(实现了 IDuck) 变量, 应用程序才可实现编译.

例 11.16 duck_dance.go

在这里插入图片描述
在这里插入图片描述
如果 Bird 类型未实现 Walk()(可注释相关代码), 将得到一个编译器错误:
在这里插入图片描述
如果基于其他类型 (未实现对应的接口), 调用 DuckDance, 在 Go 语言中将得到一个编译器错误, 而在 Python或 Ruby 中, 将得到一个运行时错误.

11.12.2 动态方法的执行

在 Python 和 Ruby 中, 类似于 duck typing 的功能都可实现最终绑定 (也就是在运行时状态下完成绑定), 方法将基于相关参数, 进行简单调用, 而传入的变量将在运行时进行解析, 而这类语言的大多数方法, 都类似于responds_to, 需要检查对象与方法的绑定, 但这类操作通常需要大量代码.

而 Go 语言的类似机制正好相反, 在大多数情况下, 都将使用基于编译器的静态检查, 当类型变量分配给接口变量时, 将检查该类型是否实现了对应接口的所有函数, 如果基于一个通用类型 (比如 interface{}) 而调用一个方法, 则需要使用类型断言, 来检查变量类型是否实现了对应接口.

假定有不同操作, 需要描述成一种类型, 这里以写入 XML 流为例, 可定义一个 XML 的写入接口, 并给出一个方法 ( 这类方式甚至可视为一个私有接口):
在这里插入图片描述
这时可为任意变量的数据流, 创建一个函数 StreamXML, 并使用一个类型断言, 来检查传入变量的类型是否实现了对应接口, 如果未实现, 可调用自定义函数 encodeToXML, 来完成上述任务:
在这里插入图片描述
gob 包也使用了相同的机制, 其中定义了两个接口 GobEncoder 和 GobDecoder, 并允许类型自定义编码和解码操作, 否则将使用反射 (标准方式), 因此 Go 语言具有动态语言的优势, 但并未出现运行时绑定的缺点. 同时可降低对单元测试的依赖, 这对于动态语言来说相当重要, 并能节约大量的精力.

Go 接口有利于相关信息的分离, 并能改进代码的重用性, 以便在代码开发中, 获得易于构建的模式, 同时 Go接口已经实现了一种依赖注入(Injection Dependency) 的构建模式.

11.12.3 提取接口

在 Go 语言中, 还提供了一种模式, 即提取接口, 它可减弱与类型和方法的关联性, 也无须和传统 OO 语言一样, 管理一个完整的类层次.

Go 接口允许编程者基于数据类型, 以实现所需的接口类型, 如果若干对象具备相同的操作, 编程者则希望能够提取这类操作, 则需创建一个接口, 来实现这些操作, 在以下示例中, 需要构建一个新接口 TopologicalGenus,它可给出一个基于外形 (这里会使用一个 int 值, 来标记不同的外形) 的排序, 如果需要在类型中使用该接口,则必须实现一个方法 Rank():

例 11.17 multi_interfaces_poly.go

在这里插入图片描述
在这里插入图片描述
因此在开发过程中, 无须预先实现好所有接口, 即整个设计可处于一个发展过程, 无须事先决定好一切, 如果一个数据类型必须实现一个新接口, 而数据类型不会产生变化, 则必须为该类型, 创建一个新方法.

11.12.4 显式标记实现接口的类型

如果希望显式声明已实现的接口类型, 则需要在接口的方法集合中, 添加一个方法 ( 并给出一个适合的方法名), 如下:
在这里插入图片描述
之后的 Bar 类型, 实现了 ImplementsFooer 和 Foo, 这等同于实现了 Fooer 接口:
在这里插入图片描述
在上述代码中, 大多数的实现代码并未给出, 但是这类方法, 将会限制接口的使用, 因为有时需要处理相似接口之间的冲突.

11.12.5 空接口和函数重载

在 6.1 节中已知,Go 语言并不允许重载函数, 但是可通过可变形参列表 (…T), 来实现函数的重载, 如果 T 是一个空接口, 那么它可接收任意类型的变量, 因此通过形参, 可将任意类型的实参, 传递给函数, 也就是函数的重载, 在函数定义中, 可使用以下语句:
在这里插入图片描述
上述函数可基于一个 slice 提供的不同类型值, 而实现不同类型的自动处理, 如果每个类型都实现了 String()方法, 那么上述语句可自动调用不同类型的 String() 方法.

11.12.6 接口的继承

如果一种类型中, 包含 (嵌套) 了另一种类型 (该类型实现了一个或多个接口) 的指针, 这时上层类型可使用下层类型的所有接口方法. 先给出一个类型定义:
在这里插入图片描述
上述类型的工厂函数如下:
在这里插入图片描述
当 log.Logger 实现了一个 Log() 方法,Tash 实例则可调用它,task.Log(). 因此类型可通过多个接口实现继承, 这类功能等同于多重继承.
在这里插入图片描述
在所有的 Go 语言包中, 都应用了上述的机制, 以保证最大的可能性 (多态性), 以及最少的代码长度, 所以上述机制是 Go 语言编程中最重要的概念, 同时也依托于接口的强大功能, 它更容易添加新接口, 并且原有类型无须更改 (只需实现自身的方法), 将数据类型的实参, 传递给接口类型的形参, 可使原有函数实现泛型 (通用性),通常情况下, 只有函数的形参需要修改, 相比于 OO 语言, 应用设计需要与整个类层次相匹配.

11.13 无对象的 Go 语言

Go 语言中不存在类, 而类型, 方法, 以及接口实现, 都是松耦合.OO 语言最重要的三个概念: 封装, 继承和多态性, Go 语言将如何实现这些功能?

  • 封装 (隐藏数据): 在 OO 语言中, 可能会出现 4 层以上的访问层次, 而 Go 语言通常只有 2 层 ( 参见 4.2节的可见性规则).
    ▶ 包空间: 只在包中有效的元素, 将使用小写字母开头.
    ▶ 导出空间: 如果包元素需要在导出空间中有效, 将使用大写字母开头.
    在包中定义的类型, 只能包含本包给出的方法.

  • 继承: 嵌入一个或多个类型 (类型包含了所需的行为, 比如数据域或方法), 通过多个类型的嵌入, 可实现多重继承.

  • 多态性: 类型变量可分配给任意接口 (类型所实现的接口) 的变量, 而类型与接口并无太大的关联性, 通过多个接口的实现, 也可完成多重继承,Go 接口与 Java 和 C# 接口并不相同, 最大的不同是, 接口之间都是独立的, 并能为大型编程开发, 提供一种相符的真正实现迭代的设计方法.

11.14 结构, 集合以及高阶函数

通常情况下, 在应用程序中, 需要使用结构, 以及结构对象的集合.
在这里插入图片描述
在这里插入图片描述
之后可使用高阶函数, 也就是将其他函数作为形参, 而这类函数可完成所需的功能:

  1. 当定义一个通用函数 Process() 时, 可包含另一个函数 f, 用于操控每一种车:
    在这里插入图片描述

  2. 使用一个查找函数, 可获得车辆的一个子集, 其中会在一个切片数组中, 调用 Process().
    在这里插入图片描述

  3. 创建 map 函数, 用于处理每一种车型:
    在这里插入图片描述
    从函数中, 需要返回形参中包含的数值, 也可能是将车辆, 附加到厂商集合中, 因此这类信息一直在变量, 所以需要定义一个特殊的 append 函数, 作为 map 结合.

在这里插入图片描述
在这里插入图片描述
之后我们可将在不同的集合中, 对车辆进行排序, 如下:
在这里插入图片描述
在以下示例中, 会基于上述代码, 实现相关的处理:

例 11.18 cars.go

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述