Go 入门指南 Go 入门指南
文档 GitHub
译者 GitHub
文档 GitHub
译者 GitHub
  • 前言
  • 学习 Go 语言

    • 第1章:Go 语言的起源,发展与普及

      • 1.1 起源与发展
      • 1.2 语言的主要特性与发展的环境和影响因素
    • 第2章:安装与运行环境

      • 2.1 平台与架构
      • 2.2 Go 环境变量
      • 2.3 在 Linux 上安装 Go
      • 2.4 在 Mac OS X 上安装 Go
      • 2.5 在 Windows 上安装 Go
      • 2.6 安装目录清单
      • 2.7 Go 运行时 (runtime)
      • 2.8 Go 解释器
    • 第3章:编辑器、集成开发环境与其它工具

      • 3.1 Go 开发环境的基本要求
      • 3.2 编辑器和集成开发环境
      • 3.3 调试器
      • 3.4 构建并运行 Go 程序
      • 3.5 格式化代码
      • 3.6 生成代码文档
      • 3.7 其它工具
      • 3.8 Go 性能说明
      • 3.9 与其它语言进行交互
  • 语言的核心结构与技术

    • 第4章:基本结构和基本数据类型

      • 4.1 文件名、关键字与标识符
      • 4.2 Go 程序的基本结构和要素
      • 4.3 常量
      • 4.4 变量
      • 4.5 基本类型和运算符
      • 4.6 字符串
      • 4.7 strings 和 strconv 包
      • 4.8 时间和日期
      • 4.9 指针
    • 第5章:控制结构
      • 5.1 if-else 结构
      • 5.2 测试多返回值函数的错误
      • 5.3 switch 结构
      • 5.4 for 结构
      • 5.5 break 与 continue
      • 5.6 标签与 goto
    • 第6章:函数(function)
      • 6.1 介绍
      • 6.2 函数参数与返回值
      • 6.3 传递变长参数
      • 6.4 defer 和追踪
      • 6.5 内置函数
      • 6.6 递归函数
      • 6.7 将函数作为参数
      • 6.8 闭包
      • 6.9 应用闭包:将函数作为返回值
      • 6.10 使用闭包调试
      • 6.11 计算函数执行时间
      • 6.12 通过内存缓存来提升性能
    • 第7章:数组与切片
      • 7.1 声明和初始化
      • 7.2 切片
      • 7.3 For-range 结构
      • 7.4 切片重组 (reslice)
      • 7.5 切片的复制与追加
      • 7.6 字符串、数组和切片的应用
    • 第8章:Map
      • 8.1 声明、初始化和 make
      • 8.2 测试键值对是否存在及删除元素
      • 8.3 for-range 的配套用法
      • 8.4 map 类型的切片
      • 8.5 map 的排序
      • 8.6 将 map 的键值对调
    • 第9章:包(package)
      • 9.1 标准库概述
      • 9.2 regexp 包
      • 9.3 锁和 sync 包
      • 9.4 精密计算和 big 包
      • 9.5 自定义包和可见性
      • 9.6 为自定义包使用 godoc
      • 9.7 使用 go install 安装自定义包
      • 9.8 自定义包的目录结构、go install 和 go test
      • 9.9 通过 Git 打包和安装
      • 9.10 Go 的外部包和项目
      • 9.11 在 Go 程序中使用外部库
    • 第10章:结构(struct)与方法(method)

      • 10.1 结构体定义
      • 10.2 使用工厂方法创建结构体实例
      • 10.3 使用自定义包中的结构体
      • 10.4 带标签的结构体
      • 10.5 匿名字段和内嵌结构体
      • 10.6 方法
      • 10.7 类型的 String() 方法和格式化描述符
      • 10.8 垃圾回收和 SetFinalizer
    • 第11章:接口(interface)与反射(reflection)

      • 11.1 接口是什么
      • 11.2 接口嵌套接口
      • 11.3 类型断言:如何检测和转换接口变量的类型
      • 11.4 类型判断:type-switch
      • 11.5 测试一个值是否实现了某个接口
      • 11.6 使用方法集与接口
      • 11.7 第一个例子:使用 Sorter 接口排序
      • 11.8 第二个例子:读和写
      • 11.9 空接口
      • 11.10 反射包
      • 11.11 Printf() 和反射
      • 11.12 接口与动态类型
      • 11.13 总结:Go 中的面向对象
      • 11.14 结构体、集合和高阶函数
  • Go 高级编程

    • 第12章:读写数据

      • 12.1 读取用户的输入
      • 12.2 文件读写
      • 12.3 文件拷贝
      • 12.4 从命令行读取参数
      • 12.5 用 buffer 读取文件
      • 12.6 用切片读写文件
      • 12.7 用 defer 关闭文件
      • 12.8 使用接口的实际例子:fmt.Fprintf
      • 12.9 JSON 数据格式
      • 12.10 XML 数据格式
      • 12.11 用 Gob 传输数据
      • 12.12 Go 中的密码学
    • 第13章:错误处理与测试

      • 13.1 错误处理
      • 13.2 运行时异常和 panic
      • 13.3 从 panic 中恢复 (recover)
      • 13.4 自定义包中的错误处理和 panicking
      • 13.5 一种用闭包处理错误的模式
      • 13.6 启动外部命令和程序
      • 13.7 Go 中的单元测试和基准测试
      • 13.8 测试的具体例子
      • 13.9 用(测试数据)表驱动测试
      • 13.10 性能调试:分析并优化 Go 程序
    • 第14章:协程 (goroutine) 与通道 (channel)

      • 14.1 并发、并行和协程
      • 14.2 协程间的信道
      • 14.3 协程的同步:关闭通道-测试阻塞的通道
      • 14.4 使用 select 切换协程
      • 14.5 通道、超时和计时器(Ticker)
      • 14.6 协程和恢复 (recover)
      • 14.7 新旧模型对比:任务和 worker
      • 14.8 惰性生成器的实现
      • 14.9 实现 Futures 模式
      • 14.10 复用
      • 14.11 限制同时处理的请求数
      • 14.12 链式协程
      • 14.13 在多核心上并行计算
      • 14.14 并行化大量数据的计算
      • 14.15 漏桶算法
      • 14.16 对 Go 协程进行基准测试
      • 14.17 使用通道并发访问对象
    • 第 15 章:网络、模板与网页应用

      • 15.1 tcp 服务器
      • 15.2 一个简单的 web 服务器
      • 15.3 访问并读取页面数据
      • 15.4 写一个简单的网页应用
      • 15.5 确保网页应用健壮
      • 15.6 用模板编写网页应用
      • 15.7 探索 template 包
      • 15.8 精巧的多功能网页服务器
      • 15.9 用 rpc 实现远程过程调用
      • 15.10 基于网络的通道 netchan
      • 15.11 与 websocket 通信
      • 15.12 用 smtp 发送邮件
  • 实际应用

    • 第16章:常见的陷阱与错误

      • 16.1 误用短声明导致变量覆盖
      • 16.2 误用字符串
      • 16.3 发生错误时使用 defer 关闭一个文件
      • 16.4 何时使用 new() 和 make()
      • 16.5 不需要将一个指向切片的指针传递给函数
      • 16.6 使用指针指向接口类型
      • 16.7 使用值类型时误用指针
      • 16.8 误用协程和通道
      • 16.9 闭包和协程的使用
      • 16.10 糟糕的错误处理
    • 第17章:模式

      • 17.1 逗号 ok 模式
      • 17.2 defer 模式
      • 17.3 可见性模式
      • 17.4 运算符模式和接口
    • 第18章:出于性能考虑的实用代码片段

      • 18.1 字符串
      • 18.2 数组和切片
      • 18.3 映射
      • 18.4 结构体
      • 18.5 接口
      • 18.6 函数
      • 18.7 文件
      • 18.8 协程 (goroutine) 与通道 (channel)
      • 18.9 网络和网页应用
      • 18.10 其他
      • 18.11 出于性能考虑的最佳实践和建议
    • 第19章:构建一个完整的应用程序

      • 19.1 简介
      • 19.2 短网址项目简介
      • 版本 1 - 数据结构和前端界面
      • 19.4 用户界面:web 服务端
      • 版本 2 - 添加持久化存储
      • 版本 3 - 添加协程
      • 版本 4 - 用 JSON 持久化存储
      • 版本 5 - 分布式程序
      • 19.9 使用代理缓存
      • 19.10 总结和增强
    • 第20章:Go 语言在 Google App Engine 的使用

      • 20.1 什么是 Google App Engine?
      • 20.2 云上的 Go
      • 20.3 安装 Go App Engine SDK:为 Go 部署的开发环境
      • 20.4 建造你自己的 Hello world 应用
      • 20.5 使用用户服务和探索其 API
      • 20.6 处理窗口
      • 20.7 使用数据存储
      • 20.8 上传到云端
    • 第21章:实世界中 Go 的使用

      • 21.1 Heroku:一个使用 Go 的高度可用一致数据存储
      • 21.2 MROffice:一个使用 Go 的呼叫中心网络电话 (VOIP) 系统
      • 21.3 Atlassian:一个虚拟机群管理系统
      • 21.4 Camilistore:一个可寻址内容存储系统
      • 21.5 Go 语言的其他应用

11.12 接口与动态类型

11.12.1 Go 的动态类型

在经典的面向对象语言(像 C++,Java 和 C#)中数据和方法被封装为类的概念:类包含它们两者,并且不能剥离。

Go 没有类:数据(结构体或更一般的类型)和方法是一种松耦合的正交关系。

Go 中的接口跟 Java/C# 类似:都是必须提供一个指定方法集的实现。但是更加灵活通用:任何提供了接口方法实现代码的类型都隐式地实现了该接口,而不用显式地声明。

和其它语言相比,Go 是唯一结合了接口值,静态类型检查(是否该类型实现了某个接口),运行时动态转换的语言,并且不需要显式地声明类型是否满足某个接口。该特性允许我们在不改变已有的代码的情况下定义和使用新接口。

接收一个(或多个)接口类型作为参数的函数,其实参可以是任何实现了该接口的类型的变量。 实现了某个接口的类型可以被传给任何以此接口为参数的函数。

类似于 Python 和 Ruby 这类动态语言中的动态类型 (duck typing);这意味着对象可以根据提供的方法被处理(例如,作为参数传递给函数),而忽略它们的实际类型:它们能做什么比它们是什么更重要。

这在程序 duck_dance.go 中得以阐明,函数 DuckDance() 接受一个 IDuck 接口类型变量。仅当 DuckDance() 被实现了 IDuck 接口的类型调用时程序才能编译通过。

示例 11.16 duck_dance.go:

package main

import "fmt"

type IDuck interface {
	Quack()
	Walk()
}

func DuckDance(duck IDuck) {
	for i := 1; i <= 3; i++ {
		duck.Quack()
		duck.Walk()
	}
}

type Bird struct {
	// ...
}

func (b *Bird) Quack() {
	fmt.Println("I am quacking!")
}

func (b *Bird) Walk()  {
	fmt.Println("I am walking!")
}

func main() {
	b := new(Bird)
	DuckDance(b)
}

输出:

I am quacking!
I am walking!
I am quacking!
I am walking!
I am quacking!
I am walking!

如果 Bird 没有实现 Walk()(把它注释掉),会得到一个编译错误:

cannot use b (type *Bird) as type IDuck in function argument:
*Bird does not implement IDuck (missing Walk method)

如果对 cat 调用函数 DuckDance(),Go 会提示编译错误,但是 Python 和 Ruby 会以运行时错误结束。

11.12.2 动态方法调用

像 Python,Ruby 这类语言,动态类型是延迟绑定的(在运行时进行):方法只是用参数和变量简单地调用,然后在运行时才解析(它们很可能有像 responds_to 这样的方法来检查对象是否可以响应某个方法,但是这也意味着更大的编码量和更多的测试工作)

Go 的实现与此相反,通常需要编译器静态检查的支持:当变量被赋值给一个接口类型的变量时,编译器会检查其是否实现了该接口的所有函数。如果方法调用作用于像 interface{} 这样的“泛型”上,你可以通过类型断言(参见 11.3 节)来检查变量是否实现了相应接口。

例如,你用不同的类型表示 XML 输出流中的不同实体。然后我们为 XML 定义一个如下的“写”接口(甚至可以把它定义为私有接口):

type xmlWriter interface {
	WriteXML(w io.Writer) error
}

现在我们可以实现适用于该流类型的任何变量的 StreamXML() 函数,并用类型断言检查传入的变量是否实现了该接口;如果没有,我们就调用内建的 encodeToXML() 来完成相应工作:

// Exported XML streaming function.
func StreamXML(v interface{}, w io.Writer) error {
	if xw, ok := v.(xmlWriter); ok {
		// It’s an  xmlWriter, use method of asserted type.
		return xw.WriteXML(w)
	}
	// No implementation, so we have to use our own function (with perhaps reflection):
	return encodeToXML(v, w)
}

// Internal XML encoding function.
func encodeToXML(v interface{}, w io.Writer) error {
	// ...
}

Go 在这里用了和 gob 相同的机制:定义了两个接口 GobEncoder 和 GobDecoder。这样就允许类型自己实现从流编解码的具体方式;如果没有实现就使用标准的反射方式。

因此 Go 提供了动态语言的优点,却没有其他动态语言在运行时可能发生错误的缺点。

对于动态语言非常重要的单元测试来说,这样即可以减少单元测试的部分需求,又可以发挥相当大的作用。

Go 的接口提高了代码的分离度,改善了代码的复用性,使得代码开发过程中的设计模式更容易实现。用 Go 接口还能实现“依赖注入模式”。

11.12.3 接口的提取

提取接口是非常有用的设计模式,可以减少需要的类型和方法数量,而且不需要像传统的基于类的面向对象语言那样维护整个的类层次结构。

Go 接口可以让开发者找出自己写的程序中的类型。假设有一些拥有共同行为的对象,并且开发者想要抽象出这些行为,这时就可以创建一个接口来使用。

我们来扩展 11.1 节的示例 11.2 interfaces_poly.go,假设我们需要一个新的接口 TopologicalGenus,用来给 shape 排序(这里简单地实现为返回 int)。我们需要做的是给想要满足接口的类型实现 Rank() 方法:

示例 11.17 multi_interfaces_poly.go:

//multi_interfaces_poly.go
package main

import "fmt"

type Shaper interface {
	Area() float32
}

type TopologicalGenus interface {
	Rank() int
}

type Square struct {
	side float32
}

func (sq *Square) Area() float32 {
	return sq.side * sq.side
}

func (sq *Square) Rank() int {
	return 1
}

type Rectangle struct {
	length, width float32
}

func (r Rectangle) Area() float32 {
	return r.length * r.width
}

func (r Rectangle) Rank() int {
	return 2
}

func main() {
	r := Rectangle{5, 3} // Area() of Rectangle needs a value
	q := &Square{5}      // Area() of Square needs a pointer
	shapes := []Shaper{r, q}
	fmt.Println("Looping through shapes for area ...")
	for n, _ := range shapes {
		fmt.Println("Shape details: ", shapes[n])
		fmt.Println("Area of this shape is: ", shapes[n].Area())
	}
	topgen := []TopologicalGenus{r, q}
	fmt.Println("Looping through topgen for rank ...")
	for n, _ := range topgen {
		fmt.Println("Shape details: ", topgen[n])
		fmt.Println("Topological Genus of this shape is: ", topgen[n].Rank())
	}
}

输出:

Looping through shapes for area ...
Shape details:  {5 3}
Area of this shape is:  15
Shape details:  &{5}
Area of this shape is:  25
Looping through topgen for rank ...
Shape details:  {5 3}
Topological Genus of this shape is:  2
Shape details:  &{5}
Topological Genus of this shape is:  1

所以你不用提前设计出所有的接口;整个设计可以持续演进,而不用废弃之前的决定。类型要实现某个接口,它本身不用改变,你只需要在这个类型上实现新的方法。

11.12.4 显式地指明类型实现了某个接口

如果你希望满足某个接口的类型显式地声明它们实现了这个接口,你可以向接口的方法集中添加一个具有描述性名字的方法。例如:

type Fooer interface {
	Foo()
	ImplementsFooer()
}

类型 Bar 必须实现 ImplementsFooer 方法来满足 Fooer 接口,以清楚地记录这个事实。

type Bar struct{}
func (b Bar) ImplementsFooer() {}
func (b Bar) Foo() {}

大部分代码并不使用这样的约束,因为它限制了接口的实用性。

但是有些时候,这样的约束在大量相似的接口中被用来解决歧义。

11.12.5 空接口和函数重载

在 6.1 节中, 我们看到函数重载是不被允许的。在 Go 语言中函数重载可以用可变参数 ...T 作为函数最后一个参数来实现(参见 6.3 节)。如果我们把 T 换为空接口,那么可以知道任何类型的变量都是满足 T (空接口)类型的,这样就允许我们传递任何数量任何类型的参数给函数,即重载的实际含义。

函数 fmt.Printf 就是这样做的:

fmt.Printf(format string, a ...interface{}) (n int, errno error)

这个函数通过枚举 slice 类型的实参动态确定所有参数的类型,并查看每个类型是否实现了 String() 方法,如果是就用于产生输出信息。我们可以回到 11.10 节查看这些细节。

11.12.6 接口的继承

当一个类型包含(内嵌)另一个类型(实现了一个或多个接口)的指针时,这个类型就可以使用(另一个类型)所有的接口方法。

例如:

type Task struct {
	Command string
	*log.Logger
}

这个类型的工厂方法像这样:

func NewTask(command string, logger *log.Logger) *Task {
	return &Task{command, logger}
}

当 log.Logger 实现了 Log() 方法后,Task 的实例 task 就可以调用该方法:

task.Log()

类型可以通过继承多个接口来提供像多重继承一样的特性:

type ReaderWriter struct {
	*io.Reader
	*io.Writer
}

上面概述的原理被应用于整个 Go 包,多态用得越多,代码就相对越少(参见 12.8 节)。这被认为是 Go 编程中的重要的最佳实践。

有用的接口可以在开发的过程中被归纳出来。添加新接口非常容易,因为已有的类型不用变动(仅仅需要实现新接口的方法)。已有的函数可以扩展为使用接口类型的约束性参数:通常只有函数签名需要改变。对比基于类的 OO 类型的语言在这种情况下则需要适应整个类层次结构的变化。

练习 11.11:map_function_interface.go:

在练习 7.13 中我们定义了一个 map() 函数来使用 int 切片 (map_function.go)。

通过空接口和类型断言,现在我们可以写一个可以应用于许多类型的泛型的 map() 函数,为 int 和 string 构建一个把 int 值加倍和将字符串值与其自身连接(译者注:即 "abc" 变成 "abcabc" )的 map() 函数 mapFunc()。

提示:为了可读性可以定义一个 interface{} 的别名,比如:type obj interface{}。

练习 11.12:map_function_interface_var.go:

稍微改变练习 11.11,允许 mapFunc() 接收不定数量的 items。

练习 11.13:main_stack.go—stack/stack_general.go:

在练习 10.16 和 10.17 中我们开发了一些栈结构类型。但是它们被限制为某种固定的内建类型。现在用一个元素类型是 interface{}(空接口)的切片开发一个通用的栈类型。

实现下面的栈方法:

Len() int
IsEmpty() bool
Push(x interface{})
Pop() (interface{}, error)

Pop() 改变栈并返回最顶部的元素;Top() 只返回最顶部元素。

在主程序中构建一个充满不同类型元素的栈,然后弹出并打印所有元素的值。

Last Updated:
Contributors: Mr.Fang
Prev
11.11 Printf() 和反射
Next
11.13 总结:Go 中的面向对象