学习Go 语言 作者: Miek Gieben 译者: 邢星 感谢: Go 作者 Google Go Nuts 邮件列表 This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 3.0 License. Miek Gieben – ©2010 - 2012 邢星– ©2011, 2012 本作品依照署名-非商业性使用-相同方式共享3.0 Unported许可证发布。访 问http://creativecommons.org/licenses/by-nc-sa/3.0/ 查看该许可证副本,或写信到Creative Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA。 本书所有实例代码依此方式放入公共领域。 让我们开始学习Go 吧。 (0.5) 更新至Go 发布版本weekly/weekly.2011-08-10 Contents 读者 ........................ x 本书使用的设置 . . . . . . . . . . . . . . . . . . xii 1 简介 1 官方文档 ..................... 2 前身 ........................ 3 获得 Go ...................... 5 在 Windows 下获得 Go .............. 6 练习 ........................ 7 答案 ........................ 9 2 基础 10 Hello World .................... 12 编译和运行代码 . . . . . . . . . . . . . . . . . . 14 本书使用的设置 . . . . . . . . . . . . . . . . . . 14 变量、类型和保留字 . . . . . . . . . . . . . . . 15 运算符和内建函数 . . . . . . . . . . . . . . . . 23 Go 保留字 ..................... 25 控制结构 ..................... 26 内建函数 ..................... 38 array、slices 和 map . . . . . . . . . . . . . . . 40 ii Chapter: Contents 练习 ........................ 49 答案 ........................ 51 3 函数 60 作用域 ....................... 63 多值返回 ..................... 67 命名返回值 .................... 68 延迟代码 ..................... 71 变参 ........................ 74 函数作为值 .................... 75 回调 ........................ 77 恐慌(Panic)和恢复(Recover). . . . . . . . 78 练习 ........................ 80 答案 ........................ 87 4 包 100 标识符 . . . . . . . . . . . . . . . . . . . . . . . 103 包的文档 . . . . . . . . . . . . . . . . . . . . . 105 测试包 . . . . . . . . . . . . . . . . . . . . . . . 106 常用的包 . . . . . . . . . . . . . . . . . . . . . 110 练习 . . . . . . . . . . . . . . . . . . . . . . . . 113 答案 . . . . . . . . . . . . . . . . . . . . . . . . 115 5 进阶 122 内存分配 . . . . . . . . . . . . . . . . . . . . . 124 Contents iii 定义自己的类型 . . . . . . . . . . . . . . . . . . 129 转换 . . . . . . . . . . . . . . . . . . . . . . . . 134 练习 . . . . . . . . . . . . . . . . . . . . . . . . 137 答案 . . . . . . . . . . . . . . . . . . . . . . . . 143 6 接口 152 方法 . . . . . . . . . . . . . . . . . . . . . . . . 157 接口名字 . . . . . . . . . . . . . . . . . . . . . 160 简短的例子 . . . . . . . . . . . . . . . . . . . . 161 练习 . . . . . . . . . . . . . . . . . . . . . . . . 171 答案 . . . . . . . . . . . . . . . . . . . . . . . . 173 7 并发 178 更多关于 channel . . . . . . . . . . . . . . . . . 184 练习 . . . . . . . . . . . . . . . . . . . . . . . . 185 答案 . . . . . . . . . . . . . . . . . . . . . . . . 187 8 通讯 192 io.Reader . . . . . . . . . . . . . . . . . . . . . 195 一些例子 . . . . . . . . . . . . . . . . . . . . . 195 命令行参数 . . . . . . . . . . . . . . . . . . . . 196 执行命令 . . . . . . . . . . . . . . . . . . . . . 197 网络 . . . . . . . . . . . . . . . . . . . . . . . . 198 练习 . . . . . . . . . . . . . . . . . . . . . . . . 200 答案 . . . . . . . . . . . . . . . . . . . . . . . . 207 iv Chapter: Contents A 版权 224 贡献者 . . . . . . . . . . . . . . . . . . . . . . . 224 许可证和版权 . . . . . . . . . . . . . . . . . . . 226 B 索引 228 C Bibliography 232 List of Figures 1.1 Go 编年史 .................... 4 2.1 array 与 slice 对比 . . . . . . . . . . . . . . . . 43 3.1 一个简单的 LIFO 栈 . . . . . . . . . . . . . . . 82 6.1 使用反射去除层次关系 . . . . . . . . . . . . . 169 List of Code Examples 2.1 Hello world .................... 12 2.2 Declaration with = . . . . . . . . . . . . . . . . 15 2.3 Declaration with := . . . . . . . . . . . . . . . . 16 List of Code Examples v 2.4 Familiar types are still distinct . . . . . . . . . . 18 2.5 array 和 slice ................... 44 2.6 Simple for loop . . . . . . . . . . . . . . . . . . 51 2.7 For loop with an array . . . . . . . . . . . . . . 52 2.8 Fizz-Buzz ..................... 53 2.9 Strings ...................... 55 2.10 Runes in strings . . . . . . . . . . . . . . . . . . 57 2.11 Reverse a string . . . . . . . . . . . . . . . . . . 58 3.1 函数定义 ..................... 60 3.2 递归函数 ..................... 62 3.3 局部作用域 .................... 64 3.4 全局作用域 .................... 65 3.5 当函数调用函数时的作用域 . . . . . . . . . . . 66 3.6 没有 defer .................... 71 3.7 With defer .................... 72 3.8 函数符号 ..................... 73 3.9 带参数的函数符号 . . . . . . . . . . . . . . . . 74 3.10 在 defer 中访问返回值 . . . . . . . . . . . . . . 74 3.11 匿名函数 ..................... 75 3.12 使用 map 的函数作为值 . . . . . . . . . . . . . 76 3.13 Go 中的平均值函数 . . . . . . . . . . . . . . . . 87 3.14 stack.String() ................... 92 3.15 有变参的函数 . . . . . . . . . . . . . . . . . . 93 3.16 Go 编写的斐波那契函数 . . . . . . . . . . . . . 94 3.17 Map 函数 ..................... 95 vi Chapter: Contents 3.18 冒泡排序 ..................... 97 4.1 A small package . . . . . . . . . . . . . . . . . . 100 4.2 even 包的使用 . . . . . . . . . . . . . . . . . . 101 4.3 even 包的测试 . . . . . . . . . . . . . . . . . . 108 4.4 包里的 Stack . . . . . . . . . . . . . . . . . . . 115 4.5 Push/Pop 测试 . . . . . . . . . . . . . . . . . . 116 4.6 逆波兰计算器 . . . . . . . . . . . . . . . . . . 117 5.1 Use of a pointer . . . . . . . . . . . . . . . . . . 122 5.2 获取指针指向的值 . . . . . . . . . . . . . . . . 123 5.3 Structures . . . . . . . . . . . . . . . . . . . . . 130 5.4 Go 中更加通用的 map 函数 . . . . . . . . . . . 143 5.5 cat 程序 . . . . . . . . . . . . . . . . . . . . . . 146 6.1 定义结构和结构的方法 . . . . . . . . . . . . . . 152 6.2 用空接口作为参数的函数 . . . . . . . . . . . . 156 6.3 实现接口失败 . . . . . . . . . . . . . . . . . . 157 6.4 扩展内建类型错误 . . . . . . . . . . . . . . . . 158 6.5 扩展非本地类型错误 . . . . . . . . . . . . . . . 159 6.6 使用反射自省 . . . . . . . . . . . . . . . . . . 166 6.7 反射类型和值 . . . . . . . . . . . . . . . . . . 168 6.8 私有成员的反射 . . . . . . . . . . . . . . . . . 170 6.9 公有成员的反射 . . . . . . . . . . . . . . . . . 170 6.10 通用的计算最大值 . . . . . . . . . . . . . . . . 174 7.1 Go routine 实践 . . . . . . . . . . . . . . . . . . 179 7.2 Go routines 和 channel . . . . . . . . . . . . . . 181 7.3 使用 select . . . . . . . . . . . . . . . . . . . . 182 List of Exercises vii 7.4 Go 的 channel . . . . . . . . . . . . . . . . . . . 187 7.5 添加额外的退出 channel . . . . . . . . . . . . . 188 7.6 Go 的斐波那契函数 . . . . . . . . . . . . . . . . 190 8.1 从文件读取(无缓冲) . . . . . . . . . . . . . . 192 8.2 从文件读取(缓冲) . . . . . . . . . . . . . . . 193 8.3 Processes in Perl . . . . . . . . . . . . . . . . . 201 8.6 uniq(1) 的 Perl 实现 . . . . . . . . . . . . . . . 203 8.4 Go 中的进程 . . . . . . . . . . . . . . . . . . . 207 8.5 wc(1) 的 Go 实现 . . . . . . . . . . . . . . . . . 209 8.7 uniq(1) 的 Go 实现 . . . . . . . . . . . . . . . . 211 8.8 简易 echo 服务器 . . . . . . . . . . . . . . . . . 213 8.9 数字游戏 . . . . . . . . . . . . . . . . . . . . . 215 List of Exercises 1 (1) 文档 ...................... 7 2 (1) For-loop .................... 49 3 (1) FizzBuzz .................... 49 4 (1) Strings ..................... 49 5 (4) Average .................... 50 6 (4) 平均值 ..................... 80 7 (3) 整数顺序 ................... 80 8 (4) 作用域 ..................... 80 9 (5) 栈 ....................... 81 viii Chapter: Contents 10 (5) 变参 ...................... 82 11 (5) 斐波那契 ................... 82 12 (4) Map function . . . . . . . . . . . . . . . . . 83 13 (3) 最小值和最大值 . . . . . . . . . . . . . . . . 83 14 (5) 冒泡排序 ................... 83 15 (6) 函数返回一个函数 . . . . . . . . . . . . . . 84 16 (2) stack 包 . . . . . . . . . . . . . . . . . . . . 113 17 (7) 计算器 . . . . . . . . . . . . . . . . . . . . . 114 18 (4) 指针运算 . . . . . . . . . . . . . . . . . . . 137 19 (6) 使用 interface 的 map 函数 . . . . . . . . . . 138 20 (6) 指针 . . . . . . . . . . . . . . . . . . . . . . 138 21 (6) 链表 . . . . . . . . . . . . . . . . . . . . . . 139 22 (6) Cat . . . . . . . . . . . . . . . . . . . . . . . 139 23 (8) 方法调用 . . . . . . . . . . . . . . . . . . . 139 24 (6) 接口和编译 . . . . . . . . . . . . . . . . . . 171 25 (5) 指针和反射 . . . . . . . . . . . . . . . . . . 171 26 (7) 接口和 max() . . . . . . . . . . . . . . . . . 172 27 (4) Channel . . . . . . . . . . . . . . . . . . . . 185 28 (7) 斐波那契 II . . . . . . . . . . . . . . . . . . 185 29 (8) 进程 . . . . . . . . . . . . . . . . . . . . . . 200 30 (5) 单词和字母统计 . . . . . . . . . . . . . . . . 202 31 (4) Uniq . . . . . . . . . . . . . . . . . . . . . . 203 32 (9) Quine . . . . . . . . . . . . . . . . . . . . . 203 33 (8) Echo 服务 . . . . . . . . . . . . . . . . . . . 204 34 (9) 数字游戏 . . . . . . . . . . . . . . . . . . . 204 List of Exercises ix 35 (8) *Finger 守护进程 . . . . . . . . . . . . . . . 205 前言 “Go 是面向对象的语言吗?是也不是。” FAQ GOAUTHORS 读者 这是关于来自 Google 的 Go 语言的简介。目标是为这个新 的、革命性的语言提供一个指南。 这本书的目标读者是那些熟悉编程,并且了解某些编程 语言,例如 C[7],C++[29], Perl[9],Java[22],Erlang[8],Scala[23],Haskell[1]。这 不是教你如何编程的书,只是教你如何使用 Go。 学习某样新东西,最佳的方式可能是通过编写程序来探 索它。因此每章都包含了若干练习(和答案)来让你熟 悉这个语言。练习标有编号 Qn,而 n 是一个数字。在练 习编号后面的圆括号中的数字指定了该题的难度。难度 范围从 0 到 9,0 是最简单,而 9 最难。其后为了容易索 引,提供了一个简短的标题。例如: Q1. (1) map 函数… 读者 xi 展示了难度等级 1、编号 Q1 的关于 map() 函数的问题。相 关答案在练习的下一页。答案的顺序和练习一致,以 An 开头的答案,对应编号 n 的练习。一些练习没有答案, 它们被用星号标识出来。 内容布局 第 1 章:简介 提供了关于 Go 的简介和发展历史。同时讨论了 如 何 获 得 Go 自 身 的 代 码 。 虽 然 Go 完 全 可 以 在 Windows 平台上使用,但这里还是假设使用 类 Unix 环境。 第 2 章:基础 讨论了语言中可用的基本类型、变量和控制结构。 第 3 章:函数 会了解到函数,这是 Go 程序中的基本部件。 第 4 章:包 会了解在包中整合函数和数据。同时也将了解如何 对包编写文档和进行测试。 第 5 章:进阶 会看到如何创建自定义的类型。同时也将了解 Go 中的内存分配。 xii Chapter 0: 前言 第 6 章:接口 Go 不支持传统意义上的面向对象。在 Go 中接口是 核心概念。 第 7 章:并发 通 过 go 关键字,函数可以在不同的例程(叫 做 goroutines)中执行。通过 channel 来完成这 些 goroutines 之间的通讯。 第 8 章:通讯 最后一章展示了如何用接口来完成 Go 程序的其他 部分。如何创建、读取和写入文件。同时也简要了 解一下网络的使用。 希望你喜欢本书,同时也喜欢上 Go 语言。 本书使用的设置 • Go 被安装在 ˜/go; • 希望编译的 Go 代码放在 ˜/g/src,而 $GOPATH 设置 为 GOPATH=˜/g。 本书使用的设置 xiii 翻译 本书的内容可随意取用。这里已经有相关翻译: • 中 文 , 邢 星 :http://www.mikespook.com/ learning-go/ • 俄文,Michael Davydenko Miek Gieben,2011 – miek@miek.nl 邢星,2011 – mikespook@gmail.com 1 简介 “对此感兴趣,并且希望做点什么。” 在为 Go 添加复数支持时 KENTHOMPSON 什么是 Go?来自其网站[18]的介绍: Go 编程语言是一个使得程序员更加有效率的 开源项目。Go 是有表达力、简洁、清晰和有 效率的。它的并行机制使其很容易编写多核 和网络应用,而新奇的类型系统允许构建有 弹性的模块化程序。Go 编译到机器码非常快 速,同时具有便利的垃圾回收和强大的运行 时反射。它是快速的、静态类型编译语言, 但是感觉上是动态类型的,解释型语言。 Go 1 是 Go 语言的第一个稳定发布版本。本文档的所有练 习都工作于 Go 1 – 如果不能工作,那就一定是 bug。 本书使用了下面的约定: 2 Chapter 1: 简介 • 代码用 DejaVuMono 显示; • 关键词用 DejaVuMonoBold 显示; • 注释用 DejaVuMonoItalic 显示; • 代码中额外的标记, ← 用这种形式展现; • 使用数字 ..1 标记长内容– 解释会跟随其后; • 行号在右边展示; • Shell 示例用 % 作为标记; • 强调的段落会缩进,在左边有竖线。 官方文档 Go 已经有大量的文档。 例如 Go Tutorial [17] 和 Effective在 互 联 网 上 搜 索 时 , 应 当 使 用“golang”这 个 词 来 代 替 原 始 的“go”。 Go [12]。网站 http://golang.org/doc/ 也是绝佳的起点a。 虽然并不一定要阅读这些文档,但是强烈建议这么做。 Go 1 通过叫做 godoc 的标准程序提供其文档。如果你想 了解内建相关(参阅下一章“运算符和内建函数”小节)的 文档,可以像这样获取: ahttp://golang.org/doc/ 本身是由 godoc 提供服务的。 前身 3 % godocbuiltin 在第 4 章解释了如何构造你自己的包的文档。 前身 Go 的 前 身 来 自 于 Inferno[2]( 基 于 Plan 9[3] 的 改 造)。Inferno 包含了一个叫做 Limbo[4] 的语言。这里 引用了一段来自于 Limbo 论文的描述: Limbo 是用于开发运行在小型计算机上的分 布式应用的编程语言。它支持模块化编程, 编译期和运行时的强类型检查,进程内基 于具有类型的 channel 通讯,原子性垃圾收 集,和简单的抽象数据类型。它被设计用于 即便是没有硬件内存保护的小型设备上,也 能安全的运行。 Go 从 Limbo 继承的另一个特性是 channel(参阅第 7 章)。从 Limbo 文档来的另一段描述: [channel] 是用于向系统中其他代理发送和接 收特定类型对象的通讯机制。channel 可以用 于本地进程间通讯;用于连接到命名的目的 4 Chapter 1: 简介 地的库方法。两种情况都是直接发送和接收 操作的。 channel 在 Go 中比在 Limbo 中更加好用。如果我们对 Go 的历史深入探索,会发现一个指向 “Newsqueak[26]”的 线索,这是在类 C 语言中使用 channel 进行通讯的先 驱。channel 并不是这些语言独有的,另一个非类 C 语 言:Erlang[8],也在使用它。 Figure 1.1. Go 编年史 Newsqueak Limbo GoErlang 1989 19951986 2009 使用 channel 与其他进程进行通讯叫做“通讯序列化过 程(Communicating Sequential Processes - CSP)”,由 C. A. R. Hoare[25] 设计构想,而他正是那个发明快速排 序[24]算法的人。 Go 是第一个实现了简单的(或更加简单的)并行开 发,且跨平台的类 C 语言。 获得 Go 5 获得Go 在这一节中将介绍如何在本地设备上安装 Go。也可以在 线上http://play.golang.org/ 编译 Go 代码,这是最为便 捷的快速体验的方法。 也可以从网站[19]获得已经编译好的二进制版本。 Ubuntu 和 Debian 在 其 仓 库 中 都 有 Go 包 , 可 以 查 找“golang”包进行安装。但是工作的时候可能会有一些 小问题。所以这里仍然使用源码进行安装。 手工从 mercurial 中获取 Go 代码并且编译。对于其他 类 Unix 系统,过程类似。 • 首先安装 Mercurial(获取 hg 命令)。在 Ubun- tu/Debian/Fedora 需要安装 mercurial 包; • 为 了 编 译 Go 需 要 包 :bison ,gcc ,libc6- dev,ed,gawk 和make; • 设置环境变量 GOROOT 作为 Go 的安装目录: % exportGOROOT=˜/go • 然后获取 Go 最新的发布版(=Go 1)源代码: 6 Chapter 1: 简介% hg clone -r re- leasehttps://go.googlecode.com/hg/$GOROOT • 设置 PATH 指向到 Go 的二进制文件所在目录,这样 就可以让 Shell 找到它们: % exportPATH=$GOROOT/bin:$PATH • 编译 Go % cd$GOROOT/src % ./all.bash 如果全部都没问题,你最终会看到下面的内容: +---cd../test +0knownbugs;0unexpectedbugs + +ALLTESTSPASSED + +--- +InstalledGoforlinux/amd64in/home/go +Installedcommandsin/home/go/bin 在 Windows 下获得 Go 最好的方式仍然是遵循网站[19]的介绍,为了方便重复如 下。 练习 7 • 下 载 Go 1 :http://code.google.com/p/go/ downloads/list?q=OpSys-Windows+Type%3DArchive ; • 解压缩到 C:\ 盘; • 确保内容在 C:\Go。注意:在解压缩 zip 文件时,这 个目录会被创建; • 添加 C:\Go\bin 到 $PATH: exportPATH=C:\Go\bin 练习 Q1. (1) 文档 1. Go 的文档可以通过 godoc 程序阅读,它包含在 Go 的发布包中。 godochash 给出了 hash 包的信息: 8 Chapter 1: 简介% godochash PACKAGE packagehash ... ... ... SUBDIRECTORIES adler32 crc32 crc64 fnv 哪个 go doc 的命令可以显示 hash 包中的 fnv 文 档? 答案 9 答案 A1. (1) 文档 1. fnv 包在 hash 的子目录中,所以只需要 go doc hash/fnv 即可。 也可以指定“Go 手册”中某个函数的的文档。例 如,函数 Printf 在 fmt 包中,仅阅读这个函数的 文档,使用:godocfmtPrintf 。 所有的内建函数同样可以通过 godoc 程序访问:go docbuiltin 。 2 基础 “在 Go 中,代码说到做到。” Go Nuts 邮件列表 ANDREWGERRAND 有一些东西使得 Go 区别于其他语言。 清晰并且简洁 Go 努力保持小并且优美,你可以在短短几行代码 里做许多事情; 并行 Go 让函数很容易成为非常轻量的线程。这些线程 在 Go 中被叫做 goroutines a; Channel 这些 goroutines 之间的通讯由 channel[25, 33] 完 成; a是的,它的发音很接近 coroutines,但是 goroutines 确实有一些不 同,我们将在第 7 章讨论。 基础 11 快速 编译很快,执行也很快。目标是跟 C 一样快。编译 时间用秒计算; 安全 当转换一个类型到另一个类型的时候需要显式的转 换并遵循严格的规则。Go 有垃圾收集,在 Go 中无 须 free(),语言会处理这一切; 标准格式化 Go 程序可以被格式化为程序员希望的(几乎)任 何形式,但是官方格式是存在的。标准也非常简 单:gofmt 的输出就是官方认可的格式; 类型后置 类型在变量名的后面,像这样 varaint ,来代替 C 中的 inta ; UTF-8 任何地方都是 UTF-8的,包括字符串以及程序代 码。你可以在代码中使用 Φ = Φ + 1; 开源 Go 的许可证是完全开源的,参阅 Go 发布的源码中 的 LICENSE 文件; 开心 用 Go 写程序会非常开心! 12 Chapter 2: 基础 Erlang[8] 与 Go 在部分功能上类似。Erlang 和 Go 之间 主要的区别是 Erlang 是函数式语言,而 Go 是命令式 的。Erlang 运行在虚拟机上,而 Go 是编译的。Go 用起 来感觉更接近 Unix。 Hello World 在 Go 指南中,用一个传统的方式展现了 Go:让它打 印“Hello World” (Ken Thompson 和 Dennis Ritchie 在 20 世纪 70 年代,发布 C 语言的时候开创了这个先河)。我 们不认为其他方法可以做得更好,所以就是这个吧:Go 的“Hello World”。 Listing 2.1. Hello world 1package main ..0 3import "fmt" // 实现格式化的 I/O。..1 5/* Print something */ ..2 6func main() {..3 7..4 8fmt.Printf("Hello, world; or καληµ´ρα κóσµ; orこんにちは世 Hello World 13 界\n") 9} 逐行阅读这个程序。 ..0 首行这个是必须的。所有的 Go 文件以 package < something> 开头,对于独立运行的执行文件必须是 package main; ..1 这是说需要将 ”fmt” 包加入 main。不是 main 的其他包都 被称为库,其他许多编程语言有着类似的概念(参阅第 4 章)。末尾以// 开头的内容是注释; ..2 这同样是注释,不过这是被包裹于/* 和*/ 之间的; ..3 package main 必须首先出现,紧跟着是import 。 在Go 中,package 总是首先出现,然后是import,然后是其他 所有内容。当Go 程序在执行的时候,首先调用的函数 是main.main(),这是从C 中继承而来。这里定义了这个函 数; ..4 第8 行调用了来自于fmt 包的函数打印字符串到屏幕。字 符串由" 包裹,并且可以包含非ASCII 的字符。这里使用 了希腊文和日文。 14 Chapter 2: 基础 编译和运行代码 构建 Go 程序的最佳途径是使用 go 工具。 构建 helloworld 只需要: % gobuildhelloworld.go 结果是叫做 helloworld 的可执行文件。 % ./helloworld Hello,world;or καληµ´ρα κóσµ; or こんにちは世 界 本书使用的设置 • Go 被安装在 ˜/go; • 希望编译的 Go 代码放在 ˜/g/src 而 $GOPATH 设置 为 GOPATH=˜/g。在使用包的时候需要用到这个变量 (参阅第 4 章)。 变量、类型和保留字 15 变量、类型和保留字 在接下来的章节中,我们将会了解这个新语言的变量、 基本类型、保留字和控制流。Go 在语法上有着类 C 的感 觉。如果你希望将两个(或更多)语句放在一行书写, 它们必须用分号(’;’)分隔。一般情况下,你不需要分号。 Go 同其他语言不同的地方在于变量的类型在变量名的后 面。不是:int a,而是 a int。当定义了一个变量,它默 认赋值为其类型的null 值。这意味着,在 var a int后,a 的值为 0。而 var s string,意味着 s 被赋值为零长度字 符串,也就是 ""。 在 Go 中,声明和赋值是两过程,但是可以连在一起。比 较下面作用相同的代码片段。 Listing 2.2. Declaration with = var a int var b bool a = 15 b = false 16 Chapter 2: 基础 Listing 2.3. Declaration with := a :=15 b :=false 在左边使用了保留字 var 声明变量,然后赋值给它。右边 的代码使用了 := 使得在一步内完成了声明和赋值(这一 形式只可用在函数内)。在这种情况下,变量的类型是 由值推演出来的。值 15 表示是 int 类型,值 false 告诉 Go 它的类型应当是 bool。多个 var 声明可以成组,const 和 import 同样允许这么做。留意圆括号的使用: var ( x int b bool ) 有相同类型的多个变量同样可以在一行内完成声明:var x,y int 让 x 和 y 都是 int 类型变量。同样可以使用 平行 赋值: a, b :=20,16 变量、类型和保留字 17 让 a 和 b 都是整数变量,并且赋值 20 给 a,16 给 b。 一个特殊的变量名是 _(下划线)。任何赋给它的值都被 丢弃。在这个例子中,将 35 赋值给 b,同时丢弃 34。 _, b :=34,35 Go 的编译器对声明却未使用的变量在报错。下面的代码 会产生这个错误:声明了 i 却未使用 package main func main() { var i int } 布尔类型 布尔类型表示由预定义的常量 true 和 false 代表的布尔判 定值。布尔类型是 bool。 数字类型 Go 有众所周知的类型如 int,这个类型根据你的硬件决定 适当的长度。意味着在 32 位硬件上,是 32 位的;在 64 位硬件上是 64 位的。注意:int 是 32 或 64 位之一,不会 定义成其他值。uint 情况相同。 18 Chapter 2: 基础 如果你希望明确其长度,你可以使用 int32 或 者 uint32 。完整的整数类型列表(符号 和 无 符 号 ) 是 int8 ,int16 ,int32 ,int64 和 byte,uint8,uint16,uint32,uint64。byte 是 uint8 的别名。浮点类型的值有 float32 和 float64 (没有 float 类型)。64 位的整数和浮点数总是 64 位的,即便是在 32 位的架构上。 需要留意的是这些类型全部都是独立的,并且混合用 这些类型向变量赋值会引起编译器错误,例如下面的代 码: Listing 2.4. Familiar types are still distinct 1package main 3func main() { 4var a int ← Genericintegertype 5var b int32 ← 32bitsintegertype 6a = 15 7b = a + a ← Illegalmixingofthesetypes 8b = b + 5 ← 5 isa (typeless)constant,sothisisOK 9} 在行 7 触发一个赋值错误: 变量、类型和保留字 19 types.go:7:cannotusea+ a (typeint)astypeint32 inassignment 赋值可以用八进制、十六进制或科学计数法:077,0xFF ,1e3 或者 6.022e23 这些都是合法的。 常量 常量在 Go 中,也就是 constant。它们在编译时被创建, 只能是数字、字符串或布尔值;const x = 42 生成 x 这个 常量。可以使用 iota b 生成枚举值。 const ( a = iota b = iota ) 第一个 iota 表示为 0,因此 a 等于 0,当 iota 再次在新的 一行使用时,它的值增加了 1,因此 b 的值是 1。 也可以像下面这样,省略 Go 重复的 = iota: const ( a = iota b单词 [iota] 在日常英语短语“not one iota”,意思是“不是最小”,是 来自新约中的短语:“until heaven and earth pass away, not an iota, not a dot, will pass from the Law.”[35] 20 Chapter 2: 基础 b ← Implicitlyb= iota ) 如果需要,可以明确指定常量的类型: const ( a = 0 ← Isan int now b string = "0" ) 字符串 另一个重要的内建类型是 string。赋值字符串的例子: s :="Hello World !" 字符串在 Go 中是 UTF-8 的由双引号(”)包裹的字符序 列。如果你使用单引号(’)则表示一个字符(UTF-8编 码)——这种在 Go 中不是 string。 一旦给变量赋值,字符串就不能修改了:在 Go 中字符串 是不可变的。从 C 来的用户,下面的情况在 Go 中是非法 的。 var s string = "hello" s[0] = 'c' ← 修改第一个字符为'c',这会报错 变量、类型和保留字 21 在 Go 中实现这个,需要下面的方法: s :="hello" c :=[] byte(s) ..0 c[0] = 'c' ..1 s2 := string(c) ..2 fmt.Printf("%s\n", s2) ..3 ..0 转换s 为字节数组,查阅在第5 章”转换”节、134 页 的内容; ..1 修改数组的第一个元素; ..2 创建新的字符串s2 保存修改; ..3 用fmt.Printf 函数输出字符串。 多行字符串 基于分号的置入(查阅文档 [12]的“分号”章节),你需 要小心使用多行字符串。如果这样写: s :="Starting part" + "Ending part" 会被转换为: 22 Chapter 2: 基础 s :="Starting part" ; + "Ending part"; 这是错误的语法,应当这样写: s :="Starting part" + "Ending part" Go 就不会在错误的地方插入分号。另一种方式是使用 反引号 ` 作为原始字符串符号: s :=`Starting part Ending part` 留意最后一个例子 s 现在也包含换行。不像转义字符 串标识 ,原始字符串标识的值在引号内的字符是不转 义的。 rune Rune 是 int32 的别名。用UTF-8 进行编码。这个类型在什 么时候使用呢?例如需要遍历字符串中的字符。可以循 环每个字节(仅在使用US ASCII 编码字符串时与字符等 价,而它们在Go 中不存在!)。因此为了获得实际的字 符,需要使用rune 类型。 运算符和内建函数 23 复数 Go 原生支持复数。它的变量类型是 complex128 (64 位虚 数部分)。如果需要小一些的,还有 complex64 – 32 位的 虚数部分。复数写为 re+ im i,re 是实数部分,im 是虚 数部分,而 i 是标记 ’i’( √−1)。使用复数的一个例子: var c complex64 = 5+5i;fmt.Printf("Valueis:%v",c) 将会打印:(5+5i) 错误 任何足够大的程序或多或少都会需要使用到错误报告。 因此 Go 有为了错误而存在的内建类型,叫做 error。var a error 定义了a 为一个error,a 的值是nil。 运算符和内建函数 Go 支持普通的数字运算符,表格 2.1 列出了当前支持的 运算符,以及其优先级。它们全部是从左到右结合的。 24 Chapter 2: 基础 Table 2.1. 运算优先级 Precedence Operator(s) Highest * / % << >> & &^ + - | ^ == != < <= > >= <- && Lowest || + - * / 和 % 会像你期望的那样工作,& | ^ 和 &^ 分别 表示位运算符按位与,按位或,按位异或和位清除。&& 和 || 运算符是逻辑与和逻辑或。表格中没有列出的是逻 辑非:!。 虽然 Go 不支持运算符重载(或者方法重载),而一些内 建运算符却支持重载。例如 + 可以用于整数、浮点数、复 数和字符串(字符串相加表示串联它们)。 Go 保留字 25 Go 保留字 Table 2.2. Go 中的保留字 break default func interfaceselect case defer go map struct chan else goto packageswitch const fallthroughif range type continuefor importreturn var 表格 2.2 列出了 Go 中所有的保留字。在下面的段落和章 节中会介绍它们。其中有一些已经遇到过了。 • var 和 const 参阅“变量、类型和保留字”在第 15 页; • 在“Hello World”部分,package 和 import 已经有过 短暂的接触。在第 4 章对其有详细的描述。 其他都有对应的介绍和章节: • func 用于定义函数和方法; 26 Chapter 2: 基础 • return 用于从函数返回,func 和 return 参阅第 3 章 了解详细信息; • go 用于并行(第 7 章); • select 用于选择不同类型的通讯,参阅第 7 章; • interface 参阅第 6 章; • struct 用于抽象数据类型,参阅第 5 章; • type 同样参阅第 5 章。 控制结构 在 Go 中只有很少的几个控制结构c 。例如这里没有 do 或者 while 循环,只有 for。有(灵活的)switch 语句 和 if,而switch 接受像 for 那样可选的初始化语句。还 有叫做类型选择和多路通讯转接器的 select(参阅第 7 章)。语法有所不同(同 C 相比):无需圆括号,而语句 体必须总是包含在大括号内。 c这个章节复制于 [12]。 控制结构 27 if-eles 在 Go 中 if 看起来是这样的: if x > 0 { ← { ismandatory return y } else { return x } 强制大括号鼓励将简单的 if 语句写在多行上。无论如 何,这都是一个很好的形式,尤其是语句体中含有控制 语句,例如 return 或者 break。 if 和 switch 接受初始化语句,通常用于设置一个(局 部)变量。 if err :=file.Chmod(0664) ; err != nil { ← nilislikeC'sNULL log.Stderr(err) ← Scopeoferrislimitedto if'sbody return err } 可以像通常那样使用逻辑运算符(参考 2.1 表格): if true &&true { 28 Chapter 2: 基础 println("true") } if ! false { println("true") } 在 Go 库中,你会发现当一个 if 语句不会进入下一个语 句流程 – 也就是说,语句体结束于 break,continue,goto 或者 return – 不必要的 else 会被省略。 f, err :=os.Open(name, os.O_RDONLY, 0) if err != nil { return err } doSomething(f) 这个例子通常用于检测可能的错误序列。成功的流程 一直执行到底部使代码很好读,当遇到错误的时候就排 除它。这样错误的情况结束于 return 语句,这样就无 须 else 语句。 f, err :=os.Open(name, os.O_RDONLY, 0) if err != nil { return err } d, err :=f.Stat() 控制结构 29 if err != nil { return err } doSomething(f, d) 下面的语法在 Go 中是非法的: if err != nil { ← 必须同 if 在同一行 return err } 参阅文档[12]的“分号”章节了解其后更深入的原因。 if-then-else 的结尾 注意如果在函数中这样结束: if err != nil { return err } else { return nil } 它不会编译。这是 Go 编译器的一个 bug。参阅 [21] 了 解更多关于此问题的描述,以及可能的修复。 30 Chapter 2: 基础 goto Go 有 goto 语句——明智的使用它。用 goto 跳转到一定是 当前函数内定义的标签。例如假设这样一个循环: func myfunc() { i := 0 Here: ← 这行的第一个词,以分号结束作为标签 println(i) i++ goto Here ← 跳转 } 标签名是大小写敏感的。 for Go 的 for 循环有三种形式,只有其中的一种使用分号。 for init; condition; post {} ← 和 C 的 for 一样 for condition {} ← 和 while 一样 for { } ← 和 C 的 for(;;) 一样(死循环) 控制结构 31 短声明使得在循环中声明一个序号变量更加容易。 sum :=0 for i := 0; i < 10; i++ { sum +=i ← sum=sum+i 的简化写法 } ← i 实例在循环结束会消失 最后,由于 Go 没有逗号表达式,而 ++ 和 – 是语句而不是 表达式,如果你想在 for 中执行多个变量,应当使用 平 行赋值。 // Reversea for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 { ← 平行赋值 a[i], a[j] = a[j], a[i] ← 这里也是 } break 和 continue 利用 break 可以提前退出循环,break 终止当前的循环。 for i := 0; i < 10; i++ { if i > 5 { break ← 终止这个循环,只打印 0 到 5 } println(i) 32 Chapter 2: 基础 } 循环嵌套循环时,可以在 break 后指定标签。用标签决定 哪个循环被终止: J: for j := 0; j < 5; j++ { for i :=0 ; i < 10; i++ { if i > 5 { break J ← 现在终止的是j 循环,而不是i 的那个 } println(i) } } 利用 continue 让循环进入下一个迭代,而略过剩下的所 有代码。下面循环打印了 0 到 5。 for i := 0; i < 10; i++ { if i > 5 { continue ← 跳过循环中所有的代码println(i) 控制结构 33 range 保 留 字 range 可 用 于 循 环 。 它 可 以 在 slice 、array 、string 、map 和 channel ( 参 阅 第 7 章)。range 是个迭代器,当被调用的时候,从它循环的 内容中返回一个键值对。基于不同的内容,range 返回不 同的东西。 当对 slice 或者 array 做循环时,range 返回序号作为键, 这个序号对应的内容作为值。考虑这个代码: list :=[] string{"a", "b", "c", "d", "e", "f"} ..0 for k, v := range list {..1 // 对 k 和 v 做想做的事情 ..2 } ..0 创 建 一 个 字 符 串 的slice ( 参 阅“array 、slices 和 map”的第 40 页)。 ..1 用 range 对其进行循环。每一个迭代,range 将返 回 int 类型的序号,string 类型的值,以 0 和“a”开 始。 ..2 k 的值为 0…5,而 v 在循环从“a”…“f”。 34 Chapter 2: 基础 也可以在字符串上直接使用 range。这样字符串被打散 成独立的 Unicode 字符d并且起始位按照 UTF-8 解析。循 环: for pos, char := range "aΦx" { fmt.Printf("character '%c' starts at byte position %d\n", char, pos) } 打印 character'a'startsatbyteposition0 character' Φ' startsatbyteposition1 character'x'startsatbyteposition3 ← Φ took2bytes switch Go 的 switch 非常灵活。表达式不必是常量或整数,执 行的过程从上至下,直到找到匹配项,而如果 switch 没有表达式,它会匹配 true 。这产生一种可能——使 用 switch 编写 if-else-if-else 判断序列。 d在 UTF-8 世界的字符有时被称作 runes。通常,当人们讨论字符 时,多数是指 8 位字符。UTF-8 字符可能会有 32 位,称作 rune。在这个 例子里,char 的类型是rune。 控制结构 35 func unhex(c byte) byte { switch { case '0' <=c && c <='9': return c - '0' case 'a' <=c && c <='f': return c - 'a' + 10 case 'A' <=c && c <='F': return c - 'A' + 10 } return 0 } 它不会匹配失败后自动向下尝试,但是可以使 用fallthrough 使其这样做。没有 fallthrough: switch i { case 0: // 空的case 体 case 1: f() // 当i ==0 时,f 不会被调用! } 而这样: switch i { case 0: fallthrough case 1: 36 Chapter 2: 基础 f() // 当i ==0 时,f 会被调用! } 用 default 可以指定当其他所有分支都不匹配的时候的行 为。 switch i { case 0: case 1: f() default: g() // 当i 不等于0 或1 时调用 } 分支可以使用逗号分隔的列表。 func shouldEscape(c byte) bool { switch c { case ' ', '?', '&', '=', '#', '+': ← , as"or" return true } return false } 这里有一个使用两个 switch 对字节数组进行比较的例 子: 控制结构 37 // 比较返回两个字节数组字典数序先后的整数。 // 如果a ==b 返回0,如果a < b 返回-1,而如果a > b 返回+1 func Compare(a, b []byte) int { for i := 0; i < len(a) &&i < len(b); i++ { switch { case a[i] > b[i]: return 1 case a[i] < b[i]: return -1 } } // 长度不同,则不相等 switch { case len(a) < len (b): return -1 case len(a) > len (b): return 1 } return 0 // 字符串相等 } 38 Chapter 2: 基础 内建函数 预定义了少数函数,这意味着无需引用任何包就可以使 用它们。表格 2.3 列出了所有的内建函数。e Table 2.3. Go 中的预定义函数 closenew panic complex deletemakerecoverreal len appendprintimag cap copy println close 用于 channel 通讯。使用它来关闭 channel,参阅 第 7 章了解更多。 delete 用于在 map 中删除实例。 len 和 cap 可 用 于 不 同 的 类 型 ,len 用 于 返 回 字 符 串、slice 和数组的长度。参阅“array、slices 和 map”小 节了解更多关于 slice、数组和函数 cap 的详细信息。 e可以使用命令 go doc builtin 获得关于内建类型和函数的在线文 档。 内建函数 39 new 用于各种类型的内存分配。参阅“用 new 分配内 存”的第 124 页。 make 用于内建类型(map、slice 和 channel)的内存分 配。参阅“用 make 分配内存”的第 125 页。 copy 用于复制 slice。参阅本章的“slice”。 append 用于追加 slice。参阅本章的“slice”。 panic 和 recover 用于异常处理机制。参阅“恐 慌 (Panic)和恢复(Recover)”的第 78 页了解更多信息。 print 和 println 是底层打印函数,可以在不引入 fmt 包的情况下使用。它们主要用于调试。 complex、real 和 imag 全部用于处理复数。有了之前给 的简单的例子,不用再进一步讨论复数了。 40 Chapter 2: 基础 array、slices 和 map 可以利用 array 在列表中进行多个值的排序,或者使用更 加灵活的:slice。字典或哈希类型同样可以使用,在 Go 中叫做 map。 array array 由 [n] 定义,n 标示 array 的长度,而 标示希望存储的内容的类型。对 array 的元素赋值或索引 是由方括号完成的: var arr [10]int arr[0] = 42 arr[1] = 13 fmt.Printf("The first element is%d\n", arr[0]) 像 var arr=[10] int 这样的数组类型有固定的大小。大 小是类型的一部分。由于不同的大小是不同的类型,因 此不能改变大小。数组同样是值类型的:将一个数组赋 值给另一个数组,会复制所有的元素。尤其是当向函数 内传递一个数组的时候,它会获得一个数组的副本,而 不是数组的指针。 可以像这样声明一个数组:var a [3]int,如果不使用零 来初始化它,则用复合声明:a :=[3] int{1,2,3 } 也可 array、slices 和 map 41 以简写为a :=[...] int{1,2,3 },Go 会自动统计元素的 个数。 注意,所有项目必须都指定。因此,如果你使用 复 合 声 明 允 许 你 直 接 将 值 赋 值 给 ar- ray、slice 或 者 map。 参 阅 第 127 页 的“构 造 函 数 与 复 合 声 明”了 解 更 多 信息。 多维数组,有一些内容你必须录入: a :=[2][2] int{ [2]int{1,2}, [2]int{ 3,4}} 类似于: a :=[2][2] int{ [...]int{1,2}, [...] int{3,4}} 当声明一个 array 时,你必须在方括号内输入些内容,数 字或者三个点(...)。 从发布版 2010-10-27[16] 开始这个语 Go 发 布 版 2010- 10- 27[16]。 法就变得更加简单了。来自于发布记录: array、slice 和 map 的复合声明变得更加简 单。使用复合声明的array、slice 和 map,元 素复合声明的类型与外部一直,则可以省 略。 这表示上面的例子可以修改为: a :=[2][2] int{ {1,2},{3,4}} slice TODO Add push/pop to this sec- tion as contain- er/vector will be depri- cated. slice 与 array 接近,但是在新的元素加入的时候可以增 加长度。slice 总是指向底层的一个 array。slice 是一个指 42 Chapter 2: 基础 向 array 的指针,这是其与 array 不同的地方;slice 是引 用类型, 这意味着当赋值某个 slice 到另外一个变量,两引 用 类 型 使 用 make 创建。 个引用会指向同一个 array。例如,如果一个函数需要一 个 slice 参数,在其内对 slice 元素的修改也会体现在函数 调用者中,这和传递底层的 array 指针类似。通过: sl := make([]int , 10) 创建了一个保存有 10 个元素的 slice。需要注意的是底层 的 array 并无不同。slice 总是与一个固定长度的 array 成 对出现。其影响 slice 的容量和长度。 图 2.1 描述了下面 的 Go 代码。首先创建了 m 个元素长度的array,元素类 型 int:var array[m]int 然后对这个 array 创建 slice:slice:=array[0:n] 然后现在有: • len(slice)==n ; • cap(slice)==m ; • len(array)== cap(array)==m . array、slices 和 map 43 Figure 2.1. array 与slice 对比 . . . len == cap == m array slice 0 0 n-1 n-1 m-1 len == n . . . m-1 cap == m 给定一个 array 或者其他 slice,一个新 slice 通过 a[I:J] 的方式创建。这会创建一个新的 slice,指向变量 a,从序 号 I 开始,结束在序号 J之前。长度为 J-I。 // array[n:m] 从 array 创建了一个 slice,具有元 素 n 到 m-1 a :=[...] int{1, 2, 3, 4, 5}..0 s1 :=a[2:4] ..1 s2 :=a[1:5] ..2 s3 :=a[:] ..3 44 Chapter 2: 基础 s4 :=a[:4] ..4 s5 :=s2[:] ..5 ..0 定义一个 5 个元素的 array,序号从 0 到 4; ..1 从序号 2 至 3 创建 slice,它包含元素 3,4 ; ..2 从序号 1 至 4 创建,它包含元素 2,3,4,5 ; ..3 用 array 中的所有元素创建 slice,这是 a[0:len(a)] 的简化写法; ..4 从序号 0 至 3 创建,这是 a[0:4] 的简化写法,得 到 1,2,3,4 ; ..5 从 slice s2 创建 slice,注意 s5 仍然指向 array a。 在 2.5 列出的代码中,我们在第八行尝试做一些错误的事 情,让一些东西超出范围(底层 array 的最大长度),然 后得到了一个运行时错误。 Listing 2.5. array 和slice 1package main 3func main() { array、slices 和 map 45 4var array [100]int // Createarray, indexfrom0to99 5slice := array[0:99] // Createslice, indexfrom0to98 7slice[98] = 'a' // OK 8slice[99] = 'a' // Error:"throw: indexoutofrange" 9} 如果你想要扩展 slice,有一堆内建函数让你的日子更加 好过一些:append 和 copy。来自于 [14]: 函数 append 向 slice s 追加零值或其他 x 值, 并且返回追加后的新的、与 s 有相同类型 的 slice。如果 s 没有足够的容量存储追加的 值,append 分配一个足够大的、新的 slice 来 存放原有 slice 的元素和追加的值。因此,返 回的 slice 可能指向不同的底层 array。 s0 :=[] int{0, 0} s1 := append(s0, 2) ..0 s2 := append(s1, 3, 5, 7) ..1 s3 := append(s2, s0...) ..2 46 Chapter 2: 基础 ..0 追加一个元素,s1==[]int{0,0,2} ; ..1 追加多个元素,s2==[]int{0,0,2,3,5,7} ; ..2 追加一个slice,s3==[]int{0,0,2,3, 5,7,0, 0}。注意这三个点! 还有 函数 copy 从源 slice src 复制元素到目标 dst ,并且返回复制的元素的个数。源和目标可 能重叠。复制的数量是 len(src) 和 len(dst) 中的最小值。 var a = [...] int{0, 1, 2, 3, 4, 5, 6, 7} var s = make([]int , 6) n1 := copy(s, a[0:]) ← n1==6,s ==[]int{0,1,2,3,4,5} n2 := copy(s, s[2:]) ← n2==4,s ==[]int{2,3,4,5,4,5} map 许多语言都内建了类似的类型,例如 Perl 有 哈 希,Python 有字典,而 C++ 同样也有 map(作为库)。 array、slices 和 map 47 在 Go 中有map 类型。map 可以认为是一个用字符串做索引 的数组(在其最简单的形式下)。下面定义了 map 类型, 用于将 string (月的缩写)转换为 int – 那个月的天数。 一般定义 map 的方法是:map[] monthdays := map[string]int{ "Jan": 31, "Feb": 28,"Mar": 31, "Apr": 30, "May": 31,"Jun": 30, "Jul": 31, "Aug": 31,"Sep": 30, "Oct": 31, "Nov": 30,"Dec": 31, ← 逗号是必须的 } 留意,当只需要声明一个 map 的时候,使用 make 的形 式:monthdays:= make(map[string]int) 当在 map 中索引(搜索)时,使用方括号。例如打印 出 12 月的天数:fmt.Printf("%d\n",monthdays["Dec"]) 当 对 array 、slice 、string 或 者 map 循 环 遍 历 的 时 候,range 会帮助你,每次调用,它都会返回一个键和 对应的值。 year :=0 for _, days := range monthdays { ← 键没有使用,因此用 _,days year +=days } 48 Chapter 2: 基础 fmt.Printf("Numbers ofdays ina year: %d\n", year) 向 map 增加元素, 可以这样做: monthdays["Undecim"] = 30 ← 添加一个月 monthdays["Feb"] = 29 ← 闰年时重写这个元素 检查元素是否存在,可以使用下面的方式[27]: var value int var present bool value, present = monthdays["Jan"] ← 如果存在,present 则有值true ← 或者更接近Go 的方式 v, ok:=monthdays["Jan"] ← “逗号ok”形式 也可以从 map 中移除元素: delete(monthdays, "Mar") ← 删除"Mar" 吧,总是下雨的月份 通常来说语句 delete(m,x) 会删除 map 中由 m[x] 建立的 实例。 练习 49 练习 Q2. (1) For-loop 1. 创建一个基于 for 的简单的循环。使其循环 10 次,并且使用 fmt 包打印出计数器的值。 2. 用 goto 改写 1 的循环。保留字 for 不可使用。 3. 再次改写这个循环,使其遍历一个 array,并将这 个 array 打印到屏幕上。 Q3. (1) FizzBuzz 1. 解决这个叫做 Fizz-Buzz[31] 的问题: 编写一个程序,打印从 1 到 100 的数 字。当是三个倍数就打印“Fizz”代替 数字,当是五的倍数就打印“Buzz”。 当数字同时是三和五的倍数时,打 印“FizzBuzz”。 Q4. (1) Strings 1. 建立一个 Go 程序打印下面的内容(到 100 个字 符): 50 Chapter 2: 基础A AA AAA AAAA AAAAA AAAAAA AAAAAAA ... 2. 建立一个程序统计字符串里的字符数量: asSASAddddsjkdsjsdk 同时输出这个字符串的字节数。提示: 看看 uni- code/utf8 包。 3. 扩展上一个问题的程序,替换位置 4 开始的三个 字符为 “abc”。 4. 编写一个 Go 程序可以逆转字符串,例如“foobar” 被打印成“raboof”。提示:不幸的是你需要知道一 些关于转换的内容,参阅“转换”第 134 页的内容。 Q5. (4) Average 1. 编写计算一个类型是 float64 的 slice 的平均值的 代码。在稍候的练习 Q6 中将会改写为函数。 float64 的 slice 的平均值的代码。在稍候的练习 Q6 中。 答案 51 答案 A2. (1) For-loop 1. 有许多种解法,其中一种可能是: Listing 2.6. Simple for loop package main import "fmt" func main() { for i :=0 ; i < 10; i++ { ← Seesection for onpage 30 fmt.Printf("%d\n", i) } } 编译并观察输出。 52 Chapter 2: 基础% gobuildfor.go %./for 0 1 . . . 9 2. 改写的循环最终看起来像这样(仅显示了 main 函 数): func main() { i :=0 ← 定义循环变量 I: ← 定义标签 fmt.Printf("%d\n", i) i++ if i < 10 { goto I ← 跳转回标签 } } 3. 下面是可能的解法之一: Listing 2.7. For loop with an array func main() { 答案 53 var arr [10]int ← Createanarraywith10elements for i :=0 ; i < 10; i++ { arr[i] = i ← Fillitonebyone } fmt.Printf("%v", arr) ← With%vGoprintsthetype } 也可以用复合声明的硬编码来实现这个: a := [...] int{0,1,2,3,4,5,6,7,8,9} ← 通过 [...] 让 Go 来计数 fmt.Printf("%v\n", a) A3. (1) FizzBuzz 1. 下面简单的程序,是一种解决办法。 Listing 2.8. Fizz-Buzz package main import "fmt" 54 Chapter 2: 基础 func main() { const ( FIZZ = 3 ..0 BUZZ = 5 ) var p bool ..1 for i :=1 ; i < 100 ; i++ {..2 ; p = false if i%FIZZ == 0 {..3 fmt.Printf("Fizz") p = true } if i%BUZZ == 0 {..4 fmt.Printf("Buzz") p = true } if! p {..5 fmt.Printf("%v", i) } fmt.Println() ..6 } } 答案 55 ..0 为了提高代码的可读性,定义两个常量。参 阅“常量”; ..1 判断是否需要打印内容; ..2 for 循环,参阅“for” ..3 如果能被FIZZ 整除,打印“Fizz”; ..4 如 果 能 被BUZZ 整 除 , 打 印“Buzz” 。 注 意,FizzBuzz 的情况已经被处理了; ..5 如 果FIZZ 和BUZZ 都 没 有 打 印 , 打 印 原 始 值; ..6 换行。 A4. (1) Strings 1. 这是一个解法: Listing 2.9. Strings package main import "fmt" func main() { str :="A" 56 Chapter 2: 基础 for i :=0 ; i < 100 ; i++ { fmt.Printf("%s\n", str) str = str + "A" ← Stringconcatenation } } 2. 为了解决这个问题,需要 unicode/utf8 包的帮助。 首先,阅读一下文档 godocunicode/utf8|less 。 在阅读文档的时候,会注意到func RuneCount(p []byte)int。然后,将 string 转换为 byte slice: str:="hello" b := []byte(str) ← 转换,参阅第 134 页 将这些整合到一起,得到下面的程序。 答案 57 Listing 2.10. Runes in strings package main import ( "fmt" "unicode/utf8" ) func main() { str :="dsjkdshdjsdh....js" fmt.Printf("String %s\nLength: %d,Runes: % d\n", str, len([]byte(str)), utf8.RuneCount([] byte(str))) } 3. 可以用下面的方法逆转字符串。我们从左边(i) 至右(j)的交换字符,就像这样: 58 Chapter 2: 基础 Listing 2.11. Reverse a string import "fmt" func main() { s :="foobar" a :=[] byte(s) ← Againaconversion // Reversea for i, j :=0, len(a)-1; i < j; i, j = i+1, j-1 { a[i], a[j] = a[j], a[i] ← Parallelassignment } fmt.Printf("%s\n", string(a)) ← Convertitback } A5. (4) Average 1. 下面的代码计算了平均值。 sum:=0.0 switch len(xs) { case 0: ..0 avg = 0 答案 59 default:..1 for _, v := range xs { sum +=v } avg = sum/ float64(len (xs)) ..2 } ..0 如果长度是零,返回 0; ..1 否则计算平均值; ..2 为了能够进行除法,必须将值转换 为 float64。 3 函数 “我总是兴奋于阳光的轻抚和沉寂在早期编程 语言中。无需太多文字;许多已经完成了。旧 的程序阅读起来就像是同表达良好的研究工作 者或受到良好训练的机器同事沟通一样,而不 是与编译器争论。谁愿意让其成熟到发出这样 的声音呢?” RICHARDP.GABRIEL 函数是构建 Go 程序的基础部件;所遇有趣的事情都是在 它其中发生的。函数的定义看起来像这样: Listing 3.1. 函数定义 .. .. 0 . .. 1 . .. 2 . .. 3 . .. 4 . .. 5 type mytype int ← 新的类型,参阅第 5 章 func (p mytype) funcname(q int) (r,s int){ return 0,0 } 函数 61 ..0 保留字 func 用于定义一个函数; ..1 函数可以定义用于特定的类型,这类函数更加通俗 的称呼是 method。这部分称作 receiver 而它是可选 的。它将在第 6 章使用; ..2 funcname 是你函数的名字; ..3 int 类型的变量 q 作为输入参数。参数用 pass-by- value 方式传递,意味着它们会被复制; ..4 变量 r 和 s 是这个函数的 命名返回值。在 Go 的 函数中可以返回多个值。参阅第 67 页的“多值返 回”。如果不想对返回的参数命名,只需要提供类 型:(int,int)。如果只有一个返回值,可以省略圆 括号。如果函数是一个子过程,并且没有任何返回 值,也可以省略这些内容; ..5 这是函数体,注意 return 是一个语句,所以包裹参 数的括号是可选的。 这里有两个例子,左边的函数没有返回值,右边的只是 简单的将输入返回。 62 Chapter 3: 函数 func subroutine(in int) { return } func identity(in int) int { return in } 可以随意安排函数定义的顺序,编译器会在执行前扫描 每个文件。所以函数原型在 Go 中都是过期的旧物。Go 不 允许函数嵌套。然而你可以利用匿名函数实现它,参阅 本章第 75 页的“函数作为值”。 递归函数跟其他语言是一样的: Listing 3.2. 递归函数 func rec(i int){ if i ==10 { return } rec(i+1) fmt.Printf("%d ", i) } 这会打印:9 8 7 6 5 4 3 2 1 0。 作用域 63 作用域 在 Go 中,定义在函数外的变量是全局的,那些定义在函 数内部的变量,对于函数来说是局部的。如果命名覆盖 ——一个局部变量与一个全局变量有相同的名字——在 函数执行的时候,局部变量将覆盖全局变量。 64 Chapter 3: 函数 Listing 3.3. 局部作用域 . package main var a = 6 func main() { p() q() p() } func p() { println(a) } func q() { a := 5 ← 定义 println(a) } 作用域 65 Listing 3.4. 全局作用域 . package main var a = 6 func main() { p() q() p() } func p() { println(a) } func q() { a = 5 ← 赋值 println(a) } 在 3.3 中定义了函数 q() 的局部变量 a。局部变量 a 仅 在 q() 中可见。这也就是为什么代码会打印:656。在 3.4 66 Chapter 3: 函数 中没有定义局部变量,只有全局变量 a。这将使得对 a 的 赋值全局可见。这段代码将会打印:655。 在下面的例子中,我们在 f() 中调用 g(): Listing 3.5. 当函数调用函数时的作用域 package main var a int func main() { a = 5 println(a) f() } func f() { a := 6 println(a) g() } func g() { println(a) } 多值返回 67 输出内容将是:565。局部变量仅仅在执行定义它的函数 时有效。 多值返回 Go 一个非常特别的特性(对于编译语言而言)是函数和 方法可以返回多个值(Python 和 Perl 同样也可以)。这 可以用于改进一大堆在 C 程序中糟糕的惯例用法:修改 参数的方式,返回一个错误(例如遇到 EOF 则返回 -1)。 在Go 中,Write 返回一个计数值和一个错误:“是的,你 写入了一些字节,但是由于设备异常,并不是全部都写 入了。”。os 包中的 *File.Write 是这样声明的: func (file *File) Write(b [] byte) (n int, err error ) 如同文档所述,它返回写入的字节数,并且当 n != len(b ) 时,返回非 nil 的 error。这是 Go 中常见的方式。 类似的方法避免了传递指针模拟引用参数来返回值。这 里有个样例函数,从字节数组的指定位上取得数值,返 回这个值和下一个位置。 func nextInt(b []byte, i int )(int , int){ x := 0 // 假设所有的都是数字 68 Chapter 3: 函数 for ; i < len (b); i++ { x = x*10 + int(b[i])-'0' } return x, i } 你可以在输入的数组中扫描数字,像这样: a :=[] byte{'1', '2', '3', '4'} var x int for i := 0; i < len(a);{ ← 没有 i++ x, i = nextInt(a, i) println(x) } 没有元组作为原生类型,多返回值可能是最佳的选择。 你可以精确的返回希望的值,而无须重载域空间到特定 的错误信号上。 命名返回值 Go 函数的返回值或者结果参数可以指定一个名字,并且 像原始的变量那样使用,就像输入参数那样。如果对其 命名,在函数开始时,它们会用其类型的零值初始化; 如果函数在不加参数的情况下执行了 return 语句,结 命名返回值 69 果参数的当前值会作为返回值返回。用这个特性,允许 (再一次的)用较少的代码做更多的事a。 名字不是强制的,但是它们可以使得代码更加健壮和清 晰:这是文档。例如命名 int 类型的 nextPos 返回值,就 能说明哪个代表哪个。 func nextInt(b []byte, pos int) (value, nextPos int ){/* ...*/ } 由于命名结果会被初始化并关联于无修饰的 return,它 们可以非常简单并且清晰。这里有一段 io.ReadFull 的代 码,很好的运用了它: func ReadFull(r Reader, buf[] byte ) (n int, err error){ for len (buf) > 0 &&err==nil { var nr int nr, err = r.Read(buf) n +=nr buf = buf[nr:len(buf)] } return } 在下面的例子中,定义了一个简单的函数,用于计算 值 x 这 一 节 的 部 分 内 容 来 自 [5]。 a这是 Go 的格言:“用更少的代码做更多的事”。 70 Chapter 3: 函数 的阶乘。 func Factorial(x int) int { ← funcFactorial(xint)(int) 同样也行 if x ==0 { return 1 } else { return x * Factorial(x - 1) } } 所以,也可以将函数编写为: func Factorial(x int) (result int){ if x ==0 { result = 1 } else { result = x * Factorial(x - 1) } return } 当命名了返回值,代码变得健壮并且易读。同样也可以 编写一个多返回值的函数: func fib(n) (val, pos int){ ← 都是int if n ==0 { 延迟代码 71 val = 1 pos = 0 } else if n == 1 { val = 1 pos = 1 } else { v1, _ :=fib(n-1) v2, _ :=fib(n-2) val = v1 + v2 pos = n } return } 延迟代码 假设有一个函数,打开文件并且对其进行若干读写。 在这样的函数中,经常有提前返回的地方。如果你这样 做,就需要关闭正在工作的文件描述符。这经常导致产 生下面的代码: Listing 3.6. 没有defer func ReadWrite() bool { 72 Chapter 3: 函数 file.Open("file") // 做一些工作 if failureX { file.Close() ← return false } if failureY { file.Close() ← return false } file.Close() ← return true } 在这里有许多重复的代码。为了解决这些,Go 有了 defer 语句。在 defer 后指定的函数会在函数退出前调用。 上面的代码可以被改写为下面这样。将 Close 对应的放置 于 Open 后,能够使函数更加可读、健壮。 Listing 3.7. With defer func ReadWrite() bool { file.Open("file") defer file.Close() ← file.Close() 被添加到了 defer 列表 延迟代码 73 // 做一些工作 if failureX { return false ← Close() 现在自动调用 } if failureY { return false ← 这里也是 } return true } 可以将多个函数放入“延迟列表”中,这个例子来自 [12]: for i := 0; i < 5; i++ { defer fmt.Printf("%d ", i) } 延迟的函数是按照后进先出(LIFO)的顺序执行,所以 上面的代码打印:4 3 2 1 0。 利用 defer 甚至可以修改返回值,假设正在使用命名结果 参数和函数符号 b,例如: Listing 3.8. 函数符号 defer func(){ /* ...*/ b函数符号也就是被叫做闭包的东西。 74 Chapter 3: 函数 }() ← () 在这里是必须的 或者这个例子,更加容易了解为什么,以及在哪里需要 括号: Listing 3.9. 带参数的函数符号 defer func(x int){ /* ...*/ }(5) ← 为输入参数 x 赋值5 在这个(匿名)函数中,可以访问任何命名返回参数: Listing 3.10. 在defer 中访问返回值 func f() (ret int){ ← ret 初始化为零 defer func () { ret++ ← ret 增加为1 }() return 0 ← 返回的是 1 而不是 0! } 变参 接受变参的函数是有着不定数量的参数的。为了做到这 点,首先需要定义函数使其接受变参: 函数作为值 75 func myfunc(arg ...int){} arg... int 告诉 Go 这个函数接受不定数量的参数。注 意,这些参数的类型全部是 int。在函数体中,变量arg 是一个 int 类型的 slice: for _, n := range arg { fmt.Printf("And the number is: %d\n", n) } 如果不指定变参的类型,默认是空的接口 interface{} (参阅第 6 章)。假设有另一个变参函数叫做 myfunc2, 下面的例子演示了如何向其传递变参: func myfunc(arg ...int){ myfunc2(arg...) ← 按原样传递 myfunc2(arg[:2]...) ← 传递部分 } 函数作为值 就像其他在 Go 中的其他东西一样,函数也是值而已。它 们可以像下面这样赋值给变量: Listing 3.11. 匿名函数 76 Chapter 3: 函数 func main() { a := func(){ ← 定义一个匿名函数,并且赋值给 a println("Hello") } ← 这里没有 () a() ← 调用函数 } 如果使用 fmt.Printf("\%T\n",a) 打印 a 的类型,输出结 果是 func()。 函数作为值,也会被用在其他一些地方,例如map。这里 将整数转换为函数: Listing 3.12. 使用map 的函数作为值 var xs= map [int]func() int{ 1: func() int{ return 10 }, 2: func() int{ return 20 }, 3: func() int{ return 30 }, ← 必须有逗号 /* ...*/ } 也可以编写一个接受函数作为参数的函数,例如用于操 作 int 类型的 slice 的 Map 函数。这是一个留给读者的练 习,参考在第 83 页的练习 Q12。 回调 77 回调 当函数作为值时,就可以很容易的传递到其他函数里, 然后可以作为回调。首先定义一个函数,对整数做一 些“事情”: func printit(x int){ ← 函数无返回值 fmt.Print("%v\n", x) ← 仅仅打印 } 这个函数的标识是 func printit(int),或者没有函数名 的:func(int)。创建新的函数使用这个作为回调,需要 用到这个标识: func callback(y int, f func (int)){ ← f 将会保存函数 f(y) ← 调用回调函数 f 输入变量 y } 78 Chapter 3: 函数 恐慌(Panic)和恢复(Recover) c Go 没有像 Java 那样的异常机制:不能抛出一个异常。 作为替代,它使用了恐慌和恢复(panic-and-recover) 机制。一定要记得,这应当作为最后的手段被使用,你 的代码中应当没有,或者很少的令人恐慌的东西。这是 个强大的工具,明智的使用它。那么,应该如何使用它 呢。 下面的描述来自于 [11]: Panic 是一个内建函数,可以中断原有的控制流程,进 入一个令人恐慌的流程中。当函数 F 调用 panic, 函数 F 的执行被中断,并且 F 中的延迟函数会正常 执行,然后 F 返回到调用它的地方。在调用的地 方,F 的行为就像调用了 panic。这一过程继续向 上,直到程序崩溃时的所有 goroutine 返回。 恐慌可以直接调用 panic 产生。也可以由运行时错 误产生,例如访问越界的数组。 Recover 是一个内建的函数,可以让进入令人恐慌的流程中 c对应异常机制,Go 的这种错误机制或许可以叫做恐慌机制:当你 遇到它时应该感到恐慌(panic),然后应该恢复(recover)它。 恐慌(Panic)和恢复(Recover) 79 的 goroutine 恢复过来。recover 仅在延迟函数中有 效。 在正常的执行过程中,调用 recover 会返回 nil 并 且没有其他任何效果。如果当前的 goroutine 陷入 恐慌,调用 recover 可以捕获到 panic 的输入值, 并且恢复正常的执行。 这个函数检查作为其参数的函数在执行时是否会产 生 panic d: func throwsPanic(f func()) (b bool ){..0 defer func(){..1 if x := recover(); x != nil { b = true } }() f() ..2 return ..3 } ..0 定义一个新函数 throwsPanic 接受一个函数作为参 数,参看“函数作为值”。当这个函数产生 panic, 就返回 true,否则返回 false; d复制于 Eleanor McHugh 的演讲稿 80 Chapter 3: 函数 ..1 定义了一个利用 recover 的 defer 函数,如果当前 的 goroutine 产生了 panic,这个 defer 函数能够发 现。当 recover() 返回非 nil 值,设置 b 为true; ..2 调用作为参数接收的函数。 ..3 返回 b 的值。由于 b 是命名返回值 (第 68 页),无须指定 b。 练习 Q6. (4) 平均值 1. 编写一个函数用于计算一个 float64 类型的 slice 的平均值。 Q7. (3) 整数顺序 1. 编写函数,返回其(两个)参数正确的(自然) 数字顺序: f(7,2) → 2,7 f(2,7) → 2,7 Q8. (4) 作用域 1. 下面的程序有什么错误? 练习 81 1package main 3import "fmt" 5func main() { 6for i :=0 ; i < 10; i++ { 7fmt.Printf("%v\n", i) 8} 9fmt.Printf("%v\n", i) 10} Q9. (5) 栈 1. 创建一个固定大小保存整数的栈。它无须超出限 制的增长。定义 push 函数——将数据放入栈, 和 pop 函数从栈中取得内容。栈应当是后进先出 (LIFO)的。 82 Chapter 3: 函数 Figure 3.1. 一个简单的 LIFO 栈 push(k) pop() kki l m i++ i-- 0 2. 更进一步。编写一个 String 方法将栈转化为字符 串形式的表达。可以这样的方式打印整个栈:fmt .Printf("Mystack%v\n",stack) 栈可以被输出成这样的形式:[0:m][1:l][2:k] Q10. (5) 变参 1. 编写函数接受整数类型变参,并且每行打印一个 数字。 Q11. (5) 斐波那契 1. 斐 波 那 契 数 列 以 :1, 1, 2, 3, 5, 8, 13,... 开 始 。 或者用数学形式表达:x1 = 1; x2 = 1; xn = xn−1 + xn−2 ∀n > 2。 编写一个接受 int 值的函数,并给出这个值得到 的斐波那契数列。 练习 83 Q12. (4) Map function map() 函数是一个接受一个函数和 一个列表作为参数的函数。函数应用于列表中的每个元 素,而一个新的包含有计算结果的列表被返回。因此: map(f(),(a1, a2, . . . , an−1, an)) = (f(a1), f(a2), . . . , f(an−1), f(an)) 1. 编写 Go 中的简单的 map() 函数。它能工作于操作 整数的函数就可以了。 2. 扩展代码使其工作于字符串列表。 Q13. (3) 最小值和最大值 1. 编写一个函数,计算 int slice ([]int) 中的最大 值。 2. 编写一个函数,计算 int slice ([]int) 中的最小 值。 Q14. (5) 冒泡排序 1. 编写一个针对 int 类型的 slice 冒泡排序的函数。 这里[32]: 它在一个列表上重复步骤来排序,比 较每个相邻的元素,并且顺序错误 的时候,交换它们。一遍一遍扫描列 表,直到没有交换为止,这意味着列 84 Chapter 3: 函数 表排序完成。算法得名于更小的元素 就像“泡泡”一样冒到列表的顶端。 [32] 这里有一个过程代码作为示例: procedure bubbleSort( A : list of sortable items ) do swapped = false for each i in 1 to length(A) - 1 inclusive do: if A[i-1] > A[i] then swap( A[i-1], A[i] ) swapped = true end if end for while swapped endprocedure Q15. (6) 函数返回一个函数 1. 编写一个函数返回另一个函数,返回的函数的作 用是对一个整数 +2。函数的名称叫做 plusTwo。 然后可以像下面这样使用: p := plusTwo() fmt.Printf("%v\n", p(2)) 练习 85 应该打印4。参阅第 77 页的 “回调”小节了解更多 相关信息。 2. 使 1 中的函数更加通用化,创建一个 plusX(x) 函 数,返回一个函数用于对整数加上 x。 答案 87 答案 A6. (4) 平均值 1. 下面的函数计算平均值。 Listing 3.13. Go 中的平均值函数 func average(xs []float64) (avg float64){ ..0 sum :=0.0 switch len(xs) { case 0: ..1 avg = 0 default:..2 for _, v := range xs { sum +=v } avg = sum / float64(len(xs)) ..3 } return ..4 } 88 Chapter 3: 函数 ..0 可以使用命名返回值; ..1 如果长度是零,返回 0; ..2 否则,计算平均值; ..3 为了使除法能正常计算,必须将值转换 为 float64; ..4 得到平均值,返回它 A7. (3) 整数顺序 1. 这里可以利用 Go 中的多返回值(参阅“多值返 回”小节): func order(a, b int)(int, int){ if a > b { return b,a } return a,b } A8. (4) 作用域 1. 这个程序不能被编译,由于第 9 行的变量 i, 未定义:i 仅在 for 循环中有效。为了修正这 个,main() 应修改为: 答案 89 func main() { var i int for i = 0; i < 10; i++ { fmt.Printf("%v\n", i) } fmt.Printf("%v\n", i) } 现在 i 在 for 循环外定义,并且在其后仍然可访 问。这会打印数字从 0 到 10。 A9. (5) 栈 1. 首先定义一个新的类型来表达栈;需要一个数组 (来保存键)和一个指向最后一个元素的索引。 这个小栈只能保存 10 个元素。 type stack struct { ← 栈不应该被导出 i int data [10] int } 然后需要 push 和 pop 函数来使用这个。首先展示 一下错误的解法! 在 Go 的数据传递中,是值传 递,意味着一个副本被创建并传递给函数。push 函数的第一个版本大约是这样: 90 Chapter 3: 函数 func (s stack) push(k int){ ← 工作于参数的副本 if s.i+1 > 9 { return } s.data[s.i] = k s.i++ } 函数对 stack 类型的变量 s 进行处理。调用这 个,只需要 s.push(50),将整数50 放入栈中。但 是push 函数得到的是 s 的副本,所以它不会有真 正的结果。用这个方法,不会有内容放入栈中, 例如下面的代码: var s stack ← 让 s 是一个 stack 变量 s.push(25) fmt.Printf("stack %v\n", s); s.push(14) fmt.Printf("stack %v\n", s); 打印: stack [0:0] stack [0:0] 答案 91 为了解决这个,需要向函数 push 提供一个指向栈 的指针。这意味着需要修改 push func (sstack)push(k int) → func (s*stack) push(k int) 应当使用 new()(参阅第 5 章“用 new 分配内存”小 节)创建指针指向的 stack 的空间,因此例子中 的第 1 行需要是s := new(stack) 而两个函数变为: func (s *stack) push(k int){ s.data[s.i] = k s.i++ } func (s *stack) pop() int { s.i-- return s.data[s.i] } 像下面这样使用 func main() { var s stack s.push(25) s.push(14) 92 Chapter 3: 函数 fmt.Printf("stack %v\n", s) } 2. 这里有一个额外的问题,对于这个练习中编写打 印栈的代码的时候非常有价值。根据 Go 文档 fmt. Printf("\%v") 可以打印实现了 Stringer 接口的任 何值(%v)。为了使其工作,需要为类型定义一 个 String() 函数: Listing 3.14. stack.String() func (s stack) String() string { var str string for i :=0 ; i <=s.i ; i++ { str = str + "[" + strconv.Itoa(i) + ": " + strconv. Itoa(s.data[i]) + "]" } return str } A10. (5) 变参 答案 93 1. 需要使用... 语法来实现函数接受若干个数字作为 变参。 Listing 3.15. 有变参的函数 package main import "fmt" func main() { printthem(1, 4, 5, 7, 4) printthem(1, 2, 4) } func printthem(numbers ... int){ ← numbers 现在是整数类型的 slice for _, d := range numbers { fmt.Printf("%d\n", d) } } A11. (5) 斐波那契 1. 下面的程序会计算出斐波那契数列。 94 Chapter 3: 函数 Listing 3.16. Go 编写的斐波那契函数 package main import "fmt" func fibonacci(value int ) []int{ x := make([]int , value) ..0 x[0], x[1] = 1, 1 ..1 for n :=2 ; n < value ; n++ { x[n] = x[n-1] + x[n-2] ..2 } return x ..3 } func main() { for _, term := range fibonacci(10) { ..4 fmt.Printf("%v ", term) } } ..0 创建一个用于保存函数执行结果的 array; 答案 95 ..1 开始计算斐波那契数列; ..2 xn = xn−1 + xn−2; ..3 返回整个 array; ..4 使用保留字 range 可以逐个得到斐波那契函 数返回的序列。这里有 10 个,且打印了出 来。 A12. (4) Map function Listing 3.17. Map 函数 1. func Map(f func(int) int, l []int) []int{ j := make([]int , len(l)) for k, v := range l { j[k] = f(v) } return j } func main() { m :=[] int{1, 3, 4} f := func(i int) int { return i * i 96 Chapter 3: 函数 } fmt.Printf("%v", (Map(f, m))) } 2. 字符串问题的答案 A13. (3) 最小值和最大值 1. 这个函数用于计算最大值: func max(l []int) (max int){..0 max = l[0] for _, v := range l {..1 if v > max {..2 max = v } } return ..3 } ..0 使用了命名返回参数; ..1 对 l 循环。元素的序号不重要; ..2 如果找到了新的最大值,记住它; ..3 一个“遥远的”返回,当前的 max 值被返回。 答案 97 2. 这个函数用于计算最小值,这几乎与 max 完全一 致。 func min(l []int) (min int){ min = l[0] for _, v := range l { if v < min { min = v } } return } 有心的读者可能已经将 max 和 min 合成一个函数, 用一个选择来判断是取最小值还是最大值,或者 两个值都返回。 A14. (5) 冒泡排序 1. 冒泡排序并不是最有效率的,对于 n 个元素它的 算法复杂度是 O(n2)。快速排序 [24] 是更好的排 序算法。 但是冒泡排序容易实现,下面是一个例子。 Listing 3.18. 冒泡排序 98 Chapter 3: 函数 func main() { n :=[] int{5, -1,0, 12,3, 5 } fmt.Printf("unsorted %v\n", n) bubblesort(n) fmt.Printf("sorted %v\n", n) } func bubblesort(n []int){ for i :=0 ; i < len(n) - 1; i++ { for j := i + 1; j < len(n); j++ { if n[j] < n[i] { n[i], n[j] = n[j], n[i] } } } } 由于 slice 是一个引用类型,bubblesort 函数可以 工作,并且无须返回排序后的 slice。 A15. (6) 函数返回一个函数 答案 99 1. func main() { p2 :=plusTwo() fmt.Printf("%v\n",p2(2)) } func plusTwo() func(int) int { ..0 return func(x int) int { return x + 2 }..1 } ..0 定义新的函数返回一个函数。看看你写的跟 要表达的意思是如何的; ..1 函数符号,在返回语句中定义了一个 +2 的 函数。 2. 这里我们使用闭包: func plusX(x int) func(int) int{ ..0 return func(y int) int { return x + y }..1 } ..0 再次定义一个函数返回一个函数; ..1 在函数符号中使用局部变量x。 4 包 “^” 对是否有按位非的运算符的回答。 KENTHOMPSON 包是函数和数据的集合。用package 保留字定义一个包。 文件名不需要与包名一致。包名的约定是使用小写字 符。Go 包可以由多个文件组成,但是使用相同的package 这一行。让我们在文件even.go 中定义一个叫 做even 的包。 Listing 4.1. A small package package even ← 开始自定义的包 func Even(i int) bool { ← 可导出函数 return i % 2 == 0 } func odd(i int) bool { ← 私有函数 return i % 2 == 1 包 101 } 名称以大写字幕起始的是可导出的,可以在包的外部调 用,稍候会讨论这个。 现在只需要构建这个包。在$GOPATH 下建立一个目录, 复制even.go 到这个目录(参阅第2 章的“编译和运行代 码”)。 % mkdir$GOPATH/src/even ← 建立一个顶级目录 % cpeven.go$GOPATH/src/even ← 复制包文件 % gobuildeven ← 构建它 接下来在程序myeven.go 中使用这个包: Listing 4.2. even 包的使用 package main import (..0 "even" ..1 "fmt" ..2 ) func main() { i := 5 102 Chapter 4: 包 fmt.Printf("Is %d even? %v\n", i, even.Even (i)) ..3 } ..0 导入下面的包; ..1 本地包even 在这里导入; ..2 官方fmt 包导入; ..3 调用even 包中的函数。访问一个包中的函数的语法 是.Function()。 % gobuildmyeven.go % ./myeven Is5 even?false 在Go 中,当函数的首字母大写的时候,函数会被从包 中导出(在包外部可见,或者说公有的),因此函数名 是Even。如果修改myeven.go 的第10 行,使用未导出的函 数even.odd: fmt.Printf("Is%deven?%v\n",i,even.odd(i)) 由于使用了私有的函数,会得到一个编译错误: 标识符 103 myeven.go:10:cannotrefertounexportednameeven.odd 概括来说: • 公有函数的名字以大写字母开头; • 私有函数的名字以小写字幕开头。 这个规则同样适用于定义在包中的其他名字(新类型、 全局变量)。注意,“大写”的含义并不仅限于US ASCII, 它被扩展到了整个Unicode 范围。所以大写的希腊语、古 埃及语都是可以的。 标识符 像在其他语言中一样,Go 的命名是很重要的。在某些情 况下,它们甚至有语义上的作用:例如,在包外是否可 见决定于首字母是不是大写。因此有必要花点时间讨论 一下Go 程序的命名规则。 使用的规则是让众所周知的缩写保持原样,而不是去尝 试到底哪里应该大写。Atoi,Getwd,Chmod。 驼峰式对那些有完整单词的会很 好:,,ReadFileNewWriterMakeSlice。 104 Chapter 4: 包 包名 当包导入(通过import)时,包名成为了内容的入口。在 import "bytes" 之后,导入包的可以调用函数bytes.Buffer。任何使用 这个包的人,可以使用同样的名字访问到它的内容, 因此这样的包名是好的:短的、简洁的、好记的。根据 规则,包名是小写的一个单词;不应当有下划线或混合 大小写。由于每个人都可能需要录入这个名字,所以尽 可能的简短。不要提前考虑冲突。包名是导入的默认名 称。就上面的bytes.Buffer 来说。通过 import bar "bytes" 它变成了bar.Buffer。因此,它无须在整个代码中唯一, 在少有的冲突中,可以给导入的包选择另一个名字在局 部使用。在任何时候,冲突都是很少见的,因为导入的 文件名会用来做判断,到底是哪个包使用了。 另一个规则是包名就是代码的根目录名; 在src/pkg/compress/gzip 的包,作为compress/gzip 导入, 但名字是gzip,不是compress_gzip 也不是compressGzip。 导入包将使用其名字引用到内容上,所以导入的 包可以利用这个避免罗嗦。例如,缓冲类型bufio 包 的 读 取 方 法 , 叫 做Reader , 而 不 是BufReader , 因 包的文档 105 为 用 户 看 到 的 是bufio.Reader 这 个 清 晰 、 简 洁 的 名 字。更进一步说,由于导入的实例总是它们包名指 向 的 地 址 ,bufio.Reader 不 会 与io.Reader 冲 突 。 类 似 的,ring.Ring(包container/ring)创建新实例的函数— —在Go 中定义的构造函数——通常叫做NewRing,但是 由于Ring 是这个包唯一的一个导出的类型,同时,这个 包也叫做ring,所以它可以只称作New。包的客户看到的 是ring.New。用包的结构帮助你选择更好的名字。 另 外 一 个 简 短 的 例 子 是once.Do ( 参 看sync );once.Do(setup) 读起来很不错,并且命名 为once.DoOrWaitUntilDone(setup) 不 会 有 任 何 帮 助 。 长 的名字不会让其变得容易阅读。如果名字表达了一些复 杂并且微妙的内容,更好的办法是编写一些有帮助的注 释,而不是将所有信息都放入名字里。 最 后 , 在Go 中 使 用 混 合 大 小 写 MixedCaps 或 者mixedCaps,而不是下划线区分含有多个单词的名字。 包的文档 这 段 复 制 于[12]。每个包都应该有包注释,在package 前的一个注释块。对 于多文件包,包注释只需要出现在一个文件前,任意一 个文件都可以。包注释应当对包进行介绍,并提供相关 于包的整体信息。这会出现在godoc 生成的关于包的页面 106 Chapter 4: 包 上,并且相关的细节会一并显示。来自官方regexp 包的例 子: /* Theregexppackageimplementsasimplelibraryfor regularexpressions. Thesyntaxoftheregularexpressionsacceptedis: regexp: concatenation'|'concatenation */ packageregexp 每个定义(并且导出)的函数应当有一小段文字描述该 函数的行为。来自于fmt 包的例子: //Printfformatsaccordingtoa formatspecifierandwritestostandard //output.Itreturnsthenumberofbyteswrittenandanywriteerror //encountered. funcPrintf(formatstring,a...interface)(nint,errerror) 测试包 在Go 中为包编写单元测试应当是一种习惯。编写测试需 测试包 107 要包含testing 包和程序gotest 。两者都有良好的文档。 当对某个包进行测试时,务必要记得必须使用Makefile 进 行编译(参阅第“??” 节)。 测试本身是由go test 完成的。go test 程序调用了所有 的测试函数。even 包没有定义任何测试函数,执行go test,这样: % gotest ? even [notestfiles] 在测试文件中定义一个测试来修复这个。测试文件也在 包目录中,被命名为*_test.go。这些测试文件同Go 程序 中的其他文件一样,但是gotest 只会执行测试函数。每 个测试函数都有相同的标识,它的名字以Test 开头: func TestXxx(t *testing.T) ← TestrestOftheNe 编写测试时,需要告诉go test 测试是失败还是成功。 测试成功则直接返回。当测试失败可以用下面的函数标 记[15]。这是非常重要的(参阅go doctesting 了解更 多): func (t *T) Fail() Fail 标记测试函数失败,但仍然继续执行。 108 Chapter 4: 包 func (t *T) FailNow() FailNow 标记测试函数失败,并且中断其执行。这将会执 行下一个测试。因此,当前文件的其他所有测试都被跳 过。 func (t *T) Log(args ...interface{}) Log 用默认格式对其参数进行格式化,与Print() 类似, 并且记录文本到错误日志。 func (t *T) Fatal(args ... interface{}) Fatal 等价于Log() 后跟随FailNow()。 将这些凑到一起,就可以编写测试了。首先,选择名 字even_test.go。然后添加下面的内容: Listing 4.3. even 包的测试 1package even 3import "testing" 5func TestEven(t *testing.T) { 6if ! Even(2) { 7t.Log("2 should beeven !") 8t.Fail() 测试包 109 9} 10} 注意在第一行使用了package even,测试使用与被测试的 包使用相同的名字空间。这不仅仅是为了方便,也允许 了测试未导出的函数和结构。然后导入testing 包,并且 在第5 行定义了这个文件中唯一的测试函数。展示的Go 代码应当没有任何惊异的地方:检查了Even 函数是否工 作正常。现在等待了好久的时刻到了,执行测试: % gotest ok even 0.001s 测试执行并且报告ok。成功了! 为了展示失败的测试,修改测试函数: // Enteringthetwilightzone func TestEven(t *testing.T) { if Even(2) { t.Log("2 should beodd !") t.Fail() } } 然后得到: 110 Chapter 4: 包 FAIL even 0.004s +---FAIL:TestEven(0.00seconds) 2 shouldbeodd! FAIL 然后你可以以此行事(修复测试的实例) 在编写包的时候应当一边写代码,一边写(一些)文档 和测试函数。这可以让你的程序更好,并且它展示了 你的努力。 常用的包 标准的Go 代码库中包含了大量的包,并且在安装Go 的时 候多数会伴随一起安装。浏览$GOROOT/src/pkg 目录并且 查看那些包会非常有启发。无法对每个包就加以解说, 不过下面的这些值得讨论:a fmt 包fmt 实现了格式化的I/O 函数,这与C 的printf 和scanf 类似。格式化短语派生于C。一些短语 (%-序列)这样使用: a描述来自包的godoc 。额外的解释用斜体。 常用的包 111 %v 默认格式的值。当打印结构时,加号(%+v) 会增加字段名; %#v Go 样式的值表达; %T 带有类型的Go 样式的值表达; io 这个包提供了原始的I/O 操作界面。它主要的任务 是对os 包这样的原始的I/O 进行封装,增加一些其 他相关,使其具有抽象功能用在公共的接口上。 bufio 这个包实现了缓冲的I/O。它封装于io.Reader 和io .Writer 对象,创建了另一个对象(Reader 和Writer)在提供缓冲的同时实现了一些文本I/O 的功能。 sort sort 包提供了对数组和用户定义集合的原始的排序 功能。 strconv strconv 包提供了将字符串转换成基本数据类型, 或者从基本数据类型转换为字符串的功能。 112 Chapter 4: 包 os os 包提供了与平台无关的操作系统功能接口。其设 计是Unix 形式的。 sync sync 包提供了基本的同步原语,例如互斥锁。 flag flag 包实现了命令行解析。参阅“命令行参数” 在 第196 页。 encoding/json encoding/json 包实现了编码与解码RFC 4627 [6] 定 义的JSON 对象。 text/template 数据驱动的模板,用于生成文本输出,例 如HTML。 将模板关联到某个数据结构上进行解析。模板内容 指向数据结构的元素(通常结构的字段或者map 的 键)控制解析并且决定某个值会被显示。模板扫描 结构以便解析,而“游标”@ 决定了当前位置在结构 中的值。 net/http net/http 实现了HTTP 请求、响应和URL 的解析,并 练习 113 且提供了可扩展的HTTP 服务和基本的HTTP 客户 端。 unsafe unsafe 包包含了Go 程序中数据类型上所有不安全 的操作。通常无须使用这个。 reflect reflect 包实现了运行时反射,允许程序通过抽象类 型操作对象。通常用于处理静态类型interface{} 的值,并且通过Typeof 解析出其动态类型信息,通 常会返回一个有接口类型Type 的对象。 参阅6,第“自省和反射” 节。 os/exec os/exec 包执行外部命令。 练习 Q16. (2) stack 包 1. 参考Q9 练习。在这个练习中将从那个代码中建 立一个独立的包。为stack 的实现创建一个合适的 包,Push、Pop 和Stack 类型需要被导出。 114 Chapter 4: 包 2. 为这个包编写一个单元测试,至少测试Push 后Pop 的工作情况。 Q17. (7) 计算器 1. 使用stack 包创建逆波兰计算器。 答案 115 答案 A16. (2) stack 包 1. 在 创 建stack 包时,仅有一些小细节需要修 改。首先,导出的函数应当大写首字母, 因 此 应 该 是Stack 。 包 所 在 的 文 件 被 命 名 为stack-as-package.go,内容是: Listing 4.4. 包里的Stack package stack // 保存元素的Stack type Stack struct { i int data [10]int } // Push 将元素压入栈中 func (s *Stack) Push(k int){ s.data[s.i] = k s.i++ } 116 Chapter 4: 包 // Pop 从栈中弹出一个元素 func (s *Stack) Pop() (ret int){ s.i-- ret = s.data[s.i] return } 2. 为了让单元测试正常工作,需要做一些准备。下 面用一分钟的时间来做这些。首先是单元测试本 身。创建文件pushpop_test.go,有如下内容: Listing 4.5. Push/Pop 测试 package stack import "testing" func TestPushPop(t *testing.T) { c := new(Stack) c.Push(5) if c.Pop() != 5 { t.Log("Pop doesn't give 5") t.Fail() } 答案 117 } 为了让go test 能够工作,需要将包所在文件放 到$GOPATH/src: % mkdir$GOPATH/src/stack % cppushpop_test.go$GOPATH/src/stack % cpstack-as-package.go$GOPATH/src/stack 输出: % goteststack PASS ok stack0.001s A17. (7) 计算器 1. 这是第一个答案: Listing 4.6. 逆波兰计算器 package main import ( "bufio";"os";"strconv";"fmt") var reader *bufio.Reader = bufio.NewReader( os.Stdin) var st = new(Stack) 118 Chapter 4: 包 type Stack struct { i int data [10]int } func (s *Stack) push(k int){ if s.i+1 > 9 { return } s.data[s.i] = k s.i++ } func (s *Stack) pop() (ret int){ s.i-- if s.i < 0 { s.i = 0; return } ret = s.data[s.i] return } func main() { for { s, err:=reader.ReadString( '\n') var token string if err != nil { return } 答案 119 for _, c := range s { switch { case c >='0'&&c <= '9': token = token + string (c) case c == ' ': r, _ := strconv .Atoi( token) st.push(r) token = "" case c == '+': fmt.Printf(" %d\n", st.pop ()+st. pop()) case c == '*': fmt.Printf(" %d\n", st.pop 120 Chapter 4: 包 ()*st. pop()) case c == '-': p := st.pop () q := st.pop () fmt.Printf(" %d\n", q-p) case c == 'q': return default: //error } } } } 5 进阶 “Go 有指针,但是没有指针运算。你不能用指 针变量遍历字符串的各个字节。” Go For C++ Programmers GOAUTHORS Go 有指针。然而却没有指针运算,因此它们更象是引 用而不是你所知道的来自于C 的指针。指针非常有用。 在Go 中调用函数的时候,得记得变量是值传递的。因 此,为了修改一个传递入函数的值的效率和可能性,有 了指针。 通过类型作为前缀来定义一个指针’*’:var p *int。现 在p 是一个指向整数值的指针。所有新定义的变量都被赋 值为其类型的零值,而指针也一样。一个新定义的或者 没有任何指向的指针,有值nil。在其他语言中,这经常 被叫做空(NULL)指针,在Go 中就是nil。让指针指向 某些内容,可以使用取址操作符 (&),像这样: Listing 5.1. Use of a pointer var p *int 进阶 123 fmt.Printf("%v", p) ← 打印nil var i int ← 定义一个整形变量i p = &i ← 使得p 指向i fmt.Printf("%v", p) ← 打印出来的内容类似0x7ff96b81c000a 从指针获取值是通过在指针变量前置’*’实现的: Listing 5.2. 获取指针指向的值 p = &i ← 获取i 的地址 *p = 8 ← 修改i 的值 fmt.Printf("%v\n", *p) ← 打印8 fmt.Printf("%v\n", i) ← 同上 前面已经说了,没有指针运算,所以如果这样写:*p++, 它表示(*p)++:首先获取指针指向的值,然后对这个值加 一。 a a参看练习18。 124 Chapter 5: 进阶 内存分配 Go 同样也垃圾收集,也就是说无须担心内存分配和回 收。 Go 有两个内存分配原语,new 和make。它们应用于不同的 类型,做不同的工作,可能有些迷惑人,但是规则很简 单。下面的章节展示了在Go 中如何处理内存分配,并且 希望能够让new 和make 之间的区别更加清晰。 用new 分配内存 内建函数new 本质上说跟其他语言中的同名函数功能一 样:new(T) 分配了零值填充的T 类型的内存空间,并且返 回其地址,一个*T 类型的值。用Go 的术语说,它返回了 一个指针,指向新分配的类型T 的零值。有一点非常重 要: new 返回指针。 这意味着使用者可以用new 创建一个数据结构的实例并且 可以直接工作。如bytes.Buffer 的文档所述“Buffer 的零 值是一个准备好了的空缓冲。” 类似的,sync.Mutex 也没 有明确的构造函数或Init 方法。取而代之,sync.Mutex 的 零值被定义为非锁定的互斥量。 内存分配 125 零值是非常有用的。例如这样的类型定义,129 页的”定 义自己的类型” 内容。 type SyncedBuffer struct { lock sync.Mutex buffer bytes.Buffer } SyncedBuffer 的值在分配内存或定义之后立刻就可以使 用。在这个片段中,p 和v 都可以在没有任何更进一步处 理的情况下工作。 p := new(SyncedBuffer) ← Type*SyncedBuffer ,已经可以使用 var v SyncedBuffer ← TypeSyncedBuffer ,同上 用make 分配内存 回到内存分配。内建函数make(T,args) 与new(T) 有着不同 的功能。它只能创建slice,map 和channel,并且返回一 个有初始值(非零)的T 类型,而不是*T。本质来讲,导 致这三个类型有所不同的原因是指向数据结构的引用在 使用前必须被初始化。例如,一个slice,是一个包含指向 数据(内部array)的指针,长度和容量的三项描述符; 126 Chapter 5: 进阶 在这些项目被初始化之前,slice 为nil。对于slice,map 和channel,make 初始化了内部的数据结构,填充适当的 值。 make 返回初始化后的(非零)值。 例如,make([]int, 10,100) 分配了100 个整数的数组, 然后用长度10 和容量100 创建了slice 结构指向数组的前10 个元素。区别是,new([]int) 返回指向新分配的内存的指 针,而零值填充的slice 结构是指向nil 的slice 值。 这个例子展示了new 和make 的不同。 var p *[]int = new([]int ) ← 分配slice 结构内存;*p==nil ← 已经可用 var v []int = make([]int, 100) ← v 指向一个新分配的有100 个整数的数组。 var p *[]int = new([]int ) ← 不必要的复杂例子 *p = make([]int, 100, 100) v := make([]int, 100) ← 更常见 务必记得make 仅适用于map,slice 和channel,并且返回 的不是指针。应当用new 获得特定的指针。 内存分配 127 new 分配;make 初始化 上面的两段可以简单总结为: • new(T) 返回*T 指向一个零值T • make(T) 返回初始化后的T 当然make 仅适用于slice,map 和channel。 构造函数与复合声明 有时零值不能满足需求,必须要有一个用于初始化的构 造函数,例如这个来自os 包的例子。 func NewFile(fd int, name string) *File { if fd < 0 { return nil } f := new (File) f.fd = fd f.name = name f.dirinfo = nil f.nepipe = 0 return f 128 Chapter 5: 进阶 } 有许多冗长的内容。可以使用复合声明使其更加简洁, 每次只用一个表达式创建一个新的实例。 func NewFile(fd int, name string) *File { if fd < 0 { return nil } f := File {fd, name, nil, 0 } ← Createanew File return &f ← Returntheaddressoff } 返回本地变量的地址没有问题;在函数返回后,相关的 存储区域仍然存在。 事实上,从复合声明获取分配的实例的地址更好,因此 可以最终将两行缩短到一行。b return &File{fd, name, nil, 0 } The items (called of a composite +literal are laid out in order and must all be 所有的项目(称作字段)都必须按顺序全 b从复合声明中获取地址,意味着告诉编译器在堆中分配空间,而 不是栈中。 定义自己的类型 129 部写上。然而,通过对元素用字段:值成对的标识,初始 化内容可以按任意顺序出现,并且可以省略初始化为零 值的字段。因此可以这样 return &File{fd:fd,name: name } 在特定的情况下,如果复合声明不包含任何字段,它创 建特定类型的零值。表达式new(File) 和&File{} 是等价 的。 复合声明同样可以用于创建array,slice 和map,通过指 定适当的索引和map 键来标识字段。在这个例子中,无 论是Enone,Eio 还是Einval 初始化都能很好的工作,只要 确保它们不同就好了。 ar :=[...] string {Enone: "no error", Einval: " invalid argument"} sl :=[] string {Enone: "no error", Einval: " invalid argument"} ma := map[int]string{Enone: "no error", Einval: " invalid argument"} 定义自己的类型 自然,Go 允许定义新的类型,通过保留字type 实现: 130 Chapter 5: 进阶 type foo int 创建了一个新的类型foo 作用跟int 一样。创建更加复杂 的类型需要用到struct 保留字。这有个在一个数据结构 中记录某人的姓名(string)和年龄(int),并且使其 成为一个新的类型的例子: Listing 5.3. Structures package main import "fmt" type NameAge struct { name string ← 不导出 age int ← 不导出 } func main() { a := new(NameAge) a.name = "Pete"; a.age = 42 fmt.Printf("%v\n", a) } 定义自己的类型 131 通常,fmt.Printf("%v\n",a) 的输出是 &{Pete42} 这很棒!Go 知道如何打印结构。如果仅想打印某一个, 或者某几个结构中的字段,需要使用. 。例 如,仅仅打印名字: fmt.Printf("%s", a.name) ← %s 格式化字符串 结构字段 之前已经提到结构中的项目被称为field。没有字段的结 构:struct{} 或者有四个c字段的: struct { x, y int A *[]int F func() } 如果省略字段的名字,可以创建匿名字段,例如: c是的,四(4)个。 132 Chapter 5: 进阶 struct { T1 ← 字段名字是T1 *T2 ← 字段名字是T2 P.T3 ← 字段名字是T3 x, y int ← 字段名字是x 和y } 注意首字母大写的字段可以被导出,也就是说,在其他 包中可以进行读写。字段名以小写字幕开头是当前包的 私有的。包的函数定义是类似的,参阅第4 章了解更多细 节。 方法 可以对新定义的类型创先函数以便操作,可以通过两种 途径: 1. 创建一个函数接受这个类型的参数。 func doSomething(in1 *NameAge, in2 int ){/* ... */ } (你可能已经猜到了)这是函数调用。 2. 创建一个工作在这个类型上的函数(参阅在3.1 中 定义的接收方): 定义自己的类型 133 func (in1 *NameAge) doSomething(in2 int){/* ... */ } 这是方法调用,可以类似这样使用: var n *NameAge n.doSomething(2) 使用函数还是方法完全是由程序员说了算,但是若需要 满足接口(参看下一章)就必须使用方法。如果没有这 样的需求,那就完全由习惯来决定是使用函数还是方法 了。 但是下面的内容一定要留意,引用自[14]: 如果x 可获取地址,并且&x 的方法中包含 了m,x.m() 是(&x).m() 更短的写法。 根据上面所述,这意味着下面的情况不是错误: var n NameAge ← 不是指针 n.doSomething(2) 这里Go 会查找NameAge 类型的变量n 的方法列表,没有找 到就会再查找*NameAge 类型的方法列表,并且将其转化 为(&n).doSomething(2)。 下面的类型定义中有一些微小但是很重要的不同之处。 同时可以参阅[14, section “Type Declarations”]。假设有: 134 Chapter 5: 进阶 // Mutex 数据类型有两个方法,Lock 和Unlock。 type Mutex struct {/* Mutex 字段 */ } func (m *Mutex) Lock() {/* Lock 实现 */} func (m *Mutex) Unlock() {/* Unlock 实现 */} 现在用两种不同的风格创建了两个数据类型。 • type NewMutexMutex ; • type PrintableMutex struct{ Mutex }. 现在NewMutux 等同于Mutex,但是它没有任何Mutex 的方 法。换句话说,它的方法是空的。 但 是PrintableMutex 已 经 从Mutex 继 承了 方 法 集 合 。 如 同[14] 所说: *PrintableMutex 的 方 法 集 合 包 含 了Lock 和Unlock 方法,被绑定到其匿名字段Mutex。 转换 有时需要将一个类型转换为另一个类型。在Go 中可以做 到,不过有一些规则。首先,将一个值转换为另一个是 由操作符(看起来像函数:byte())完成的,并且不是所 有的转换都是允许的。 转换 135 Table 5.1. 合法的转换,float64 同float32 类似 + From xb[]bytexi[]int xr[]runesstringffloat32iint + To + []byte × []byte(s) + []int × []int(s) + []rune × []rune(s) + stringstring(xb)string(xi)string(xr) × + float32 × float32(i) + int int(f) × • 从string 到字节或者ruin 的slice。 mystring := "hello this isstring" byteslice := []byte(mystring) 转换到byte slice,每个byte 保存字符串对应字节的 整数值。注意Go 的字符串是UTF-8 编码的,一些字 符可能是1、2、3 或者4 个字节结尾。 runeslice := []rune(mystring) 136 Chapter 5: 进阶 转换到rune slice,每个rune 保存Unicode 编码的指 针。字符串中的每个字符对应一个整数。 • 从字节或者整形的slice 到string。 b := []byte{'h','e','l','l','o'} ← 复合声明 s := string(b) i := []rune{257,1024,65} r := string(i) 对于数值,定义了下面的转换: • 将整数转换到指定的(bit)长度:uint8(int); • 从浮点数到整数:int(float32)。这会截断浮点数 的小数部分; • 其他的类似:float32(int)。 用户定义类型的转换 如何在自定义类型之间进行转换?这里创建了两个类 型Foo 和Bar,而Bar 是Foo 的一个别名: 练习 137 type foo struct { int } ← 匿名字段 type bar foo ← bar 是foo 的别名 然后: var b bar= bar {1} ← 声明b 为bar 类型 var f foo= b ← 赋值b 到f 最后一行会引起错误: cannotuseb(typebar)astypefooinassignment( 不能 使用b(类型bar)作为类型foo 赋值) 这可以通过转换来修复: var f foo= foo(b) 注意转换那些字段不一致的结构是相当困难的。同时注 意,转换b 到int 同样会出错;整数与有整数字段的结构 并不一样。 练习 Q18. (4) 指针运算 1. 在正文的第123 页有这样的文字: 138 Chapter 5: 进阶 …这里没有指针运算,因此如果这样 写:*p++,它被解释为(*p)++:首先解 析引用然后增加值。 当像这样增加一个值的时候,什么类型可以工 作? 2. 为什么它不能工作在所有类型上? Q19. (6) 使用interface 的map 函数 1. 使用练习Q12 的答案,利用interface 使其更加通 用。让它至少能同时工作于int 和string。 Q20. (6) 指针 1. 假设定义了下面的结构: type Person struct { name string age int } 下面两行之间的区别是什么? var p1 Person p2 := new(Person) 2. 下面两个内存分配的区别是什么? 练习 139 func Set(t *T) { x = t } 和 func Set(t T) { x= &t } Q21. (6) 链表 1. 使用container/list 包创建(双向)链表。将值1,2 和4 存入并打印。 2. 自行实现链表。然后做与问题1 相同的实现。 Q22. (6) Cat 1. 编写一个程序,模仿Unix 的cat 程序。对于不 知道这个程序的人来说,下面的调用显示了文 件blah 的内容: % catblah 2. 使其支持n 开关,用于输出每行的行号。 Q23. (8) 方法调用 1. 假设有下面的程序。要注意的是 包container/vector 曾 经 是Go 的 一 部 分 , 但 是 140 Chapter 5: 进阶 当内建的append 出现后,就被移除了。然而,对 于当前的问题这不重要。这个包实现了有push 和pop 方法的栈结构。 package main import "container/vector" func main() { k1 :=vector.IntVector {} k2 :=&vector.IntVector {} k3 := new (vector.IntVector) k1.Push(2) k2.Push(3) k3.Push(4) } k1,k2 和k3 的类型是什么? 2. 当前,这个程序可以编译并且运行良好。在不同 类型的变量上Push 都可以工作。Push 的文档这样 描述: func (p *IntVector) Push(x int) Push 增加x 到向量的末尾。 练习 141 那么接受者应当是*IntVector 类型,为什么上面 的代码(Push 语句)可以正确工作?above (the Push statements) work correct then? 答案 143 答案 A18. (4) 指针运算 1. 这仅能工作于指向数字(int,uint 等等)的指针 值。 2. ++ 仅仅定义在数字类型上,同时由于在Go 中没有 运算符重载,所以会在其他类型上失败(编译错 误)。 A19. (6) 使用interface 的map 函数 Listing 5.4. Go 中更加通用的map 函数 1. package main import "fmt" //* definetheemptyinterfaceasa type type e interface{} func mult2(f e) e { switch f.(type){ 144 Chapter 5: 进阶 case int : return f.(int) * 2 case string: return f.(string) + f.( string) + f.(string) + f.(string) } return f } func Map(n []e, f func(e) e) []e { m := make([]e, len(n)) for k, v := range n { m[k] = f(v) } return m } func main() { m :=[]e {1, 2, 3, 4} s :=[]e {"a", "b", "c", "d"} mf :=Map(m, mult2) sf :=Map(s, mult2) fmt.Printf("%v\n", mf) fmt.Printf("%v\n", sf) 答案 145 } A20. (6) 指针 1. 第一行:var p1Person 分配了Person-值 给p1。p1 的类型是Person。 第二行:p2:= new(Person) 分配了内存并且将指 针赋值给p2。p2 的类型是*Person。 2. 在第二个函数中,x 指向一个新的(堆上分配的) 变量t,其包含了实际参数值的副本。 在第一个函数中,x 指向了t 指向的内容,也就是 实际上的参数指向的内容。 因此在第二个函数,我们有了“额外”的变量存储 了相关值的副本。 A21. (6) 链表 1. 2. A22. (6) Cat 1. 下面是cat 的实现,同样支持n 输出每行的行号。 146 Chapter 5: 进阶 Listing 5.5. cat 程序 package main ..0 import ( "io" "os" "fmt" "bufio" "flag" ) var numberFlag = flag.Bool("n", false, " number each line") ..1 ..2 func cat(r *bufio.Reader) { i :=1 for { buf, e :=r.ReadBytes('\n') ..3 if e ==io.EOF { ..4 答案 147 break } if *numberFlag { ..5 fmt.Fprintf(os. Stdout, "%5d % s", i, buf) i++ } else { ..6 fmt.Fprintf(os. Stdout, "%s", buf) } } return } func main() { flag.Parse() if flag.NArg() ==0 { cat(bufio.NewReader(os.Stdin )) } 148 Chapter 5: 进阶 for i :=0 ; i < flag.NArg() ; i++ { f, e := os.Open(flag.Arg(i), os.O_RDONLY, 0) if e != nil { fmt.Fprintf(os. Stderr, "%s: error reading from %s: %s\n", os.Args[0], flag. Arg(i), e. String ()) continue } cat(bufio.NewReader(f)) } } ..0 包含所有需要用到的包; ..1 定义新的开关”n”,默认是关闭的。注意很 容易写的帮助文本; ..2 实际上读取并且显示文件内容的函数; 答案 149 ..3 每次读一行; ..4 如果到达文件结尾; ..5 如果设定了行号,打印行号然后是内容本 身; ..6 否则,仅仅打印该行内容。 A23. (8) 方法调用 1. k1 的类型是vector.IntVector。为什么?这里使 用 了 符 号{} ,因此获得了类型的值。变量k2 是*vector.IntVector,因为获得了复合语句的地 址(&)。而最后的k3 同样是*vector.IntVector 类 型,因为new 返回该类型的指针。 2. 在[14] 的“调用”章节,有这样的描述: 当x 的方法集合包含m,并且参数列表 可以赋值给m 的参数,方法调用x.m() 是 合 法 的 。 如 果x 可 以 被 地 址 化 , 而&x 的方法集合包含m,x.m() 可以作 为(&x).m() 的省略写法。 换 句 话 说 , 由 于k1 可 以 被 地 址 化 , 而*vector.IntVector 具 有 Push 方 法 , 调 用k1 .Push(2) 被Go 转换为(&k1).Push(2) 来使型系统 150 Chapter 5: 进阶 愉悦(也使你愉悦——现在你已经了解到这一 点)。d d参阅本章的第“方法” 节。 6 接口 我对外科手术般进入我的身体总是有恐惧。你 知道我说的是什么。 eXistenZ TEDPIKUL 在Go 中,保留字interface 被赋予了多种不同的含义。每下 面 的 内 容 来 自[30]。 是Ian Lance Taylor 编 写 的 , 他 是Go 的 作 者 之 一。 个类型都有接口,意味着对那个类型定义了方法集合 。 这段代码定义了具有一个字段和两个方法的结构类型S。 Listing 6.1. 定义结构和结构的方法 type S struct { i int} func (p *S) Get() int{ return p.i } func (p *S) Put(v int ){ p.i = v } 也可以定义接口类型,仅仅是方法的集合。这里定义了 一个有两个方法的接口I: type I interface { Get() int Put( int) 接口 153 } 对于接口I,S 是合法的实现,因为它定义了I 所需的两个 方法。注意,即便是没有明确定义S 实现了I,这也是正 确的。 Go 程序可以利用这个特点来实现接口的另一个含义,就 是接口值: func f(p I) {..0 fmt.Println(p.Get()) ..1 p.Put(1) ..2 } ..0 定义一个函数接受一个接口类型作为参数; ..1 p 实现了接口I,必须有Get() 方法; ..2 Put() 方法是类似的。 这里的变量p 保存了接口类型的值。因为S 实现了I,可以 调用f 向其传递S 类型的值的指针: var s S; f(&s) 获取s 的地址,而不是S 的值的原因,是因为在s 的指针 上定义了方法,参阅上面的代码6.1。这并不是必须的— 154 Chapter 6: 接口 —可以定义让方法接受值——但是这样的话Put 方法就不 会像期望的那样工作了。 实际上,无须明确一个类型是否实现了一个接口意味 着Go 实现了叫做duck typing[34] 的模式。这不是纯粹 的duck typing,因为如果可能的话Go 编译器将对类型 是否实现了接口进行实现静态检查。然而,Go 确实有纯 粹动态的方面,如可将一个接口类型转换到另一个。通 常情况下,转换的检查是在运行时进行的。如果是非法 转换——当在已有接口值中存储的类型值不匹配将要转 换到的接口——程序会抛出运行时错误。 在Go 中的接口有着与许多其他编程语言类似的思路:C++ 中的纯抽象虚基类,Haskell 中的typeclasses 或者Python 中的duck typing。然而没有其他任何一个语言联合了接口 值、静态类型检查、运行时动态转换,以及无须明确定 义类型适配一个接口。这些给Go 带来的结果是,强大、 灵活、高效和容易编写的。 到底是什么? 来定义另外一个类型同样实现了接口I: type R struct { i int} func (p *R) Get() int{ return p.i } func (p *R) Put(v int ){ p.i = v } 接口 155 函数f 现在可以接受R 个S 类型的变量。假设需要在函数f 中知道实际的类型。在Go 中可以使用type switch 得到。 func f(p I) { switch t := p.(type){..0 case *S:..1 case *R:..2 case S:..3 case R:..4 default:..5 } } ..0 类型判断。在switch 语句中使用(type)。保存类型 到变量t; ..1 p 的实际类型是S 的指针; ..2 p 的实际类型是R 的指针; ..3 p 的实际类型是S; ..4 p 的实际类型是R; ..5 实现了I 的其他类型。 156 Chapter 6: 接口 在switch 之外使用(type) 是非法的。类型判断不是唯一的 运行时得到类型的方法。为了在运行时得到类型,同样 可以使用“comma, ok”来判断一个接口类型是否实现了某 个特定接口: if t, ok:=something.(I) ; ok { // 对于某些实现了接口I 的 //t 是其所拥有的类型 } + 确定一个变量实现了某个接口,可以使用: t :=something.(I) 空接口 由于每个类型都能匹配到空接口:interface{}。我们可 以创建一个接受空接口作为参数的普通函数: Listing 6.2. 用空接口作为参数的函数 func g(something interface{}) int { return something.(I).Get() } 方法 157 在这个函数中的return something.(I).Get() 是有一点窍 门的。值something 具有类型interface{},这意味着方法 没有任何约束:它能包含任何类型。.(I) 是类型断言, 用于转换something 到I 类型的接口。如果有这个类型, 则可以调用Get() 函数。因此,如果创建一个*S 类型的新 变量,也可以调用g(),因为*S 同样实现了空接口。 s = new(S) fmt.Println(g(s)); 调用g 的运行不会出问题,并且将打印0。如果调用g() 的 参数没有实现I 会带来一个麻烦: Listing 6.3. 实现接口失败 i :=5 ← 声明i 是一个``该死的''int fmt.Println(g(i)) 这能编译,但是当运行的时候会得到: panic:interfaceconversion:intisnotmain.I:missing methodGet 这是绝对没问题,内建类型int 没有Get() 方法。 方法 方法就是有接收者的函数(参阅第3 章)。 158 Chapter 6: 接口 可以在任意类型上定义方法(除了非本地类型,包括内 建类型:int 类型不能有方法)。然而可以新建一个拥有 方法的整数类型。例如: type Foo int func (self Foo) Emit() { fmt.Printf("%v", self) } type Emitter interface { Emit() } 对那些非本地(定义在其他包的)类型也一样: Listing 6.4. 扩展内建类型错 误 func (i int) Emit (){ fmt.Printf("%d" , i) } 不能定义新的方法 在非本地类型int 上 方法 159 Listing 6.5. 扩展非本地类型 错误 func (a *net.AddrError) Emit() { fmt.Printf("%v", a) } 不能定义新的方法 在非本地类型net.AddrError 上 接口类型的方法 接口定义为一个方法的集合。方法包含实际的代码。换 句话说,一个接口就是定义,而方法就是实现。因此, 接收者不能定义为接口类型,这样做的话会引起invalid receivertype... 的编译器错误。来自语言说明书[14] 的 权威内容: 接收者类型必须是T 或*T,这里的T 是类型 名。T 叫做接收者基础类型或简称基础类 型。基础类型一定不能使指针或接口类型, 并且定义在与方法相同的包中。 160 Chapter 6: 接口 接口指针 Pointers to interfaces 在Go 中创建指向接口的指针是无意义的。实际上 创建接口值的指针也是非法的。在2010-10-13 的发布 日志中进行的描述,使得没有任何余地怀疑这一事 实: 语言的改变是使用指针指向接口值不再自 动反引用指针。指向接口值的指针通常是 低级的错误,而不是正确的代码。 这来自[13]。如果不是这个限制,这个代码: var buf bytes.Buffer io.Copy(buf, os.Stdin) 就会复制标准输入到buf 的副本,而不是buf 本身。这 看起来永远不会是一个期望的结果。 接口名字 根据规则,单方法接口命名为方法名加上-er 后 缀:Reader,Writer,Formatter 等。 简短的例子 161 有一堆这样的命名,高效的反映了它们职责和包含的 函数名。Read,Write,Close,Flush,String 等等有着规 范的声明和含义。为了避免混淆,除非有类似的声明 和含义,否则不要让方法与这些重名。相反的,如果 类型实现了与众所周知的类型相同的方法,那么就用相 同的名字和声明;将字符串转换方法命名为String 而不 是ToString。 文 本 复 制 于[12]。 简短的例子 回顾那个冒泡排序的练习 Q14),对整型数组排序: func bubblesort(n []int){ for i := 0; i < len(n)-1; i++ { for j :=i + 1 ; j < len(n); j++ { if n[j] < n[i] { n[i], n[j] = n[j], n[i] } } } } 排序字符串的版本是类似的,除了函数的声明: func bubblesortString(n []string){/*... */ } 162 Chapter 6: 接口 基于此,可能会需要两个函数,每个类型一个。而通过 使用接口可以让这个变得更加通用。 来创建一个可以对字符串和整数进行排序的函数,这个 例子的某些行是无法运行的: func sort(i []interface{}){..0 switch i.(type){..1 case string:..2 // ... case int : // ... } return /*... */ ..3 } ..0 函数将接收一个空接口的slice; ..1 使用type switch 找到输入参数实际的类型; ..2 然后排序; ..3 返回排序的slice。 但是如果用sort([]int{1,4,5 }) 调用这个函数,会失 败:cannotusei(type[]int)astype[]interfacein functionargument 简短的例子 163 这是因为Go 不能简单的将其转换为接口的slice。转换到 接口是容易的,但是转换到slice 的开销就高了。 简单来说 :Go 不能(隐式)转换为slice。 关 于 这 个 话 题 完 整 的 邮 件 列 表 讨 论 可 以 在[20] 这 里 找 到。 那么如何创建Go 形式的这些“通用”函数呢?用Go 隐式的 处理来代替type switch 方式的类型推断吧。下面的步骤是 必须的: 1. 定义一个有着若干排序相关的方法的接口类型(这 里叫做Sorter)。至少需要获取slice 长度的函数, 比较两个值的函数和交换函数; type Sorter interface { Len() int ← len() 作为方法 Less(i, j int) bool ← p[j] < p[i] 作为方法 Swap(i, j int) ← p[i],p[j]=p[j],p[i] 作为方法 } 2. 定义用于排序slice 的新类型。注意定义的是slice 类 型; type Xi []int type Xs []string 164 Chapter 6: 接口 3. 实现Sorter 接口的方法。整数的: func (p Xi) Len() int { return len(p) } func (p Xi) Less(i int, j int ) bool { return p[j] < p[i] } func (p Xi) Swap(i int, j int ){ p[i], p [j] = p[j], p[i] } 和字符串的: func (p Xs) Len() int { return len(p) } func (p Xs) Less(i int, j int ) bool { return p[j] < p[i] } func (p Xs) Swap(i int, j int ){ p[i], p [j] = p[j], p[i] } 4. 编写作用于Sorter 接口的通用排序函数。 func Sort(x Sorter) {..0 for i :=0 ; i < x.Len() - 1; i++ {..1 for j := i + 1; j < x.Len(); j++ { if x.Less(i, j) { x.Swap(i, j) } 简短的例子 165 } } } ..0 x 现在是Sorter 类型; ..1 使用定义的函数,实现了冒泡排序。 现在可以像下面这样使用通用的Sort 函数: ints :=Xi {44, 67,3, 17, 89,10,73,9, 14,8 } strings :=Xs {"nut", "ape", "elephant", "zoo", "go" } Sort(ints) fmt.Printf("%v\n", ints) Sort(strings) fmt.Printf("%v\n", strings) 在接口中列出接口 看一下下面的接口定义,这个是来自包container/heap 的: type Interface interface { 166 Chapter 6: 接口 sort.Interface Push(x interface{}) Pop() interface{} } 这里有另外一个接口在heap.Interface 的定义中被列出, 这看起来有些古怪,但是这的确是正确的,要记得接口 只是一些方法的列表。sort.Interface 同样是这样一个列 表,因此将其包含在接口内是毫无错误的。 自省和反射 在下面的例子中,了解一下定义在Person 的 定 义 中 的“标签”(这里命名为“namestr”)。为了做到这个,需 要reflect 包(在Go 中没有其他方法)。要记得,查看标 签意味着返回类型的定义。因此使用reflect 包来指出变量 的类型,然后访问标签。 Listing 6.6. 使用反射自省 .. .. 0 . .. 1 . .. 2 type Person struct { name string "namestr" ← "namestr" 是标签 age int } 简短的例子 167 p1 := new(Person) ← new 返回Person 的指针 ShowTag(p1) ← 调用ShowTag() 并传递指针 func ShowTag(i interface{}){ switch t := reflect.TypeOf(i); t.Kind() { ← Gettype,switchonKind() case reflect.Ptr: ← Itsapointer,henceareflect.Ptr tag :=t.Elem().Field(0).Tag Elem 返回v 指向的值。如果v 是空指针,Elem 返回空值。 ..0 We are dealing with a Type and according to the doc- umentationa: // Elem returns a type’s element type. // It panics if the type’s Kind is not Array, Chan, Map, Ptr, or Slice. Elem()Type 同样的在t 使用Elem() 得到了指针指向的值。 agodocreflect 168 Chapter 6: 接口 ..1 We have now dereferened the pointer and are ”in- side” our structure. We now use Field(0) to access the zeroth field; ..2 结构StructField 有成员Tag,返回字符串类型的标 签名。因此,在第0th 个字段上可以用.Tag 访问这 个名字:Field(0).Tag。这样得到了namestr。 为了让类型和值之间的区别更加清晰,看下面的代码: Listing 6.7. 反射类型和值 func show(i interface{}){ switch t := i.(type){ case *Person: t := reflect.TypeOf(i) ← 得到类型的元数据 v := reflect.ValueOf(i) ← 得到实际的值 tag := t.Elem().Field(0).Tag ..0 name := v.Elem().Field(0).String() ..1 } ..0 这里希望获得“标签”。因此需要Elem() 重定向至其 上,访问第一个字段来获取标签。注意将t 作为一 个reflect.Type 来操作; 简短的例子 169 ..1 现在需要访问其中一个成员的值,并让 v 上 的Elem() 进行重定向。这样就访问到了结 构。然后访问第一个字段Field(0) 并且调用其上 的String() 方法。 Figure 6.1. 用反射去除层次关系。通 过Elem() 访问*Person,使用go docre- flect 中描述的方法获得string 内部包 含的内容。 reflect.Ptr .Elem() reflect.Value .Field(0) reflect.StructField .String() "Albert Einstein" "Albert Einstein" 设置值与获得值类似,但是仅仅工作在可导出的成员 上。这些代码: 170 Chapter 6: 接口 Listing 6.8. 私有成员的反射 type Person struct { name string "namestr " ← 名称 age int } func Set(i interface{}) { switch i.(type){ case *Person: r :=reflect .ValueOf (i) r.Elem(0). Field(0) . SetString ("Albert Einstein ") } } Listing 6.9. 公有成员的反射 type Person struct { Name string "namestr " ← Name age int } func Set(i interface{}) { switch i.(type){ case *Person: r :=reflect .ValueOf (i) r.Elem(). Field(0) . SetString ("Albert Einstein ") } } 左 边 的 代 码 可 以 编 译 并 练习 171 运行,但是当运行的时候,将得到打印了栈的运行时错 误: panic:reflect.Value.SetStringusingvalueobtained usingunexportedfield 右边的代码没有问题,并且设置了成员变量Name 为“Albert Einstein”。当然,这仅仅工作于调用Set() 时 传递一个指针参数。 练习 Q24. (6) 接口和编译 1. 在第157 页的代码6.3 编译正常——就像文中开始 描述的那样。但是当运行的时候,会得到运行时 错误,因此有些东西有错误。为什么代码编译没 有问题呢? Q25. (5) 指针和反射 1. 在第“自省和反射” 节,第166 页的最后一段中,有 这样的描述: 右边的代码没有问题,并且设置了成 员变量Name 为“Albert Einstein”。当然, 这仅仅工作于调用Set() 时传递一个指 针参数。 172 Chapter 6: 接口 为什么是这样的情况? Q26. (7) 接口和max() 1. 在练习Q13 中创建了工作于一个整形slice 上的最 大函数。现在的问题是创建一个显示最大数字的 程序,同时工作于整数和浮点数。虽然在这里会 相当困难,不过还是让程序尽可能的通用吧。 答案 173 答案 A24. (6) 接口和编译 1. 代码能够编译是因为整数类型实现了空接口,这 是在编译时检查的。 修复这个正确的途径是测试这个空接口可以被转 换,如果可以,调用对应的方法。6.2 列出的Go 代码中定义了函数g——这里重复一下: func g(any interface{}) int { return any.(I) .Get() } 应当修改为: func g(any interface{}) int { if v, ok := any.(I) ; ok {// 检查是否 可以转换 return v.Get() // 如果可 以,调用Get() } return -1 // 随便返回 个什么 } 174 Chapter 6: 接口 如果现在调用g(),就不会有运行时错误了。在Go 中这种用法被称作“comma ok”。 A25. (5) 指针和反射 1. 当调用一个非指针参数,变量是复制(call-by- value)的。因此,进行魔法般的反射是在副本 上。这样就不能改变原来的值,仅仅改变副本。 A26. (7) 接口和max() 1. 下面的程序计算了最大值。它是Go 能做到的最通 用的形式了。 Listing 6.10. 通用的计算最大值 package main func Less(l, r interface{}) bool { ..0 switch l.(type){ case int: if _, ok:=r.( int); ok { return l.(int) < r.( int)..1 } 答案 175 case float32: if _, ok:=r.( float32); ok { return l.(float32) < r.(float32)..2 } } return false } func main() { var a, b, c int = 5, 15, 0 var x, y, z float32 = 5.4, 29.3, 0.0 if c = a; Less(a, b) {..3 c = b } if z = x; Less(x, y) {..4 z = y } println(c, z) } ..0 也可以选择让这个函数的返回值 为interface{},但是这也就意味着调用者 176 Chapter 6: 接口 不得不总是使用类型断言来从接口中解析出 实际的类型; ..1 所有类型定义为整数。然后进行比较; ..2 参数是float32; ..3 获得a 和b 中的最大值; ..4 浮点类型也一样。 7 并发 •“并行是关于性能的; • 并发是关于程序设计的。” Google IO 2010 ROBEPIKE 在这章中将展示Go 使用channel 和goroutine 开发并行 程序的能力。goroutine 是Go 并发能力的核心要素。但 是,goroutine 到底是什么?来自[12]: 叫 做goroutine 是因为已有的短语——线 程、协程、进程等等——传递了不准确的 含义。goroutine 有简单的模型:它是与其 他goroutine 并行执行的,有着相同地址空间 的函数。。它是轻量的,仅比分配栈空间多 一点点消耗。而初始时栈是很小的,所以它 们也是廉价的,并且随着需要在堆空间上分 配(和释放)。 goroutine 是一个普通的函数,只是需要使用保留字go 作 为开头。 并发 179 ready("Tea", 2) ← 普通函数调用 go ready("Tea", 2) ← ready() 作为goroutine 运行 下面程序的思路来自[28]。让一个函数作为两个goroutine 执行,goroutine 等待一段时间,然后打印一些内容到屏 幕。在第14 和15 行,启动了goroutine。main 函数等待足 够的长的时间,这样每个goroutine 会打印各自的文本到 屏幕。现在是在第17 行等待5 秒钟,但实际上没有任何办 法知道,当所有goroutine 都已经退出应当等待多久。 Listing 7.1. Go routine 实践 8func ready(w string, sec int ){ 9time.Sleep(time.Duration(sec) * time.Second ) 10fmt.Println(w, "is ready!") 11} 13func main() { 14go ready("Tea", 2) 15go ready("Coffee", 1) 16fmt.Println("I'm waiting") 17time.Sleep(5 * time.Second) 18} 表7.1 输出: 180 Chapter 7: 并发 I'mwaiting ← 立刻 Coffeeisready! ← 1 秒后 Teaisready! ← 2 秒后 如果不等待goroutine 的执行(例如,移除第17 行), 程序立刻终止,而任何正在执行的goroutine 都会停止。 为了修复这个,需要一些能够同goroutine 通讯的机制。 这一机制通过channels 的形式使用。channel 可以与Unix sehll 中的双向管道做类比:可以通过它发送或者接收 值。这些值只能是特定的类型:channel 类型。定义一 个channel 时,也需要定义发送到channel 的值的类型。 注意,必须使用make 创建channel: ci := make(chan int) cs := make(chan string) cf := make(chan interface{}) 创建channel ci 用于发送和接收整数,创建channel cs 用 于字符串,以及channel cf 使用了空接口来满足各种类 型。向channel 发送或接收数据,是通过类似的操作符完 成的:<-. 具体作用则依赖于操作符的位置: ci <- 1 ← 发送整数1 到channelci <-ci ← 从channelci 接收整数 i := <-ci ← 从channelci 接收整数,并保存到i 中 并发 181 将这些放到实例中去。 Listing 7.2. Go routines 和channel var c chan int ..0 func ready(w string, sec int ){ time.Sleep(time.Duration(sec) * time.Second ) fmt.Println(w, "is ready!") c <- 1 ..1 } func main() { c = make(chan int )..2 go ready("Tea", 2) ..3 go ready("Coffee", 1) fmt.Println("I'm waiting, butnottoolong" ) <-c ..4 <-c ..5 } ..0 定义c 作为int 型的channel。就是说:这个channel 182 Chapter 7: 并发 传输整数。注意这个变量是全局的,这样goroutine 可以访问它; ..1 发送整数1 到channel c; ..2 初始化c; ..3 用保留字go 开始一个goroutine; ..4 等待,直到从channel 上接收一个值。注意,收到 的值被丢弃了; ..5 两个goroutines,接收两个值。 这里仍然有一些丑陋的东西;不得不从channel 中读取 两次(第14 和15 行)。在这个例子中没问题,但是如果 不知道有启动了多少个goroutine 怎么办呢?这里有另一 个Go 内建的保留字:select。通过select(和其他东西) 可以监听channel 上输入的数据。 在这个程序中使用select,并不会让它变得更短,因为运 行的goroutine 太少了。移除第14 和15 行,并用下面的内 容替换它们: Listing 7.3. 使用select 14L: for { 15select { 并发 183 16case <-c: 17i++ 18if i > 1 { 19break L 20} 21} 22} 现在将会一直等待下去。只有当从channel c 上收到多个 响应时才会退出循环L。 使其并行运行 虽然goroutine 是并发执行的,但是它们并不是并行运 行的。 如果 不告 诉Go 额外的东西,同一时刻只会有 一个goroutine 执行。利用runtime.GOMAXPROCS(n) 可以设 置goroutine 并行执行的数量。来自文档: GOMAXPROCS 设置了同时运行的CPU 的最大 数量,并返回之前的设置。如果n < 1,不会 改变当前设置。当调度得到改进后,这将被 移除。 如果不希望修改任何源代码,同样可以通过设置环境变 量GOMAXPROCS 为目标值。 184 Chapter 7: 并发 更多关于channel 当在Go 中用ch:= make(chanbool ) 创建chennel 时,bool 型 的 无 缓 冲channel 会被创建。这对于程序来说意味 着什么呢?首先,如果读取(value:= <-ch )它将会 被阻塞,直到有数据接收。其次,任何发送(ch<-5) 将会被阻塞,直到数据被读出。无缓冲channel 是在多 个goroutine 之间同步很棒的工具。 不过Go 也允许指定channel 的缓冲大小,很简单,就 是channel 可以存储多少元素。ch:= make(chanbool , 4),创建了可以存储4 个元素的bool 型channel。在这 个channel 中 , 前4 个元素可以无阻塞的写入。当写 入 第5￿ 元素时,代码将会阻塞,直到其他goroutine 从channel 中读取一些元素,腾出空间。 一句话说,在Go 中下面的为true: ch:= make(chantype , value) { value == 0 → 无缓冲 value > 0 → 缓冲value 的元素 关闭channel 当channel 被关闭后,读取端需要知道这个事情。下面的 代码演示了如何检查channel 是否被关系。 x, ok= <-ch 练习 185 当ok 被赋值为true 意味着channel 尚未被关闭,同时 可 以读取数据。否则ok 被赋值为false。在这个情况下表 示channel 被关闭。 TODO more needs to be written 练习 Q27. (4) Channel 1. 修改在练习Q2 中创建的程序,换句话说,主体中 调用的函数现在是一个goroutine 并且使用channel 通讯。不用担心goroutine 是如何停止的。 2. 在完成了问题1 后,仍有一些待解决的问题。其 中一个麻烦是goroutine 在main.main() 结束的时 候,没有进行清理。更糟的是,由于main.main() 和main.shower() 的竞争关系,不是所有数字都被 打印了。本应该打印到9,但是有时只打印到8。 添加第二个退出channel,可以解决这两个问题。 试试吧。a Q28. (7) 斐波那契II 1. 这是类似的练习,第一个在第82 页的练习11。完 整的问题描述: a需要用到select 语句。 186 Chapter 7: 并发 斐 波 那 契 数 列 以 :1, 1, 2, 3, 5, 8, 13,... 开 头 。 或用数学形式:x1 = 1; x2 = 1; xn = xn−1 + xn−2 ∀n > 2。 编写一个函数接收int 值,并给出同样 数量的斐波那契数列。 但是现在有额外条件:必须使用channel。 答案 187 答案 A27. (4) Channel 1. 程序可能的形式是: Listing 7.4. Go 的channel 1package main 3import "fmt" 5func main() { 6ch := make(chan int) 7go shower(ch) 8for i :=0 ; i < 10; i++ { 9ch <- i 10} 11} 13func shower(c chan int){ 14for { 15j := <-c 16fmt.Printf("%d\n", j) 188 Chapter 7: 并发 17} 18} 以通常的方式开始,在第6 行创建了一个新的int 类型的channel。下一行调用了shower 函数,用ch 变量作为参数,这样就可以与其通讯。然后进 入for 循环(第8-10 行),在循环中发送(通过<- )数字到函数(现在是goroutine)shower。在函 数shower 中等待(阻塞方式),直到接收到了数 字(第15 行)。每个收到的数字都被打印(第16 行)出来,然后继续第14 行开始的死循环。 2. 答案是 Listing 7.5. 添加额外的退出channel 1package main 3import "fmt" 5func main() { 6ch := make(chan int) 7quit := make(chan bool) 8go shower(ch, quit) 9for i :=0 ; i < 10; i++ { 10ch <- i 答案 189 11} 12quit <- false // 或者是true,这没 啥关系 13} 15func shower(c chan int , quit chan bool){ 16for { 17select { 18case j := <-c: 19fmt.Printf("%d\n", j ) 20case <-quit: 21break 22} 23} 24} 在第20 行从退出channel 读取并丢弃该值。可以 使用q := <-quit,但是可能只需要用这个变量一 次——在Go 中是非法的。另一种办法,你可能已 经想到了:_ = <-quit。在Go 中这是合法的,但 是第20 行的形式在Go 中更好。 A28. (7) 斐波那契II 190 Chapter 7: 并发 1. 下面的程序使用channel 计算了斐波那契数列。 Listing 7.6. Go 的斐波那契函数 package main import "fmt" func dup3(in <-chan int )(<-chan int , <-chan int, <-chan int ){ a, b, c := make(chan int , 2), make( chan int , 2), make(chan int , 2) go func(){ for { x := <-in a <- x b <- x c <- x } }() return a, b, c } func fib() <-chan int{ x := make(chan int , 2) a, b, out:=dup3(x) 答案 191 go func(){ x <- 0 x <- 1 <-a for { x <- <-a+<-b } }() return out } func main() { x :=fib() for i :=0 ; i < 10; i++ { fmt.Println(<-x) } } // See sdh33b.blogspot.com/2009/12/fibonacci- in-go.html 8 通讯 “好的沟通就像是一杯刺激的浓咖啡,然后就 难以入睡。” ANNEMORROWLINDBERGH 在这章中将介绍Go 中与外部通讯的通讯模块。将会了解 文件、目录、网络通讯和运行其他程序。Go 的I/O 核心是 接口io.Reader 和io.Writer。 在Go 中,从文件读取(或写入)是非常容易的。程序只 需要使用os 包就可以从文件/etc/passwd 中读取数据。 Listing 8.1. 从文件读取(无缓冲) package main import "os" func main() { buf := make([]byte, 1024) f, _ :=os.Open("/etc/passwd") ..0 通讯 193 defer f.Close() ..1 for { n, _ := f.Read(buf) ..2 if n ==0 { break } ..3 os.Stdout.Write(buf[:n]) ..4 } } 接下来展示了如何做到这点: ..0 打 开 文 件 ,os.Open 返 回 一 个 实 现 了io.Reader 和io.Writer 的*os.File; ..1 确保关闭了f; ..2 一次读取1024 字节; ..3 到达文件末尾; ..4 将内容写入os.Stdout 如果想要使用缓冲 IO,则有bufio 包: Listing 8.2. 从文件读取(缓冲) package main 194 Chapter 8: 通讯 import ( "os";"bufio") func main() { buf := make([]byte, 1024) f, _ :=os.Open("/etc/passwd") ..0 defer f.Close() r := bufio.NewReader(f) ..1 w := bufio.NewWriter(os.Stdout) defer w.Flush() for { n, _ := r.Read(buf) ..2 if n ==0 { break } w.Write(buf[0:n]) } } ..0 打开文件; ..1 转 换f 为 有 缓 冲 的Reader 。NewReader 需 要 一 个io.Reader,因此或许你认为这会出错。但其实 不会。任何有Read() 函数就实现了这个接口。同 时,从列表8.1可以看到,*os.File 已经这样做了; ..2 从Reader 读取,而向Writer 写入,然后向屏幕输出 文件。 io.Reader 195 io.Reader 在前面已经提到io.Reader 接口对于Go 语言来说非常重 要。许多(如果不是全部的话)函数需要通过io.Reader 读取一些数据作为输入。为了满足这个接口,只需要实 现一个方法:Read(p[]byte)(nint,errerror) 。写入则 是(你可能已经猜到了)实现了Write 方法的io.Writer。 如果你让自己的程序或者包中的类型实现了io.Reader 或 者io.Writer 接口,那么整个Go 标准库都可以使用这个类 型! 一些例子 前面的程序将整个文件读出,但是通常情况下会希望一 行一行的读取。下面的片段展示了如何实现: f, _ :=os.Open("/etc/passwd") ; defer f.Close() r :=bufio.NewReader(f) ← 使其成为一个bufio,以便访问ReadString 方法 s, ok:=r.ReadString('\n') { ← 从输入中读取一行 // ...| ← s 保存了字符串,通过string 包就可以解析它| 196 Chapter 8: 通讯 这两个例子的相似之处展示了Go 拥 有 的“脚 本”化 特 性,例 如, 用Go 编写程序感觉上类似使用动态语言 (Python、Ruby、Perl 或者PHP)。 命令行参数 来自命令行的参数在程序中通过字符串slice os.Args 获 取,导入包os 即可。flag 包有着精巧的接口,同样提供了 解析标识的方法。这个例子是一个DNS 查询工具: dnssec :=flag.Bool("dnssec", false, "Request DNSSEC records") ..0 port :=flag.String("port", "53", "Set thequery port") ..1 flag.Usage = func(){..2 fmt.Fprintf(os.Stderr, "Usage: %s [OPTIONS] [ name ...]\n", os.Args[0]) flag.PrintDefaults() ..3 } flag.Parse() ..4 ..0 定 义bool 标 识 ,-dnssec 。 变 量 必 须 是 指 针 , 否 则package 无法设置其值; 执行命令 197 ..1 类似的,port 选项; ..2 简单的重定义Usage 函数,有点罗嗦; ..3 指定的每个标识,PrintDefaults 将输出帮助信息; ..4 解析标识,并填充变量。 当参数被解析之后,就可以使用它们: if *dnssec { ← 定义传入参数dnssec // 做点啥 } 执行命令 os/exec 包有函数可以执行外部命令,这也是在Go 中 主要的执行命令的方法。通过定义一个有着数个方法 的*exec.Cmd 结构来使用。 执行ls-l : import "os/exec" cmd :=exec.Command("/bin/ls", "-l") err :=cmd.Run() 198 Chapter 8: 通讯 上面的例子运行了“ls -l”,但是没有对其返回的数据进行 任何处理,通过如下方法从命令行的标准输出中获得信 息: import "exec" cmd :=exec.Command("/bin/ls", "-l") buf, err := cmd.Output() ← buf 是一个[]byte 网络 所有网络相关的类型和函数可以在net 包中找到。这其中 最重要的函数是Dial。当Dial 到远程系统,这个函数返 回Conn 接口类型,可以用于发送或接收信息。函数Dial 简洁的抽象了网络层和传输层。因此IPv4 或者IPv6,TCP 或者UDP 可以共用一个接口。 通过TCP 连接到远程系统(端口80),然后是UDP,最后 是TCP 通过IPv6,大致是这样a: conn, e := Dial("tcp", "192.0.32.10:80") conn, e := Dial("udp", "192.0.32.10:80") a在这个例子中,可以认为192.0.32.10 和2620:0:2d0:200::10 是www. example.org。 网络 199 conn, e := Dial("tcp", "[2620:0:2d0:200::10]:80") ← 方括号是强制的 如果没有错误(由e 返回),就可以使用conn 从套接字中 读写。在包net 中的原始定义是: // Read reads data from the connection. Read(b[] byte)(n int, err error) 这使得conn 成为了io.Reader。 // Write writes data to the connection. Write(b[] byte)(n int, err error) 这 同 样 使 得conn 成 为 了io.Writer , 事 实 上conn 是io.ReadWriter。b 但是这些都是隐含的低层c,通常总是应该使用更高层次 的包。例如http 包。一个简单的http Get 作为例子: package main import ( "io/ioutil";"net/http";"fmt" ) ..0 b变 量conn 同 样 实 现 了close 方 法 , 这 使 其 成 为 一 个io.ReadWriteCloser。 c练习Q33 是关于使用这些的。 200 Chapter 8: 通讯 func main() { r, err:=http.Get("http://www.google.com/ robots.txt") ..1 if err != nil { fmt.Printf("%s\n", err.String() ); return } ..2 b, err:=ioutil.ReadAll(r.Body) ..3 r.Body.Close() if err ==nil { fmt.Printf("%s", string(b)) } ..4 } ..0 需要的导入; ..1 使用http 的Get 获取html; ..2 错误处理; ..3 将整个内容读入b; ..4 如果一切OK 的话,打印内容。 练习 Q29. (8) 进程 练习 201 1. 编写一个程序,列出所有正在运行的进程,并打 印每个进程执行的子进程个数。输出应当类似: Pid0has2children:[12] Pid490has2children:[119926524] Pid1824has1child:[7293] • 为了获取进程列表,需要得 到ps -e -opid,ppid,comm 的 输 出 。 输 出 类似: PIDPPIDCOMMAND 90249023zsh 195609024ps • 如果父进程有一个子进程,就打印child, 如果多于一个,就打印children; • 进程列表要按照数字排序,这样就以pid 0 开 始,依次展示。 这里有一个Perl 版本的程序来帮助上手(或者造 成绝对的混乱)。 Listing 8.3. Processes in Perl #!/usr/bin/perl -l my (%child, $pid, $parent) ; 202 Chapter 8: 通讯 my @ps=`ps -e-opid,ppid,comm` ;# Capture theoutput from `ps` foreach (@ps[1..$#ps]) {# Discard theheader line ($pid, $parent, undef) = split ;# Split the line, discard 'comm' push @{$child{$parent}}, $pid ;# Save the child PIDs on a list } # Walk through thesorted PPIDs foreach (sort { $a<=>$b } keys %child) { print "Pid ", $_," has ", @ {$child{$_}} +0, " child", @{$child{$_}} == 1 ? ": " : "ren: ", "[@{$child{$_}}]"; } Q30. (5) 单词和字母统计 1. 编写一个从标准输入中读取文本的小程序,并进 行下面的操作: 1. 计算字符数量(包括空格); 2. 计算单词数量; 3. 计算行数。 练习 203 换句话说,实现一个wc(1)(参阅本地的手册页 面),然而只需要从标准输入读取。 Q31. (4) Uniq 1. 编写一个Go 程序模仿Unix 命令uniq 的功能。程 序应当像下面这样运行,提供一个下面这样的列 表: 'a''b''a''a''a''c''d''e''f''g' 它将打印出没有后续重复的项目: 'a''b''a''c''d''e''f' 下面列出的8.6 是Perl 实现的算法。 Listing 8.6. uniq(1) 的Perl 实现 #!/usr/bin/perl my @a = qw/abaaacdefg/; print my $first = shift @a; foreach (@a) { if ($first ne $_) { print; $first = $_; } } Q32. (9) Quine A Quine 是一个打印自己的程序。 1. 用Go 编写一个Quine 程序。 204 Chapter 8: 通讯 Q33. (8) Echo 服务 1. 编 写 一 个 简 单 的echo 服 务 。 使 其 监 听 于 本 地 的TCP 端口8053 上。它应当可以读取一行(以换 行符结尾),将这行原样返回然后关闭连接。 2. 让这个服务可以并发,这样每个请求都可以在独 立的goroutine 中进行处理。 Q34. (9) 数字游戏 • 从列表中随机选择六个数字: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 25, 50, 75, 100 数字可以多次被选中; • 从1 ... 1000 中选择一个随机数i; • 尝试用先前的六个数字(或者其中的几个)配合运 算符+,−,∗和/,计算出i; 例如,选择了数字:1,6,7,8,8 和75。并且i 为977。 可以用许多方法来实现,其中一种: ((((1 ∗ 6) ∗ 8) + 75) ∗ 8) − 7 = 977 或者 (8 ∗ (75 + (8 ∗ 6))) − (7/1) = 977 练习 205 1. 实现像这样的数字游戏。使其打印像上面那样格 式的结果(也就是说,输出应当是带有括号的中 序表达式) 2. 计算全部可能解,并且全部显示出来(或者仅显 示有多少个)。在上面的例子中,有544 种方法。 Q35. (8) *Finger 守护进程 1. 编写一个finger 守护进程,可以工作于finger(1) 命 令。 来自Debian 的包描述: Fingerd 是一个基于RFC 1196 [36] 的简 单的守护进程,它为许多站点提供 了“finger”程序的接口。这个程序支持 返回一个友好的、面向用户的系统或 用户当前状况的详细报告。 最基本的只需要支持用户名参数。如果用户 有.plan 文件,则显示该文件内容。因此程序需 要能够提供: • 用户存在吗? • 如果用户存在,显示.plan 文件的内容。 答案 207 答案 A29. (8) 进程 1. 有许多工作需要做。可以将程序分为以下几个部 分: 1. 运行ps 获得输出; 2. 解析输出并保存每个PPID 的子PID; 3. 排序PPID 列表; 4. 打印排序后的列表到屏幕。 在下面的解法中,使用了一个map[int][]int,就 是一个使用整数作为map 的索引,元素是整数 的slice ——用于保存PID。内建的append 被用于扩 展这个整数的slice。 程序清单: Listing 8.4. Go 中的进程 package main import ( "fmt";"os/exec";"sort";"strconv" ;"string" ) 208 Chapter 8: 通讯 func main() { ps :=exec.Command("ps", "-e", "- opid,ppid,comm") output, _ :=ps.Output() child := make(map[int][]int) for i, s := range strings.Split( string(output), "\n") { if i ==0 len(s) == 0 { continue } // 去除第一 行和最后一行 f := strings.Fields(s) fpp, _ :=strconv.Atoi(f[1]) // 父pid fp, _ := strconv.Atoi(f[0]) // 子pid child[fpp] = append(child[ fpp], fp) } schild := make([]int , len(child)) i :=0 for k, _ := range child {schild[i] = k; i++ } 答案 209 sort.Ints(schild) for _, ppid := range schild { fmt.Printf("Pid %d has%d child", ppid, len(child [ppid])) iflen (child[ppid]) ==1 { fmt.Printf(": %v\n", child[ppid]) continue } fmt.Printf("ren: %v\n", child[ppid]) } } A30. (5) 单词和字母统计 1. 下面是wc(1) 的一种实现。 Listing 8.5. wc(1) 的Go 实现 package main import ( 210 Chapter 8: 通讯 "os" "fmt" "bufio" "strings" ) func main() { var chars, words, lines int r :=bufio.NewReader(os.Stdin) ..0 for { switch s, ok := r.ReadString ('\n'); true {..1 case ok != nil: ..2 fmt.Printf("%d %d %d \n", chars, words, lines) ; return default:..3 chars += len(s) words += len(strings .Fields(s)) lines++ } } 答案 211 } ..0 Start a new reader that reads from standard input; ..1 Read a line from the input; ..2 If we received an error, we assume it was be- cause of a EOF. So we print the current values; ..3 Otherwise we count the charaters, words and increment the lines. A31. (4) Uniq 1. 下面是uniq 的Go 实现. Listing 8.7. uniq(1) 的Go 实现 package main import "fmt" func main() { list :=[] string{"a", "b", "a", "a", "c", "d", "e", "f"} first :=list[0] 212 Chapter 8: 通讯 fmt.Printf("%s ", first) for _, v := range list[1:] { if first != v { fmt.Printf("%s ", v) first = v } } } A32. (9) Quine 1. 下面的Quine 来自Russ Cox: /* Go quine */ package main import "fmt" func main() { fmt.Printf("%s%c%s%c\n", q, 0x60, q, 0x60) } var q = `/* Go quine */ package main import "fmt" func main() { 答案 213 fmt.Printf("%s%c%s%c\n", q, 0x60, q, 0x60) } varq = ` A33. (8) Echo 服务 1. 一个简单的echo 服务器是这样: Listing 8.8. 简易echo 服务器 package main import ( "net";"fmt";"bufio" ) func main() { l, err:=net.Listen("tcp", " 127.0.0.1:8053") if err != nil { fmt.Printf("Failure to listen: %s\n", err. Error()) } for { if c, err :=l.Accept() ; err ==nil { Echo(c) } 214 Chapter 8: 通讯 } } func Echo(c net.Conn) { defer c.Close() line, err:=bufio.NewReader(c). ReadString('\n') if err != nil { fmt.Printf("Failure toread: %s\n", err.Error()) return } _, err= c.Write([] byte(line)) if err != nil { fmt.Printf("Failure towrite : %s\n", err.Error()) return } } 当运行起来的时候可以看到如下内容: % nc127.0.0.18053 Gois*awesome* Gois*awesome* 2. 为了使其能够并发处理链接,只需要修改一行代 答案 215 码,就是: if c, err :=l.Accept() ; err == nil { Echo(c )} 改为: if c, err :=l.Accept() ; err == nil { go Echo(c) } A34. (9) 数字游戏 1. 下面的是一种可能的解法。它使用了递归和回溯 来得到答案。 Listing 8.9. 数字游戏 package main // 为了增加而需要重写 import ( "fmt";"strconv";"flag" ) const ( _ = 1000 * iota ADD SUB 216 Chapter 8: 通讯 MUL DIV MAXPOS = 11 ) var mop = map[int ]string{ADD: "+", SUB: "-", MUL: "*", DIV: "/",} var ( ok bool value int ) type Stack struct { i int data [MAXPOS]int } func (s *Stack) Reset() { s.i = 0 } func (s *Stack) Len() int { return s.i } func (s *Stack) Push(k int){ s.data[s.i] = k; s.i++ } 答案 217 func (s *Stack) Pop() int { s.i--; return s.data[s.i] } var found int var stack = new(Stack) func main() { flag.Parse() list :=[] int{1, 6, 7, 8, 8, 75,ADD , SUB, MUL, DIV } magic, ok:=strconv.Atoi(flag.Arg (0)) // Arg0 是i if ok != nil { return } f := make([]int , MAXPOS) solve(f, list, 0, magic) } func solve(form, numberop []int, index, magic int){ var tmp int for i, v := range numberop { if v ==0 { goto NEXT} if v < ADD {// 是一个数 字,保存起来 218 Chapter 8: 通讯 tmp = numberop[i] numberop[i] = 0 } form[index] = v value, ok = rpncalc(form[0 : index+1]) if ok &&value == magic { if v < ADD { numberop[i] = tmp // 重置 并继续 } found++ fmt.Printf("%s = %d #%d\n", rpnstr (form[0:index +1]), value, found) } if index ==MAXPOS-1 { if v < ADD { numberop[i] 答案 219 = tmp // 重置 并继续 } goto NEXT } solve(form, numberop, index +1, magic) if v < ADD { numberop[i] = tmp // 重置并继续 } NEXT: } } func rpnstr(r []int) (ret string){// 将rpn 转换到固定的标记 s := make([]string, 0) // 分配内存 for k, t := range r { switch t { case ADD, SUB, MUL, DIV: a, s :=s[ len(s)-1], s[:len(s)-1] b, s :=s[ len(s)-1], 220 Chapter 8: 通讯 s[:len(s)-1] if k == len(r)-1 { s = append(s , b+mop [t]+a) } else { s = append(s , "("+b +mop[t ]+a+")" ) } default: s = append(s, strconv.Itoa(t) ) } } for _, v := range s { ret +=v } return } func rpncalc(r []int)(int, bool){ stack.Reset() for _, t := range r { 答案 221 switch t { case ADD, SUB, MUL, DIV: if stack.Len() < 2 { return 0, false } a :=stack.Pop() b :=stack.Pop() if t == ADD { stack. Push(b + a) } if t == SUB { // 不接受负 数 if b-a < 0 { return 0, false } stack.Push(b - a) } if t == MUL { stack. Push(b * a) } 222 Chapter 8: 通讯 if t == DIV { if a ==0 { return 0, false } // 不接受余 数 if b%a != 0 { return 0, false } stack.Push(b / a) } default: stack.Push(t) 答案 223 } } if stack.Len() ==1 {// 只有一个! return stack.Pop(), true } return 0, false } 2. 开始运行permrec 时,输入977 作为第一个参数: %./permrec977 1+(((6+7)*75)+(8/8))=977#1 ... ... ((75+(8*6))*8)-7=977 #542 (((75+(8*6))*8)-7)*1=977#543 (((75+(8*6))*8)-7)/1=977#544 A35. (8) *Finger 守护进程 1. TODO A 版权 本作品由LaTEX创作。主文本设置为Google Droid 字体。所 有打印文本设置为DejaVu Mono。中文字体设置为UKai。 贡献者 下面的成员编写了这本书。 • Miek Gieben ; • JC van Winkel; • 邢星,中文译本。 帮助试读、检查练习和改进文案(不论真实姓名还 是别名都不分排名先后)Andrey Mirtchovski,Anthony Magro ,Babu Sreekanth ,Ben Bullock ,Bob Cunning- ham ,Brian Fallik ,Cecil New ,Damian Gryski ,Dan Kortschak ,Filip Zaludek ,Haiping Fan ,Jaap Akker- huis,JC van Winkel,Jeroen Bulten,Jinpu Hu,Jonathan Kans ,Makoto Inoue ,Mayuresh Kathe ,Michael 贡献者 225 Stapelberg ,Olexandr Shalakhin ,Paulo Pinto ,Peter Kleiweg ,Russel Winder ,Sonia Keys ,Stefan Schroeder ,Thomas Kapplet ,T.J. Yang ,Uriel , 邢 星。 Miek Gieben Miek Gieben 从荷兰内梅亨大学取得计算机科学硕士 学位。他参与并开发了DNSSEC 协议——下一代DNS— —DNSSEC protocol 以及如核心认证[10]。在玩过了Erlang 后 ,Go 是他当前迷恋的主要语言。他将所有业余 时 间 用 来 对Go 的 探 索 和 编 码 。 他 是Go DNS 库 的 维 护者:https://github.com/miekg/godns 。他的个人博客 是http://www.miek.nl 以及Twitter 帐号@miekg。博文和推 多数情况下都是关于Go 的。 226 Appendix A: 版权 邢星 在你看到本书的中文版的时候,邢星已经从中山大学获 得了软件工程硕士学位。他的博客是http://mikespook. com 并且拥有Twitter 帐号@mikespook。博文和推大部分情 况下都是用来扯蛋的,而且会时不时的扯疼…… 许可证和版权 本 作 品 依 照 署 名-非 商 业 性 使 用-相 同 方 式 共 享3.0 Un- ported许 可 证 发 布 。 访 问http://creativecommons.org/ licenses/by-nc-sa/3.0/ 查看该许可证副本,或写信 到Creative Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA。 本书所有实例代码依此方式放入公共领域。 许可证和版权 227 ©Miek Gieben – 2010:,2012。 ©邢星– 2011,2012。 B 索引 array capacity, 42 length, 42 multidimensional, 40 buffered, 193 built-in append, 39, 45 cap, 38 close, 38 complex, 39 copy, 39, 46 delete, 38 imag, 39 len, 38 make, 39, 124 new, 39, 124 panic, 39 print, 39 println, 39 real, 39 recover, 39 channel, 10, 180 blocking read, 184 blocking write, 184 non-blocking read, 184 non-blocking write, 184 unbuffered, 184 channels, 180 closure, 73 complex numbers, 39 deferred list, 73 duck typing, 154 field anonymous, 131 fields, 128 function as values, 75 call, 132 Index 229 literal, 73 literals, 75 generic, 162 goroutine, 178 goroutines, 10 interface, 152 set of methods, 152 type, 152 value, 153 io.Reader, 195 keyword break, 27, 31 continue, 32 default, 36 defer, 72 else, 28 fallthrough, 35 for, 30 go, 178 goto, 30 if, 27 import, 104 iota, 19 map, 47 add elements, 48 existence, 48 remove elements, 48 package, 100 range, 33, 47 on maps, 34, 47 on slices, 33 return, 27 select, 182 struct, 130 switch, 34 type, 129 label, 30 literal composite, 40, 128 method, 61 method call, 133 methods inherited, 134 MixedCaps, 105 230 Appendix B: Index named return parameters, 61 networking Dial, 198 nil, 122 operator address-of, 122 and, 24 bit wise xor, 24 bitwise and, 24 clear, 24 or, 24 channel, 180 increment, 123 not, 24 or, 24 package bufio, 104, 111, 193 bytes, 104 compress/gzip, 104 encoding/json, 112 even, 100 flag, 112 fmt, 39, 110 io, 111, 195 net/http, 112 os, 112 os/exec, 113, 197 reflect, 113, 166 ring, 105 sort, 111 strconv, 111 sync, 112 text/template, 112 unsafe, 113 parallel assignment, 16, 31 pass-by-value, 61 private, 103 public, 103 receiver, 61 reference types, 42 runes, 34 scope local, 63 slice Index 231 capacity, 42 length, 42 string literal interpreted, 22 raw, 22 toolin go test, 107 tooling go, 14 build, 14 type assertion, 157 type switch, 155 variables _, 17 assigning, 15 declaring, 15 underscore, 17 C Bibliography [1] Haskell Authors. Haskell. http://www.haskell.org/, 1990. [2] Inferno Authors. Inferno. http://www.vitanuova.com/ inferno/, 1995. [3] Plan 9 Authors. Plan 9. http://plan9.bell-labs.com/ plan9/index.html, 1992. [4] Plan 9 Authors. Limbo. http://www.vitanuova.com/ inferno/papers/limbo.html, 1995. [5] Mark C. Chu-Carroll. Google’s new language: Go. http://scienceblogs.com/goodmath/2009/11/googles_ new_language_go.php, 2010. [6] D. Crockford. The application/json media type for javascript object notation (json). http://www.ietf.org/ rfc/rfc4627.txt, 2006. [7] Brian Kernighan Dennis Ritchie. The C programming language, 1975. Bibliography 233 [8] Ericsson Cooperation. Erlang. http://www.erlang.se/, 1986. [9] Larry Wall et al. Perl. http://perl.org/, 1987. [10] Kolkman & Gieben. Dnssec operational practices. http: //www.ietf.org/rfc/rfc4641.txt, 2006. [11] Go Authors. Defer, panic, and recover. http://blog. golang.org/2010/08/defer-panic-and-recover.html , 2010. [12] Go Authors. Effective Go. http://golang.org/doc/ effective_go.html, 2010. [13] Go Authors. Go faq. http://golang.org/doc/go_faq. html, 2010. [14] Go Authors. Go language specification. http://golang. org/doc/go_spec.html, 2010. [15] Go Authors. Go package documentation. http:// golang.org/doc/pkg/, 2010. [16] Go Authors. Go release history. http://golang.org/ doc/devel/release.html, 2010. 234 Appendix C: Bibliography [17] Go Authors. Go tutorial. http://golang.org/doc/go_ tutorial.html, 2010. [18] Go Authors. Go website. http://golang.org/, 2010. [19] Go Authors. Getting Started. http://golang.org/doc/ install/, 2012. [20] Go Community. Function accepting a slice of interface types. http://groups.google.com/group/golang-nuts/ browse_thread/thread/225fad3b5c6d0321, 2010. [21] Go Community. Go issue 65: Compiler can’t spot guar- anteed return in if statement. http://code.google. com/p/go/issues/detail?id=65, 2010. [22] James Gosling et al. Java. http://oracle.com/java/, 1995. [23] LAMP Group at EPFL. Scala. http://www.scala-lang. org/, 2003. [24] C. A. R. Hoare. Quicksort. http://en.wikipedia.org/ wiki/Quicksort, 1960. [25] C. A. R. Hoare. Communicating sequential processes (csp). http://www.usingcsp.com/cspbook.pdf, 1985. Bibliography 235 [26] Rob Pike. Newsqueak: A language for communicating with mice. http://swtch.com/~rsc/thread/newsqueak. pdf, 1989. [27] Rob Pike. The Go programming language, day 2. http: //golang.org/doc/{G}oCourseDay2.pdf, 2010. [28] Rob Pike. The Go programming language, day 3. http: //golang.org/doc/{G}oCourseDay3.pdf, 2010. [29] Bjarne Stroustrup. The C++ programming language, 1983. [30] Ian Lance Taylor. Go interfaces. http://www.airs.com/ blog/archives/277, 2010. [31] Imran On Tech. Using fizzbuzz to find devel- opers... http://imranontech.com/2007/01/24/ using-fizzbuzz-to-find-developers-who-grok-coding/ , 2010. [32] Wikipedia. Bubble sort. http://en.wikipedia.org/ wiki/Bubble_sort, 2010. [33] Wikipedia. Communicating sequential processes. http://en.wikipedia.org/wiki/Communicating_ sequential_processes, 2010. 236 Appendix C: Bibliography [34] Wikipedia. Duck typing. http://en.wikipedia.org/ wiki/Duck_typing, 2010. [35] Wikipedia. Iota. http://en.wikipedia.org/wiki/Iota, 2010. [36] D. Zimmerman. The finger user information protocol. http://www.ietf.org/rfc/rfc1196.txt, 1990. 当前页留空。
还剩251页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 8 金币 [ 分享pdf获得金币 ] 1 人已下载

下载pdf

pdf贡献者

wengyicai

贡献于2013-05-26

下载需要 8 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf