学习Go语言中文版


学习Go 语言 作者: Miek Gieben 译者: 邢兴 感谢: Go 作者 Google Go Nuts 邮件列表 This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 3.0 License. Miek Gieben – ©2010, 2011 邢兴– ©2011 本作品依照署名-非商业性使用-相同方式共享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.4 更新至Go 版本release.2011-03-07。 Table of Contents 1 简介 vi 官方文档 ......................................vii 获得 Go ......................................vii 前身 ........................................viii 练习 ........................................viii 答案 ........................................ xi 2 基础 1 Hello World .................................... 1 编译和运行代码 .................................. 2 变量、类型和保留字 ................................ 3 运算符和内建函数 ................................. 6 Go 保留字 ..................................... 7 控制结构 ...................................... 7 内建函数 ......................................12 array、slices 和 map ................................13 练习 ........................................18 答案 ........................................19 3 函数 24 作用域 .......................................25 多个返回值 .....................................26 命名返回参数 ...................................27 延迟的代码 .....................................28 变参 ........................................29 函数作为值 .....................................30 回调和闭包 .....................................30 恐慌(Panic)和恢复(Recover).........................31 练习 ........................................31 答案 ........................................35 4 包 42 构建一个包 .....................................43 标识符 .......................................44 包的文档 ......................................45 测试包 .......................................45 常用的包 ......................................47 练习 ........................................48 答案 ........................................49 5 进阶 52 内存分配 ......................................53 定义自己的类型 ..................................55 转换 ........................................57 练习 ........................................58 答案 ........................................61 ii Chapter: Table of Contents 6 接口 64 Methods ......................................66 Interface names ..................................67 A sorting example .................................68 Introspection ...................................70 Exercises ......................................73 Answers ......................................75 7 并行 76 More on channels .................................78 Exercises ......................................79 Answers ......................................81 8 通讯 84 Files ........................................84 Command line arguments .............................85 Executing commands ...............................85 Networking ....................................86 Netchan: networking and channels ........................87 Exercises ......................................87 Answers ......................................89 A 版权 96 贡献者 .......................................96 许可证和版权 ...................................97 B 索引 98 C Bibliography 100 List of Figures 1.1 Go 编年史 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . viii 2.1 array 与 slice 对比 ................................15 3.1 一个简单的 LIFO 栈 ...............................32 5.1 Pointers and types ................................52 6.1 Peeling away the layers using reflection .....................73 List of Tables 2.1 运算优先级 .................................... 6 2.2 Go 中的保留字 .................................. 7 2.3 Go 中的预定义函数 ................................12 List of Code Examples iii 5.1 Valid conversions ................................57 List of Code Examples 2.1 Hello world .................................... 2 2.2 Makefile for a program .............................. 3 2.3 Declaration with = ................................ 3 2.4 Declaration with := ................................ 3 2.5 Familiar types are still distinct .......................... 4 2.6 array 和 slice ...................................16 2.7 Simple for loop ..................................19 2.8 For loop with an array ..............................19 2.9 Fizz-Buzz .....................................20 2.10 Strings ......................................21 2.11 Runes in strings ..................................21 2.12 Reverse a string ..................................22 3.1 函数定义 .....................................24 3.2 递归函数 .....................................25 3.3 局部作用域 ....................................25 3.4 全局作用域 ....................................25 3.5 当函数调用函数时的作用域 ............................25 3.6 没有 defer .....................................28 3.7 With defer ....................................28 3.8 函数符号 .....................................29 3.9 带参数的函数符号 .................................29 3.10 在 defer 中访问返回值 ..............................29 3.11 Anonymous function ...............................30 3.12 使用 map 的函数作为值 ..............................30 3.13 Go 中的平均值函数 ................................35 3.14 stack.String() ...................................37 3.15 有变参的函数 ...................................37 3.16 Fibonacci function in Go .............................38 3.17 Map 函数 ......................................38 3.18 Bubble sort ....................................39 4.1 A small package ..................................42 4.2 Use of the even package .............................42 4.3 用于包的 Makefile ................................43 4.4 even 包的测试 ...................................46 4.5 包里的 Stack ...................................49 4.6 逆波兰计算器 ...................................49 5.1 Use of a pointer ..................................52 5.2 获取指针指向的值 .................................52 5.3 Structures .....................................55 5.4 Go 中更加通用的 map 函数 ............................61 5.5 cat 程序 ......................................62 6.1 Defining a struct and methods on it .......................64 6.2 Another type that implements I ..........................65 6.3 A function with a empty interface argument ...................66 6.4 Failing to implement an interface ........................66 iv Chapter: Table of Contents 6.5 Failure extending built-in types ..........................67 6.6 Failure extending non-local types .........................67 6.7 Dynamically find out the type ..........................70 6.8 A more generic type switch ............................71 6.9 Introspection using reflection ..........................71 6.10 Reflection and the type and value ........................72 6.11 Reflect with private member ...........................73 6.12 Reflect with public member ...........................73 7.1 Go routines in action ...............................76 7.2 Go routines and a channel ............................77 7.3 Using select ....................................78 7.4 Channels in Go ..................................81 7.5 Adding an extra quit channel ...........................81 7.6 A Fibonacci function in Go ............................82 8.1 Reading from a file (unbufferd) ..........................84 8.2 Reading from a file (bufferd) ...........................84 8.3 Processes in Perl .................................87 8.6 uniq(1) in Perl ..................................88 8.4 Processes in Go ..................................89 8.5 wc(1) in Go ....................................90 8.7 uniq(1) in Go ...................................91 8.8 Number cruncher .................................92 List of Exercises 1 (1) Documentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . viii 2 (1) For-loop ....................................18 3 (1) FizzBuzz ....................................18 4 (1) Strings .....................................18 5 (4) Average ....................................18 6 (4) 平均值 .....................................31 7 (3) 整数顺序 ....................................31 8 (4) 作用域 .....................................31 9 (5) 栈 .......................................32 10 (5) 变参 ......................................32 11 (5) 斐波那契 ....................................32 12 (4) Map function .................................32 13 (3) 最小值和最大值 ................................32 14 (5) 冒泡排序 ....................................32 15 (6) 函数返回一个函数 ...............................33 16 (2) stack 包 ....................................48 17 (7) 计算器 .....................................48 18 (6) 使用 interface 的 map 函数 ..........................58 19 (6) 指针 ......................................58 20 (6) 链表 ......................................58 21 (6) Cat .......................................58 22 (8) 方法调用 ....................................58 23 (6) Interfaces and compilation ..........................73 24 (5) Pointers and reflection ............................74 List of Exercises v 25 (1) Interfaces and min-max ............................74 26 (4) Channels ...................................79 27 (7) Fibonacci II ..................................79 28 (8) Processes ...................................87 29 (5) Word and letter count .............................87 30 (4) Uniq ......................................88 31 (9) Quine .....................................88 32 (9) Number cruncher ...............................88 1 简介 我对此感兴趣,并且希望做点什么。 在为Go 添加复数支持时 KENTHOMPSON 这是关于来自Google 的Go 语言的简介。目标是为这个新的、革命性的语言提供一个指南。 什么是Go?来自于网站:[9]: Go 编程语言是一个使得程序员更加有效率的开源项目。Go 是有表达力、简 洁、清晰和有效率的。它的并行机制使其很容易编写多核和网络应用,而新奇 的类型系统允许构建有弹性的模块化程序。Go 编译到机器码非常快速,同时 具有便利的垃圾回收和强大的运行时反射。它是快速的、静态类型编译语言, 但是感觉上是动态类型的,解释型语言。 这本书的目标读者是那些熟悉编程,并且了解多种编程语言,例如C[19],C++[29],Perl [21],Java [20],Erlang[18],Scala[1],Haskell[10]。这不是教你如何编程的书,只是教你 如何使用Go。 学习一样新东西,最佳的方式可能是通过建立自己的程序来探索它。因此每章都包含了 若干练习(和答案)让你熟悉这个语言。练习标有编号Qn,而n 是一个数字。在练习编号 后面的圆括号中指定了该题的难度。难度范围从0 到9,0 是最简单,而9 最难。其后为了容 易索引,提供了一个简短的名字。例如 Q1. (1) map 函数… 展示了难度等级1、编号Q1 的关于map() 函数的问题。除了那些极端麻烦的、未解决的 问题,相关答案在练习的下一页。 答案的顺序和练习一致,而对于以An 开头的问题,对应编号n 的练习。 Go 是一个年轻的语言,特性仍然在不断增加或删除中。所以当你阅读的时候,可能部分 内容已经过时。一些练习的答案可能会随着Go 不断的演化而变成错误的。我们将尽可能让 这个文档与最新的Go 发布版本保持一致。通过努力已经建立了“特性检验”代码示例。 本书使用了下面的约定: • 代码用DejaVu Mono 显示; • 关键词用DejaVuMonoBold 显示; • 注释用DejaVu Mono Italic 显示; • 代码中额外的标记, ← 用这种形式展现; • 使用数字 ..1 对长内容标记——解释会跟随其后; • 行号在右边展示; • Shell 示例用% 作为标记; • 强调的段落会缩进,在左边有竖线。 官方文档 vii 官方文档 Go 已经有大量的文档。例如Go Tutorial [8] 和Effective Go [3]。网站http://golang.org/ doc/ 是绝佳的起点a。虽然并不一定要阅读这些文档,但是强烈建议这么做。 Go 用叫做godoc 的Go 程序格式化其文档。你可以用它查阅在线文档。例如,假设我们 需要了解关于hash 包的更多信息。可以用命令godochash 查阅它。如何创建你自己的包的 文档在第4 章中介绍。 获得Go 当前(2011)任何Linux 发行版都没有提供Go 安装包。安装Go 的过程因此也比预期的要稍 微长一些。当Go 稳定之后,这一情况应该会得到改善。现在来说,你需要从mercurial 中获 取源代码,然后编译Go。对于其他类Unix 系统,过程类似。 • 首先安装Mercurial (获取hg 命令)。在Ubuntu/Debian/Fedora 需要安装mercurial 包; • 为了编译Go 需要包:bison,gcc,libc6-dev,ed,gawk 和make; • 设置环境变量GOROOT 为Go 安装目录: % exportGOROOT=~/go • 然后获取Go 源代码: % hgclone-rreleasehttps://go.googlecode.com/hg/$GOROOT • 设置PATH 到Go 的二进制文件所在目录,这样Shell 可以找到它们: % exportPATH=$GOROOT/bin:$PATH • 编译Go % cd$GOROOT/src % ./all.bash 如果全部都没问题,你应当看到下面的内容: InstalledGoforlinux/amd64in/home/gobook/go. Installedcommandsin/home/gobook/go/bin. Thecompileris6g. 现在,Go 已经被安装到了系统中,可以开始游戏了。 保持更新 新的发布会公布在Go Nuts 邮件列表[17] 中。将已经存在的代码树更新到最新,需要执行: % cd$GOROOT % hgpull % hgupdaterelease % cdsrc % ./all.bash ahttp://golang.org/doc/ 本身是由Go 程序godoc 提供服务的。 viii Chapter 1: 简介 看看你现在用的版本: % cd$GOROOT % hgidentify 79997f0e5823release/release.2010-10-20 这是release.2010-10-20 版本。 前身 Go 的前身来自于Inferno [11](基于Plan 9 [14] 的改造)。Inferno 包含了一个叫做Limbo [13] 的语言。来自于Limbo 论文中的引用: Limbo 是用于开发运行在小型计算机上的分布式应用的编程语言。它支持模 块化编程,编译期和运行时的强类型检查,进程内基于具有类型的channel 通 讯,原子性垃圾收集,和简单的抽象数据类型。它被设计用于即便是没有硬件 内存保护的小型设备上,也能安全的运行。 Limbo 的一个特性已经包含进了Go 用于支持交叉编译。 Go 从Limbo 继承的另一个特性TODO Plan 9 上 的 首 席 语 言。在1995 年提出。 是channel(参阅第7 章)。从Limbo 文档来的另一段: [channel] 是用于向系统中其他代理发送和接收特定类型对象的通讯机制。channel 可以用于本地进程间通讯;用于连接到命名的目的地的库方法。两种情况都是 直接发送和接收操作的。 在Go 中,channel 比在Limbo 中更加好用。如果我们对Go 的历史深入探索,会发现一个 到”Newsqueak” [28] 的引用,这是在类C 语言中使用channel 通讯的拓荒者。channel 对于 这些语言并不是独一无二的,另一个非类C 语言,Erlang [18],也在使用它。 Figure 1.1. Go 编年史 Newsqueak Limbo GoErlang 1994 19951986 2009 使用channel 与其他进程进行通讯的办法,叫做通讯序列化过程(Communicating Sequential Processes - CSP),由C. A. R. Hoare [23] 设计构想,而他正是那个发明快速排 序[24] 算法的人。 Go 是第一个实现了简单的(或更加简单的)并行开发的、夸平台类C 语言。 练习 Q1. (1) Documentation 1. Go 的文档可以通过godoc 程序阅读,它包含在Go 的发布包中。 godochash 给出了hash 包的信息。阅读container 包的帮助,给出了下面的结果: 练习 ix SUBDIRECTORIES heap list ring vector 哪个godoc 的命令可以显示container 包中的vector 文档? 答案 xi 答案 A1. (1) Documentation 1. vector 包在container 的子目录中,所以只需要 godoccontainer/vector 即可。 也可以指定“Go 手册”中某个函数的的文档。例如,函数Printf 在fmt 包中,仅阅读 这个函数的文档,使用:godocfmtPrintf 。 甚至可以显示源代码:godoc-srcfmtPrintf 。 2 基础 在Go 中,代码说到做到。 Go Nuts 邮件列表 ANDREWGERRAND 有一些东西使得Go 不同于其他语言。 清晰并且简洁 Go 努力保持小并且优美,你可以在短短几行代码里做许多事情。 并行 Go 让函数很容易成为非常轻量的线程。这些线程在Go 中被叫做goroutines a; Channel 这些goroutines 之间的通讯由channel [33][23] 完成; 快速 编译很快,执行也很快。目标是跟C 一样快。编译时间用秒计算; 安全 Go 有垃圾收集,在Go 中无须free(),语言会处理这一切; 标准的格式化 Go 程序可以被格式化为程序员希望的(几乎)任何形式,但是官方格式是存在的。 标准也非常简单:gofmt 的输出就是官方认可的格式。 类型后置 类型在变量名的后面,像这样varaint ,来代替C 中的inta; ; UTF-8 任何地方都是UTF-8的,包括字符串以及程序代码。你可以在代码中使用Φ = Φ + 1; 开源 Go 的许可证是完全开源的,查阅Go 发布的源码中的LICENSE 文件。 开心 用Go 写程序会非常开心! Erlang [18] 与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”。 a是的,它的发音很接近coroutines,但是goroutines 确实有一些不同,我们将在第7 章讨论。 2 Chapter 2: 基础 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こんにちは世界\n") 9} 逐行阅读这个程序。 ..0 首行这个是必须的。所有的Go 文件以package 开头,对于独立运行的执行文件必须是package main; ..1 这是说需要将”fmt” 包加入main。不是main 的其他包都被称为库,其他许多编程语言有着 类似的概念(参阅第4 章)。末尾以// 开头的内容是注释; ..2 这同样是注释,不过这是被包裹于/* 和*/ 之间的; ..3 package main 必须首先出现,紧跟着是import。在Go 中,package 总是首先出现,然后 是import,然后是其他所有内容。当Go 程序在执行的时候,首先调用的函数是main.main() ,这是从C 中继承而来。这里定义了这个函数; ..4 第8 行调用了来自于fmt 包的函数打印字符串到屏幕。字符串由" 包裹,并且可以包含 非ASCII 的字符。这里使用了希腊文和日文。 编译和运行代码 Go 编译器叫做<数字>g,数字是6 表示用于64 位Intel 而8 表示32 位Intel。连接器用相同的 命名方式:<数字>l。在本书中,我们将使用6g 和6l 完成所有的编译。为了编译上面的代 码,执行: % 6ghelloworld.go 并且用6l 链接它: % 6lhelloworld.6 然后执行: % ./6.out ← 默认的(64位) Go 可执行文件名(32 位上是 8.out) Hello,world;or καληµ´ρα κóσµ; or こんにちは世界 变量、类型和保留字 3 使用Makefile 另一种可以少折腾的(在建立好后)编译Go 程序的办法,是使用Makefile。下面是用于构 建helloworld的: Listing 2.2. Makefile for a program 1include $(GOROOT)/src/Make.inc 3TARG=helloworld 4GOFILES=\ 5helloworld.go\ 7include $(GOROOT)/src/Make.cmd 行3 指定了编译的程序的名字,而行5 列举了源代码文件。现在执行make 就可以完成你的程 序编译。留意Go 使用了不同于make 的,叫做gomake 的命令。它是(当前来说)一个GNU make 的小封装。Go 程序的构建系统在将来可能会发生变化,从而make 会被移除。所以我 们使用gomake 进行构建。留意Makefile 创建的可执行文件叫做helloworld,而不是6.out 或 者8.out。 变量、类型和保留字 在接下来的章节中,我们将会了解这个新语言的变量、基本类型、保留字和控制流。Go 在 语法上有着类C 的感觉。如果你希望将两个(或更多)语句放在一行书写,它们必须用分 号(’;’)分隔。一般情况下,你不需要分号。 Go 同其他语言不同的地方在于变量的类型在变量名的后面。不是:int a,而是a int。 当定义了一个变量,它默认赋值为其类型的null 值。这意味着,在var a int后,a 的值 为0。而var s string,意味着s 被赋值为零长度字符串,也就是""。 在Go 中,声明和赋值是两步的一个过程,但是可以连在一起。比较下面作用相同的代码 片段。 Listing 2.3. Declaration with = var a int var b bool a = 15 b = false Listing 2.4. 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 4 Chapter 2: 基础 让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 情况相同。 如果你希望明确其长度,你可以使用int32 或者uint32。完整的整数类型列表(符号 和无符号)是int8,int16,int32,int64 和byte,uint8,uint16,uint32,uint64。byte 是uint8 的别名。浮点类型的值有float32 和float64 (没有float 类型)。64 位的整数和浮 点数总是 64 位的,即便是在32 位的架构上。 需要留意的是这些类型全部都是独立的,并且混合用这些类型向变量赋值会引起编译器 错误,例如下面的代码: Listing 2.5. 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 触发一个赋值错误: types.go:7:cannotusea+ a (typeint)astypeint32inassignment 赋值可以用八进制、十六进制或科学计数法:077, 0xFF, 1e3 or 6.022e23 这些都是合法 的。 常量 常量 在Go 中,也 就是constant。它们在编译时被创建,只能是数字、字符串或布尔 值;const x = 42 生成x 这个常量。可以使用iota b 生成枚举值。 b单词[iota] 在日常英语短语’not one iota’,意思是’不是最小的差异’,是来自新约中的短语:”until heaven and earth pass away, not an iota, not a dot, will pass from the Law.” [35] 变量、类型和保留字 5 const ( a = iota b = iota ) 第一个iota 表示为0,因此a 等于0,当iota 再次在新的一行使用时,它的值增加了1,因此b 的值是1。 也可以像下面这样,省略Go 重复的= iota: const ( a = iota 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',这会报错 在Go 中实现这个,需要下面的方法: s := "hello" c := []byte(s) ..0 c[0] = 'c' ..1 s2 := string(c) ..2 fmt.Printf("%s\n", s2) ..3 ..0 转换s 为字节数组,查阅在第5 章”转换”节、57 页的内容; ..1 修改数组的第一个元素; ..2 创建新的字符串s2 保存修改; ..3 用fmt.Printf 函数输出字符串。 多行字符串 基于分号的置入(查阅[3] 章节”Semicolons”),你需要小心使用多行字符 串。如果这样写: 6 Chapter 2: 基础 s := "Starting part" + "Ending part" 会被转换为: s := "Starting part"; + "Ending part" ; 这是错误的语法,应当这样写: s := "Starting part" + "Ending part" Go 就不会在错误的地方插入分号。另一种方式是使用反引号` 作为原始字符串符 号: s := `Starting part Ending part` 留意最后一个例子s 现在也包含换行。不像转义字符串标识 ,原始字符串标识的 值在引号内的字符是不转义的。 复数 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 支持普通的数字运算符,表格2.1 列出了当前支持的运算符,以及其优先级。它们全部是 从左到右结合的。 Table 2.1. 运算优先级 Precedence Operator(s) Highest * / % << >> & &^ + - | ^ == != < <= > >= <- && Lowest || + - * / 和% 会像你期望的那样工作,& | ^ 和&^ 分别表示位运算符按位与,按位或,按 位异或和位清除。&& 和|| 运算符是逻辑与和逻辑或。表格中没有列出的是逻辑非:!。 虽然Go 不支持运算符重载(或者方法重载),而一些内建运算符却支持重载。例如+ 可 以用于整数、浮点数、复数和字符串(字符串相加表示串联它们)。 Go 保留字 7 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 参阅”变量、类型和保留字” 在3 页; • package 和import 已经有过短暂的接触,在”Hello World” 部分。在第4 章对其有详细 的描述。 其他都有对应的介绍和章节: • func 用于定义函数和方法; • 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 相比):无需圆括号,而语句 体必须总是包含在大括号内。 if 在Go 中if 看起来是这样的: if x > 0 { ← { ismandatory return y } else { return x } c这个章节复制于[3]。 8 Chapter 2: 基础 强制大括号鼓励将简单的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 { 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() if err != nil { return err } doSomething(f, d) 下面的语法在Go 中是非法的: if err != nil { ← 必须同if 在同一行 return err } 参阅[3] ”Semicolons” 章节了解其后更深入的原因。 if-then-else 的结尾 注意如果在函数中这样结束: if err != nil { return err } else { 控制结构 9 return nil } 它不会编译。这是Go 编译器的一个bug。参阅[16] 了解更多关于此问题的描述, 以及可能的修复。 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(;;) 一样(死循环) 短声明使得在循环中声明一个序号变量更加容易。 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 } 10 Chapter 2: 基础 println(i) } 循环嵌套循环时,可以在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) 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” 在13 页)。 ..1 用range 对其进行循环。每一个迭代,range 将返回int 类型的序号,string 类型的 值,以0 和”a” 开始。 ..2 k 的值为0…5,而v 在循环从”a”…”f”。 也可以在字符串上直接使用range。这样字符串被打散成独立的Unicode 字符d 并且起始 位按照UTF-8 解析。循环: for pos, char := range "aΦx" { fmt.Printf("character '%c' starts atbyte position %d\n", char, pos) } 打印 d在UTF-8 世界的字符有时被称作runes。通常,当人们讨论字符时,多数是指8 位字符。UTF-8 字符可能会 有32 位,称作rune。 控制结构 11 character'a'startsatbyteposition0 character' Φ' startsatbyteposition1 character'x'startsatbyteposition3 ← Φ took2bytes switch Go 的switch 非常灵活。表达式不必是常量或整数,执行的过程从上至下,直到找到匹 配项,而如果switch 没有表达式,它会匹配true 。这产生一种可能——使用switch 编 写if-else-if-else 判断序列。 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: 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 12 Chapter 2: 基础 } return false } 这里有一个使用两个switch 对字节数组进行比较的例子: // 比较返回两个字节数组字典数序先后的整数。 // 如果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 // 字符串相等 } 内建函数 预定义了少数函数,这意味着无需引用任何包就可以使用它们。表格2.3 列出了所有的内建 函数。 Table 2.3. Go 中的预定义函数 closenew panic complex closedmakerecoverreal len appendprintimag cap copy println close 和closed 用于channel 通讯和关闭channel,参阅第7 章了解更多。 len 和cap 可用于不同的类型,len 用于返回字符串、slice 和数组的长度。参阅”array、slices 和 map” 小节了解更多关于slice、数组和函数cap 的详细信息。 new 用于各种类型的内存分配。参阅”用 new 分配内存” 在53 页。 make 用于内建类型(map、slice 和channel)的内存分配。参阅”用 make 分配内存” 在53 页。 array、slices 和 map 13 copy 用于复制slice。append 用于追加slice。参阅本章的”slice”。 panic 和recover 用于异常处理机制。参阅”恐慌(Panic)和恢复(Recover)” 在31 页了 解更多信息。 print 和println 是底层打印函数,可以在不引入fmt 包的情况下使用。它们主要用于调 试。 complex、real 和imag 全部用于处理复数。有了之前给的简单的例子,不用再进一步讨论 复数了。 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 } 也可以简写为a :=[...] int{1,2,3 },Go 会自动统计元素的 个数。 注意,所有项目必须都指定。因此,如果你使用多维数组,有一些内容你必须录入: 复 合 声 明 允 许 你 直 接 将 值 赋 值 给array 、slice 或 者map a := [2][2] int{ [2]int{1,2}, [2] int{3,4}} 类似于: a := [2][2] int{ [...]int{1,2}, [...]int{3,4}} 当声明一个array 时,你必须在方括号内输入些内容,数字或者三个点(...)。从release.2010-10-27[7] 这个语法使用更加简单了。来自于发布记录: array、slice 和map 的复合声明变得更加简单。使用复合声明的array、slice 和map,元素复合声明的类型与外部一直,则可以省略。 这表示上面的例子可以修改为: a := [2][2] int{ {1,2},{3,4}} 14 Chapter 2: 基础 slice slice 与array 接近,但是在新的元素加入的时候可以增加长度。slice 总是指向底层的一 个array。slice 是一个指向 array 的指针,这是其与array 不同的地方;slice 是引用类型, 引用类型使用make 创 建。这意味着当赋值某个slice 到另外一个变量,两个引用会指向同一个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)==n ; • len(array)== cap(array)==m . array、slices 和 map 15 Figure 2.1. array 与slice 对比 . . . len == cap == m array . . . slice 0 0 n-1 n-1 m-1 m-1 len == cap == n 16 Chapter 2: 基础 给定一个array 或者其他slice,一个新slice 通过a[I:J] 的方式创建。这会创建一个新 的slice,指向a,从序号I 开始,结束在序号J之前。长度为J-I。 // array[n:m] 从array 创建了一个slice,具有元素n to m-1 a := [...] int{1, 2, 3, 4, 5}..0 s1 := a[2:4] ..1 s2 := a[1:5] ..2 s3 := a[:] ..3 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.6 列出的代码中,我们在第八行尝试做一些错误的事情,让一些东西超出范围(底 层array 的最大长度),然后得到了一个运行时错误。 Listing 2.6. array 和slice 1package main 3func main() { 4var array [100] int // Create array, index from 0 to 99 5slice :=array[0:99] // Create slice, index from 0 to 98 7slice[98] = 'a' // OK 8slice[99] = 'a' // Error: "throw: index out of range" 9} 如果你想要扩展slice,有一堆内建函数让你的日子更加好过一些:append 和copy。来自 于[5]: 函数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 ..0 追加一个元素,s1==[]int{0,0,2} ; array、slices 和 map 17 ..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 (在lib中)。在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 } fmt.Printf("Numbers ofdays in a year: %d\n", year) 向map 增加元素, 可以这样做: monthdays["Undecim"] = 30 ← 添加一个月 monthdays["Feb"] = 29 ← 闰年时重写这个元素 检查元素是否存在,可以使用下面的方式[26]: var value int var present bool value, present = monthdays["Jan"] ← 如果存在,present 则有值true ← 或者更接近Go 的方式 v, ok:=monthdays["Jan"] ← “逗号ok”形式 18 Chapter 2: 基础 也可以从map 中移除元素: monthdays["Mar"] = 0, false ← 删除"Mar" 吧,总是下雨 看起来有点像把“逗号ok”形式反过来。 练习 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 个字符): A AA AAA AAAA AAAAA AAAAAA AAAAAAA ... 2. 建立一个程序统计字符串里的字符数量: asSASAddddsjkdsjsdk 同时输出这个字符串的字节数。提示: 看看utf8 包。 3. 扩展上一个问题的程序,替换位置4 开始的三个字符为’abc’。 4. 编写一个Go 程序可以逆转字符串,例如”foobar” 被打印成”raboof”。提示: 不幸的 是你需要知道一些关于转换的内容,去”转换” 在57” 页的内容。 Q5. (4) Average 1. 编写计算一个类型是float64 的slice 的平均值的代码。在稍候的练习中(Q6 将会改 写为函数。 答案 19 答案 A2. (1) For-loop 1. 有许多种解法,其中一种可能是: Listing 2.7. Simple for loop package main import "fmt" func main() { for i :=0 ; i < 10; i++ { ← Seesection for onpage 9 fmt.Printf("%d\n", i) } } 编译并观察输出。 % 6gfor.go&&6l-oforfor.6 % ./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.8. For loop with an array func main() { var arr [10]int ← Createanarraywith10elements for i :=0 ; i < 10; i++ { arr[i] = i ← Fillitonebyone } fmt.Printf("%v", arr) ← With%vGoprintsthetype } 20 Chapter 2: 基础 也可以用复合声明的硬编码来实现这个: a :=[...] int{0,1,2,3,4,5,6,7,8,9} ← 通过[...] 让Go 来计数 fmt.Printf("%v\n", a) A3. (1) FizzBuzz 1. 下面简单的程序,是一种解决办法。 Listing 2.9. Fizz-Buzz package main import "fmt" 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 } } ..0 Define two constants to make the code more readable. See section ”常量”; ..1 Holds if we already printed someting; ..2 for-loop, see section ”for” ..3 If divisible by FIZZ, print ”Fizz”; ..4 And if divisble by BUZZ, print ”Buzz”. Note that we have also taken care of the FizzBuzz case; ..5 If neither FIZZ nor BUZZ printed, print the value; ..6 Format each output on a new line. A4. (1) Strings 答案 21 1. 这是一个解法: Listing 2.10. Strings package main import "fmt" func main() { str := "A" for i :=0 ; i < 100 ; i++ { fmt.Printf("%s\n", str) str= str + "A" ← Stringconcatenation } } 2. 为了解决这个问题,需要utf8 包的帮助。首先,阅读一下文档godocutf8|less 。 在阅读文档的时候,会注意到func RuneCount(p[] byte)int。然后,将string 转换 为byte slice: str := "hello" b :=[] byte(str) ← 转换,参阅57 页 将这些整合到一起,得到下面的程序。 Listing 2.11. Runes in strings package main import ( "fmt" "utf8" ) func main() { str := "dsjkdshdjsdh....js" fmt.Printf("String %s\nLenght: %d, Runes: %d\n", str, len ([]byte(str)), utf8.RuneCount([]byte (str))) } 3. 可以用下面的方法逆转字符串。我们从左边(i)至右(j)的交换字符,就像这 样: 22 Chapter 2: 基础 Listing 2.12. 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 ave = 0 default:..1 for _, v := range xs { sum+=v } ave = 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 } ..0 保留字func 用于定义一个函数; ..1 函数可以定义用于特定的类型,这类函数更加通俗的称呼是method。这部分称 作receiver 而它是可选的。它将在6 章使用; ..2 funcname 是你函数的名字; ..3 int 类型的变量q 是输入参数。参数用pass-by-value 方式传递,意味着它们会被复 制。但是留意引用类型(slice、channel、map 和interface)用pass-by-reference 传 递,虽然你并没有在代码里直接看到指针; ..4 变量r 和s 是这个函数的named return parameters。注意在Go 的函数中可以返回多 个值。参阅”多个返回值” 在26。如果想要返回无命名的参数,只需要提供类型:(int ,int)。如果只有一个返回值,可以省略圆括号。如果函数是一个子过程,并且没有 任何返回值,也可以省略这些内容; ..5 这是函数体,注意return 是一个语句,所以包裹参数的括号是可选的。 这里有两个例子,左边的函数没有返回值,右边的只是简单的将输入返回。 func subroutine(in int){ return } func identity(in int) int { return in } 可以随意安排函数定义的顺序,编译器会在执行前扫描每个文件。所以函数原型在Go 中 都是过期的旧物。Go 不允许函数嵌套。然而你可以利用匿名函数实现它,参阅本章的”函数 作为值” 在30 页。 递归函数跟其他语言是一样的: 作用域 25 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。 作用域 在Go 中,定义在函数外的变量是全局的,那些定义在函数内部的变量,对于函数来说是局 部的。如果命名覆盖——一个局部变量与一个全局变量有相同的名字——在函数执行的时 候,局部变量将覆盖全局变量。 Listing 3.3. 局部作用域 . package main var a = 6 func main() { p() q() p() } func p() { println(a) } func q() { a :=5 ← 定义 println(a) } 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 中没有定义局部变量,只有全局变量a。这将使得赋值全局可见。这段 代码将会打印:655。 在下面的例子中,我们在f() 中调用g(): Listing 3.5. 当函数调用函数时的作用域 package main var a int func main() { a = 5 26 Chapter 3: 函数 println(a) f() } func f() { a :=6 println(a) g() } func g() { println(a) } 输出内容将是:565。局部变量仅仅在执行定义它的函数时有效。 多个返回值 Go 一个非常特别的特性是函数和方法可以返回多个值(Python 也可以)。这可以用于改 进一大堆在C 程序中糟糕的惯例用法:修改参数的方式,返回一个错误(例如遇到EOF 则返 回-1)。在Go 中,Write 返回一个计数值和一个错误:”是的,你写入了一些字节,但是由 于设备异常,并不是全部都写入了。”。os 包中的*File.Write 是这样声明的: func (file *File) Write(b []byte) (n int , err Error) 如同文档所述,它返回写入的字节数和一个非nil 的Error 当n != len(b)。这是Go 中常见 的样式。 类似的方法避免了传递指针模拟引用参数来返回值。这里有个样例函数,从字节数组的 指定位上取得数值,返回这个值和下一个位置。 func nextInt(b []byte, i int)(int, int){ x :=0 // 假设所有的都是数字 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) } 没有元组作为原生类型,多返回值可能是最佳的选择。你可以精确的返回希望的值,而无须 重载域空间到特定的错误信号上。 命名返回参数 27 命名返回参数 Go 函数的返回值或者结果参数可以指定一个名字,并且像原始的变量那样使用,就像输入 参数那样。如果对其命名,在函数开始时,它们会用其类型的零值初始化;如果函数在不加 参数的情况下执行了return 语句,结果参数的当前值会作为返回值返回。用这个特性,允许 (再一次的)用较少的代码做更多的事a。 名字不是强制的,但是它们可以使得代码更加健壮和清晰:这是文档。如果命 名nextInt 的int 返回值哪个代表哪个。 func nextInt(b []byte, pos int) (value, nextPos int){/* ...*/ } 由于命名结果会被初始化并关联于无修饰的return,它们可以非常简单并且清晰。这里有一 个io.ReadFull 的版本,很好的运用了它: func ReadFull(r Reader, buf[] byte) (n int , erros.Error) { for len (buf) > 0 &&err ==nil { var nr int nr, err = r.Read(buf) n +=nr buf = buf[nr:len(buf)] } return } 在下面的例子中,定义了一个简单的函数,用于计算 值x 的阶乘。 这一节的部分内容来 自[15]。 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 { val = 1 pos = 0 } else if n ==1 { val = 1 a这是Go 的格言:”用更少的代码做更多的事”。 28 Chapter 3: 函数 pos = 1 } else { v1, _ :=fib(n-1) v2, _ :=fib(n-2) val = v1+ v2 pos = n } return } 延迟的代码 假设有一个函数,打开文件并且对其进行若干读写。在这样的函数中,经常有提前返回的地 方。如果你这样做,就需要关闭正在工作的文件描述符。这经常导致产生下面的代码: Listing 3.6. 没有defer func ReadWrite() bool { 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() 是函数 // Doyourthing if failureX { return false ← Close() 现在自动调用 } if failureY { return false ← 这里也是 } return true } 变参 29 可以将多个函数放入”延迟列表”中,这个例子来自[3]: for i :=0 ; i < 5; i++ { defer fmt.Printf("%d ", i) } 延迟的函数是按照后进先出(LIFO)的顺序执行,所以上面的代码打印:4 3 2 1 0。 利用defer 甚至可以修改返回值,假设正在使用命名结果参数和函数符号b,例如: Listing 3.8. 函数符号 defer func (){ /* ... */ }() ← () 在这里是必须的 或者这个例子,更加容易了解为什么,以及在哪里需要括号: 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! } 变参 接受变参的函数是有着不定数量的参数的。为了做到这点,首先需要定义函数使其接受变 参: 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]...) ← 传递片段 } b函数符号也就是被叫做闭包的东西。 30 Chapter 3: 函数 函数作为值 就像其他在Go 中的几乎所有东西,函数也同样是值而已。它们可以像下面这样赋值给变 量: Listing 3.11. Anonymous function 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 函数。这是一个 留给读者的练习,参考在第32 页的练习Q12。 回调和闭包 当函数作为值时,就可以很容易的传递到其他函数里,然后可以作为回调。首先定义一个函 数,对整数做一些“事情”: 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 } 我们已经在第”延迟的代码” 节看到了闭包的部分应用,但是还有一些需要讨论。当定义一个 闭包时,例如,在用函数符号开始定义时,仍然可以访问当前函数定义的(局部)变量。 // 定义一些局部变量 // 这个函数有意写得复杂了,但是用来举例还是不错的 frameSquare := func(x, y int){ // 闭包可以毫不费力的将局部变量传递到回调 if thickFrame { // 为一个矩形绘制一个3 x 3 像素的块 恐慌(Panic)和恢复(Recover) 31 for x0 := x - 1; x0 <=x+1 ; x0++ { for y0 := y - 1; y0 <=y+1 ; y0++ { rgba.Set(x0, y0,redColor) } } } else { rgba.Set(x, y, blueColor) } } 如果不希望使用闭包,并且定义一个完整的函数,需要传递所有的变量到这个函数。 TODO 要点都在这里,但是 需要更多的解说和代 码恐慌(Panic)和恢复(Recover) c Go 没有例如像Java 那样的异常机制:不能抛出一个异常。作为代替,它使用了恐慌和恢 复(panic-and-recover)机制。一定要记得,这应当作为最后的手段被使用,你的代码中 应当没有,或者很少的令人恐慌的东西。这是个强大的工具,明智的使用它。那么,应该如 何使用它。 下面的描述来自于[2]: Panic 是一个内建函数,可以中断原有的控制流程,进入一个令人恐慌的流程中。当函数F 调用panic,函数F 的执行被中断,并且F 中的延迟函数会正常执行,然后F 返回到调 用它的地方。在调用的地方,F 的行为就像调用了panic。这一过程继续向上,直到程 序崩溃时的所有goroutine 返回。 恐慌可以直接调用panic 产生。也可以由运行时错误产生,例如访问越界的数组。 Recover 是一个内建的函数,可以让进入令人恐慌的流程中的goroutine 恢复过来。Recover 仅在延迟函数中有效。 在正常的执行过程中,调用recover 会返回nil 并且没有其他任何效果。如果当前 的goroutine 陷入恐慌,调用recover 可以捕获到panic 的输入值,并且恢复正常的执 行。 练习 Q6. (4) 平均值 1. 编写一个函数用于计算一个float64 类型的slice 的平均值。 Q7. (3) 整数顺序 1. 编写函数,返回其(两个)参数正确的(自然)数字顺序: f(7,2) → 2,7 f(2,7) → 2,7 Q8. (4) 作用域 1. 下面的程序有什么错误? c对应异常机制,Go 的这种错误机制或许可以叫做恐慌机制:当你遇到它时应该感到恐慌,然后应该恢复 (recover)它。 32 Chapter 3: 函数 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)的。 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 值,并给出这个值得到的斐波那契数列。 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]: 它在一个列表上重复步骤来排序,比较每个相邻的元素,并且顺序错误 的时候,交换它们。一遍一遍扫描列表,直到没有交换为止,这意味 练习 33 着列表排序完成。算法得名于更小的元素就像“泡泡”一样冒到列表的顶 端。 [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 end procedure Q15. (6) 函数返回一个函数 1. 编写一个函数返回另一个函数,返回的函数的作用是对一个整数+2。函数的名称叫 做plusTwo。然后可以像下面这样使用: p :=plusTwo() fmt.Printf("%v\n", p(2)) 应该打印4。参阅第30 页的第回调和闭包 节了解更多关于这个的内容。 2. 使1 中的函数更加通用化,创建一个plusX(x) 函数,返回一个函数用于对整数加 上x。 答案 35 答案 A6. (4) 平均值 1. 下面的函数计算平均值。 Listing 3.13. Go 中的平均值函数 func average(xs []float64) (ave float64){..0 sum := 0.0 switch len (xs) { case 0: ..1 ave= 0 default:..2 for _, v := range xs { sum +=v } ave= sum / float64(len(xs)) ..3 } return ..4 } ..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() 应修改为: func main() { var i int for i = 0; i < 10; i++ { fmt.Printf("%v\n", i) } 36 Chapter 3: 函数 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 函数的第一个版本大约 是这样: 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] 为了解决这个,需要向函数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] 答案 37 } 像下面这样使用 func main() { var s stack s.push(25) s.push(14) 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) 变参 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. 下面的程序会计算出斐波那契数列。 38 Chapter 3: 函数 Listing 3.16. Fibonacci function in 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; ..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 } fmt.Printf("%v", (Map(f, m))) } 答案 39 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 值被返回。 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)。快速排序 (ref ???)是更好的排序算法。 但是冒泡排序容易实现,下面是一个例子。 Listing 3.18. Bubble sort func main() { n := []int{5, -1, 0, 12,3, 5 } fmt.Printf("unsorted %v\n", n) // 虽然slice 是存储的值,但是它是个引用类型,因为底层的array 已经改变了! bubblesort(n) fmt.Printf("sorted %v\n", n) } 40 Chapter 3: 函数 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] } } } } A15. (6) 函数返回一个函数 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 包是函数和数据的集合,跟Perl 的包[12]差不多。用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 } 名称以大写字幕起始的是可导出的,可以在包的外部调用,稍候会讨论这个。现在可以在下 面的程序myeven.go 中使用这个包了: Listing 4.2. Use of the even package package main import (..0 "./even" ..1 "fmt" ..2 ) func main() { i :=5 fmt.Printf("Is %d even? %v\n", i, even.Even(i)) ..3 } ..0 导入下面的包; ..1 本地包even 在这里导入; ..2 官方fmt 包导入; ..3 调用even 包中的函数。访问一个包中的函数的语法是.Function()。 现在只需要编译和链接,首先是那个包,然后是myeven.go,并且链接它们: 构建一个包 43 % 6geven.go ← 包 % 6gmyeven.go ← 程序 % 6l-omyevenmyeven.6 ← 链接步骤 然后测试一下: % ./myeven Is5 even?false 在Go 中,当函数的首字母大写的时候,函数会被从包中导出(在包外部可见,或者说公 有的),因此函数名是Even。如果修改myeven.go 的第7 行,使用未导出的函数even.odd: fmt.Printf("Is%deven?%v\n",i,even.odd(i)) 由于使用了私有的函数,会得到一个编译错误: myeven.go:7:cannotrefertounexportednameeven.odd myeven.go:7:undefined:even.odd 概括来说: • 公有函数的名字以大写字母开头; • 私有函数的名字以小写字幕开头。 这个规则同样适用于定义在包中的其他名字(新类型、全局变量)。注意,“大写”的含义并 不仅限于US ASCII,它被扩展到了整个Unicode 范围。所以大写的希腊语、古埃及语都是可 以的。 构建一个包 为了创建一个别人可以用的包(只需要import "even" 即可使用),首先需要创建一个目录 放入包文件。 % mkdireven % cpeven.goeven/ 然后可以使用根据even 包改写的Makefile 文件。 Listing 4.3. 用于包的Makefile 1include $(GOROOT)/src/Make.inc 3TARG=even 4GOFILES=\ 5even.go\ 7include $(GOROOT)/src/Make.pkg 注意第3 行定义了even 包,而第4 和第5 行录入了构成包的文件。同样注意,这不是在第2 章,第”使用 Makefile” 节中使用的那个 Makefile。最后一行的include 语句是不同的。 如果现在执行gomake,一个叫做”_go_.6” 的文件、一个叫做”_obj/” 的目录,和”_obj/” 目录中,叫做”even.a” 的文件被创建。文件even.a 是有编译后的Go 代码的静态库。通 过gomakeinstall 包(好吧,只有even.a)被安装在官方包目录中: 44 Chapter 4: 包 % makeinstall cp_obj/even.a$GOROOT/pkg/linux_amd64/even.a 安装完后,就可以修改import 从"./even" 到"even"。 但是,如果不希望将包安装到官方的Go 目录,或者没有权限这么做的话。在使用6/8g 的时候,可以指定I 标志替换包目录。有了这个标志,你可以让你的导入语句变成(import "even")却继续部署在自定义的目录中。所以,下面的命令可以联合包构建(并且链 接)myeven程序。 % 6g-Ieven/_objmyeven.go ← Building % 6l-Leven/_objmyeven.6 ← Linking 导入但是没有使用的包会引起一个错误。 标识符 像在其他语言中一样,Go 的命名是很重要的。在某些情况下,它们甚至有语义上的作用: 例如,在包外是否可见决定于首字母是不是大写。因此有必要花点时间讨论一下Go 程序的 命名规则。 使用的规则是让众所周知的缩写保持原样,而不是去尝试到底哪里应该大写。Atoi ,Getwd,Chmod。 驼峰式对那些有完整单词的会很好:,,ReadFileNewWriterMakeSlice。 包名 当包导入(通过import)时,包名成为了内容的入口。在 import "bytes" 之后,导入包的可以调用函数bytes.Buffer。任何使用这个包的人,可以使用同样的名字访 问到它的内容,因此这样的包名是好的:短的、简洁的、好记的。根据规则,包名是小写的 一个单词;不应当有下划线或混合大小写。由于每个人都可能需要录入这个名字,所以尽可 能的简短。不要提前考虑冲突。包名是导入的默认名称。就上面的bytes.Buffer 来说。通过 import bar "bytes" 它变成了bar.Buffer。因此,它无须在整个代码中唯一,在少有的冲突中,可以给导入的包 选择另一个名字在局部使用。在任何时候,冲突都是很少见的,因为导入的文件名会用来做 判断,到底是哪个包使用了。 另一个规则是包名就是代码的根目录名;在src/pkg/container/vector 的包,作为container/vector 导入,但名字是vector,不是container_vector 也不是containerVector。 导入包将使用其名字引用到内容上,所以导入的包可以利用这个避免罗嗦。例 如,缓冲类型bufio 包的读取方法,叫做Reader ,而不是BufReader ,因为用户看到的 是bufio.Reader 这个清晰、简洁的名字。更进一步说,由于导入的实例总是它们包名指 向的地址,bufio.Reader 不会与io.Reader 冲突。相似的,ring.Ring 创建新实例的函数— —在Go 中定义的构造函数——通常叫做NewRing,但是由于Ring 是这个包唯一的一个导出的 类型,同时,这个包也叫做ring,所以它可以只称作New。包的客户看到的是ring.New。用 包的结构帮助你选择更好的名字。 另外一个简短的例子是once.Do;once.Do(setup) 读起来很不错,并且命名为once. DoOrWaitUntilDone(setup) 不会有任何帮助。长的名字不会让其变得容易阅读。如果名字表 达了一些复杂并且微妙的内容,更好的办法是编写一些有帮助的注释,而不是将所有信息都 放入名字里。 包的文档 45 最后,在Go 中使用混合大小写 MixedCaps 或者mixedCaps,而不是下划线区分含有多 个单词的名字。 包的文档 这段复制于[3]。 每个包都应该有包注释,在package 前的一个注释块。对于多文件包,包注释只需要出现 在一个文件前,任意一个文件都可以。包注释应当对包进行介绍,并提供相关于包的整 体信息。这会出现在godoc 生成的关于包的页面上,并且相关的细节会跟随气候。来自官 方regexp 包的例子: /* Theregexppackageimplementsasimplelibraryfor regularexpressions. Thesyntaxoftheregularexpressionsacceptedis: regexp: concatenation'|'concatenation */ packageregexp 每个定义(并且导出)的函数应当有一小段文字描述该函数的行为,来自于fmt 包: //Printfformatsaccordingtoa formatspecifierandwritestostandardoutput. funcPrintf(formatstring,a...interface)(nint,errnoos.Error){ 测试包 在Go 中为包编写单元测试应当是一种习惯。编写测试需要包含testing 包和程序gotest。两 者都有良好的文档。当对某个包进行测试时,务必要记得必须使用Makefile 进行编译(参阅 第”构建一个包” 节)。 测试本身是由gotest 完成的。gotest 程序调用了所有的测试函数。even 包没有定义任 何测试函数,执行gomaketest ,这样: % gomaketest notestfilesfound(*_test.go) make:***[test]Error2 在测试文件中定义一个测试来修复这个。测试文件也在包目录中,被命名为*_test.go。 这些测试文件同Go 程序中的其他文件一样,但是gotest 只会执行测试函数。每个测试函数 都有相同的标识,它的名字以Test 开头: func TestXxx(t *testing.T) ← TestrestOftheNe 编写测试时,需要告诉gotest 测试是失败还是成功。测试成功则直接返回。当测试失败 可以用下面的函数标记[6]。这是非常重要的(参阅godoctesting 了解更多): func (t *T) Fail() Fail 标记测试函数失败,但仍然继续执行。 func (t *T) FailNow() 46 Chapter 4: 包 FailNow 标记测试函数失败,并且中断其执行。这将会执行下一个测试。因此,当前文件的 其他所有测试都被跳过。 func (t *T) Log(args ... interface{}) Log 用默认格式对其参数进行格式化,与Print() 类似,并且记录文本到错误日志。 func (t *T) Fatal(args ...interface{}) Fatal 等价于Log() 后跟随FailNow()。 将这些凑到一起,就可以编写测试了。首先,选择名字even_test.go。然后添加下面的 内容: Listing 4.4. even 包的测试 1package even 3import "testing" 5func TestEven(t *testing.T) { 6if true != Even(2) { 7t.Log("2 should beeven !") 8t.Fail() 9} 10} 注意在第一行使用了package even,测试使用与被测试的包使用相同的名字空间。这不 仅仅是为了方便,也允许了测试未导出的函数和结构。然后导入testing 包,并且在第5 行定 义了这个文件中唯一的测试函数。展示的Go 代码应当没有任何惊异的地方:检查了Even 函 数是否工作正常。现在等待了好久的时刻到了,执行测试: % gomaketest 6g-o_gotest_.6even.goeven_test.go rm-f_test/even.a gopackgrc_test/even.a_gotest_.6 PASS 测试执行并且报告PASS。成功了!为了展示失败的测试,修改测试函数: // Enteringthetwilightzone func TestEven(t *testing.T) { if ! Even(2) { t.Log("2 should beodd !") t.Fail() } } 然后得到: ---FAIL:even.TestEven 2 shouldbeodd! FAIL make:***[test]Error1 然后你可以以此行事(修复测试的实例) 常用的包 47 在编写包的时候应当一边写代码,一边写(一些)文档和测试函数。这可以让你 的程序更好,并且它展示了你的努力。 常用的包 标准的Go 代码库中包含了大量的包,并且在安装Go 的时候多数会伴随一起安装。浏 览$GOROOT/src/pkg 目录并且查看那些包会非常有启发。无法对每个包就加以解说,不过下 面的这些值得讨论:a fmt 包fmt 实现了格式化的I/O 函数,这与C 的printf 和scanf 类似。格式化短语派生于C 。一些短语(%-序列)这样使用: %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 包提供了将字符串转换成基本数据类型,或者从基本数据类型转换为字符串 的功能。 os os 包提供了与平台无关的操作系统功能接口。其设计是Unix 形式的。 flag flag 包实现了命令行解析。参阅”Command line arguments” 在第85 页。 json json 包实现了编码与解码RFC 4627 定义的JSON 对象。 template 数据驱动的模板,用于生成文本输出,例如HTML。 将模板关联到某个数据结构上进行解析。模板内容指向数据结构的元素(通常结构的 字段或者map 的键)控制解析并且决定某个值会被显示。模板扫描结构以便解析,而 “游标”@ 决定了当前位置在结构中的值。 a描述来自包的godoc。额外的解释用斜体。 48 Chapter 4: 包 http http 实现了HTTP 请求、响应和URL 的解析,并且提供了可扩展的HTTP 服务和基本 的HTTP 客户端。 unsafe unsafe 包包含了Go 程序中数据类型上所有不安全的操作。通常无须使用这个。 reflect reflect 包实现了运行时反射,允许程序通过抽象类型操作对象。通常用于处理静 态类型interface{} 的值,并且通过Typeof 解析出其动态类型信息,通常会返回 一个有接口类型Type 的对象。包含一个指向类型的指针,*StructType、*IntType 等等,描述了底层类型的详细信息。可以用于类型转换或者类型赋值。参阅6, 第”Introspection” 节。 exec exec 包执行外部命令。 练习 TODO 编 写 一 个 测 试stack 包的练习。 Q16. (2) stack 包 1. 参考Q9 练习。在这个练习中将从那个代码中建立一个独立的包。 为stack 的实现创建一个合适的包,Push、Pop 和Stack 类型需要被导出。 2. 先进先出栈可以使用哪个官方包实现。 Q17. (7) 计算器 1. 使用stack 包创建逆波兰计算器。 2. 扩展一下,用你在问题2 中发现的包重写计算器。 答案 49 答案 A16. (2) stack 包 1. 在创建stack 包时,仅有一些小细节需要修改。首先,导出的函数应当大写首字母, 因此应该是Stack。所以完整的包(包含String)变成为 Listing 4.5. 包里的Stack package stack import ( "strconv" ) type Stack struct { i int data [10] int } func (s *Stack) Push(k int ){ s.data[s.i] = k s.i++ } func (s *Stack) Pop() (ret int){ s.i-- ret = s.data[s.i] } 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 } A17. (7) 计算器 1. 这是第一个答案 Listing 4.6. 逆波兰计算器 package main import ( "bufio" "os" "strconv" "fmt" ) 50 Chapter 4: 包 var reader *bufio.Reader = bufio.NewReader(os.Stdin) var st= new (Stack) 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 0 } ret= s.data[s.i] return ret } 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 } func main() { for{ s, err :=reader.ReadString('\n') var token string if err != nil { return } 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()*st.pop()) case c == '-': p :=st.pop() q :=st.pop() fmt.Printf("%d\n", q-p) case c == 'q': return default: //error } } } } 2. container/vector 包应当是不错的选择。它同样预定义了Push 和Pop 函数。对于我们 的程序来说修改是非常小的,下面的差异文件显示了不同的地方: 答案 51 ---calc.go 2010-05-1610:19:13.886855818+0200 +++calcvec.go2010-05-1610:13:35.000000000+0200 @@-5,11+5,11@@ "os" "strconv" "fmt" -"./stack" + "container/vector" ) varreader*bufio.Reader=bufio.NewReader(os.Stdin) -varst= new(Stack) +varst= new(vector.IntVector) funcmain(){ for{ 只有两行需要修改。太棒了。 5 进阶 Go 有指针,但是没有指针运算。你不能用指针 变量遍历字符串的各个字节。 Go For C++ Programmers GOAUTHORS Go 有指针。然而却没有指针运算,因此它们更象是引用而不是你所知道的来自于C 的指 针。指针非常有用。在Go 中调用函数的时候,得记得变量是值传递的。因此,为了修改一 个传递入函数的值的效率和可能性,有了指针。 跟C 中一样,用类型前的’*’ 定义一个指针:var p *int。现在p 是一个指向整数值的指 针。所有新定义的变量都被赋值为其类型的零值,而指针也一样。一个新定义的或者没 有任何指向的指针,有值nil。在其他语言中,这经常被叫做空(NULL)指针,在Go 中就 是nil。让指针指向某些内容,可以使用取址操作符 (&),像第5 行那样: Listing 5.1. Use of a pointer var p *int fmt.Printf("%v", p) ← 打印nil var i int ← 定义一个整形变量i p = &i ← 使得p 指向i fmt.Printf("%v", p) ← 打印出来的内容类似0x7ff96b81c000a 更简单来说:*X 是指向X 的指针;[3]X 是有三个X 的数组。因此类型更加容易从类型变 化的名称上来理解:[] 定义了slice;’*’ 定义了指针;[size] 定义了数组。因此[]*[3]*X 是 一个slice,元素是指向有三个元素的数组的指针,数组元素是指向X 的指针。(参阅5.1)。 Figure 5.1. 指针和类型,值v 全部为X 类型 [v0] *X v [3]X []X []*[3]*X . . . [v1] [v2] [v0] [v2] . . . [n0] [n2] [k0] [k1] [k2] v0 v1 v2 从指针获取值是通过在指针变量前置’*’实现的: Listing 5.2. 获取指针指向的值 p = &i ← 获取i 的地址 *p = 8 ← 修改i 的值 fmt.Printf("%v\n", *p) ← 打印8 内存分配 53 fmt.Printf("%v\n", i) ← 同上 前面已经说了,没有指针运算,所以如果这样写:*p++,它表示(*p)++:首先获取指针 指向的值,然后对这个值加一。 内存分配 Go 有垃圾收集,意味着无须担心内存分配和回收。当然,自从1980 以来几乎所有语言都有 这个,但是在类C 语言中看到垃圾收集感觉还是很好。 Go 有两个内存分配原语,new 和make。它们应用于不同的类型,做不同的工作,可能有 些迷惑人,但是规则很简单。下面的章节展示了在Go 中如何处理内存分配,并且希望能够 让new 和make 之间的区别更加清晰。 用new 分配内存 内建函数new 本质上说跟其他语言中的同名函数功能一样:new(T) 分配了零值填充的T 类型 的内存空间,并且返回其地址,一个*T 类型的值。用Go 的术语说,它返回了一个指针,指 向新分配的类型T 的零值。有一点非常重要: new 返回指针。 这意味着使用者可以用new 创建一个数据结构的实例并且可以直接工作。如bytes.Buffer 的文档所述“Buffer 的零值是一个准备好了的空缓冲。”类似的,sync.Mutex 也没有明确的构 造函数或Init 方法。取而代之,sync.Mutex 的零值被定义为非锁定的互斥量。 零值是非常有用的。例如这样的类型定义,55 页的”定义自己的类型” 内容。 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)的指针,长度和容量的三项描述符;在这些项目被初始化之 前,slice 为nil。对于slice,map 和channel,make 初始化了内部的数据结构,填充适当的 值。 make 返回初始化后的(非零)值。 例如,make([]int, 10,100) 分配了100 个整数的数组,然后用长度10 和容量100 创建 了slice 结构指向数组的前10 个元素。区别是,new([]int) 返回指向新分配的内存的指针, 而零值填充的slice 结构是指向nil 的slice 值。 这个例子展示了new 和make 的不同。 54 Chapter 5: 进阶 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 获得特定 的指针。 构造函数与复合声明 有时零值不能满足需求,必须要有一个用于初始化的构造函数,例如这个来自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 } 有许多冗长的内容。可以使用复合声明使其更加简洁,每次只用一个表达式创建一个新的实 例。 func NewFile(fd int , name string) *File { if fd < 0 { return nil } f :=File {fd,name, nil, 0 } ← Createanew File return &f ← Returntheaddressoff } 返回本地变量的地址没有问题;在函数返回后,相关的存储区域仍然存在。 事实上,从复合声明获取分配的实例的地址更好,因此可以最终将两行缩短到一行。a return &File{fd,name, nil, 0 } 联合声明中所有的字段都必须按顺序全部写上。然而,通过对元素用字段:值成对的标识, 初始化内容可以按任意顺序出现,并且可以省略初始化为零值的字段。因此可以这样 return &File{fd: fd,name: name } 在特定的情况下,如果复合声明不包含任何字段,它创建特定类型的零值。表达式new(File ) 和&File{} 是等价的。 a从复合声明中获取地址,意味着告诉编译器在堆中分配空间,而不是栈中。 定义自己的类型 55 复合声明同样可以用于创建array,slice 和map,通过指定适当的索引和map 键来标识 字段。在这个例子中,无论是Enone,Eio 还是Einval 初始化都能很好的工作,只要确保它 们不同就好了。 ar := [...] string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"} sl := []string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"} ma := map[int ]string{Enone: "no error", Eio: "Eio", Einval: "invalid argument"} 定义自己的类型 自然,Go 允许定义新的类型,通过保留字type 实现: type foo int 创建了一个新的类型foo 作用跟int 一样。创建更加复杂的类型需要用到struct 保留字。这 有个在一个数据结构中记录某人的姓名(string)和年龄(int),并且使其成为一个新的 类型的例子: Listing 5.3. Structures package main import "fmt" type NameAge struct { name string ← 不导出 age int ← 不导出,如果是Age 就可以导出 } func main() { a := new(NameAge) a.name = "Pete" a.age = 42 fmt.Printf("%v\n", a) } 通常,fmt.Printf("%v\n",a) 的输出是 &{Pete42} 这很棒!Go 知道如何打印结构。如果仅想打印某一个,或者某几个结构中的字段,需要使 用. 。例如,仅仅打印名字: fmt.Printf("%s", a.name) ← %s 格式化字符串 结构字段 每个在结构中的项目被称为字段。没有字段的结构: 56 Chapter 5: 进阶 struct {} 有五个字段的: struct { x, y int _ float64 ← 填充 A *[]int F func() } 如果省略字段的名字,可以创建匿名字段,例如: struct { T1 // 字段名字是T1 *T2 // 字段名字是T2 P.T3 // 字段名字是T3 x, y int // 字段名字是x 和y } 注意首字母大写的字段可以被导出,也就是说,在其他包中可以进行读写。字段名以小写字 幕开头是当前包的私有的。包的函数定义是类似的,参阅第4 章。 方法 可以对新定义的类型创先函数以便操作,可以通过两种途径: 1. 创建一个函数接受这个类型的参数。 func doSomething(in1 *NameAge, in2 int ){/* ... */ } (你可能已经猜到了)这是函数调用。 2. 创建一个工作在这个类型上的函数(参阅在3.1 中定义的接收方): func (in1 *NameAge) doSomething(in2 int){/* ...*/ } 这是方法调用,可以类似这样使用: var n *NameAge n.doSomething(2) 但是下面的内容一定要留意,引用自[5]: 如果x 可获取地址,并且&x 的方法中包含了m,x.m() 是(&x).m() 更短的写法。 根据上面所述,这意味着下面的情况不是错误: var n NameAge ← 不是指针 n.doSomething(2) 这里Go 会查找NameAge 类型的变量n 的方法列表,没有找到就会再查找*NameAge 类型的方法 列表,并且将其转化为(&n).doSomething(2)。 转换 57 转换 有时需要将一个类型转换为另一个类型。在Go 中可以做到,不过有一些规则。首先,将一 个值转换为另一个是由函数完成的,并且不是所有的转换都是允许的。 Table 5.1. 合法的转换,float64 同float32 类似 From xb[]bytexi[]int s stringffloat32iint To []byte × []byte(s) []int × []int(s) stringstring(xb)string(xi) × float32 × float32(i) int int(f) × • 从string 到字节或者整形的slice。 mystring := "hello this isstring" byteslice := []byte(mystring) 转换到byte slice,每个byte 保存字符串对应字节的整数值。注意Go 的字符串是UTF- 8 编码的,一些字符可能是1、2、3 或者4 个字节结尾。 intslice :=[] int(mystring) 转换到int slice,每个int 保存Unicode 编码的指针。字符串中的每个字符对应一个 整数。 • 从字节或者整形的slice 到string。 b := []byte{'h','e','l','l','o'} ← 复合声明 s := string(b) i := []int{ 257,1024,65} r := string(i) 对于数值,定义了下面的转换: • 将整数转换到指定的(bit)长度:uint8(int); • 从浮点数到整数:int(float32)。这会截断浮点数的小数部分; • 其他的类似:float32(int)。 用户定义类型的转换 如何在自定义类型之间进行转换?这里创建了两个类型Foo 和Bar,而Bar 是Foo 的一个别 名: type foo struct { int} ← 匿名字段 type bar foo ← bar 是foo 的别名 58 Chapter 5: 进阶 然后: 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. (6) 使用interface 的map 函数 1. 使用练习Q12 的答案,利用interface 使其更加通用。 Q19. (6) 指针 1. 假设定义了下面的结构: type Person struct { name string age int } 下面两行之间的区别是什么? var p1Person p2 := new(Person) 2. 下面两个内存分配的区别是什么? func Set(t *T) { x = t } 和 func Set(t T) { x= &t } Q20. (6) 链表 1. 使用container/list 包创建(双向)链表。将值1,2 和4 存入并打印。 2. 自行实现链表。然后做与问题1 相同的实现。 Q21. (6) Cat 1. 编写一个程序,模仿Unix 的cat 程序。对于不知道这个程序的人来说,下面的调用 显示了文件blah 的内容: % catblah 2. 使其支持n 开关,用于输出每行的行号。 Q22. (8) 方法调用 练习 59 1. 假设有下面的程序: 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 到向量的末尾。 那么接受者应当是*IntVector 类型,为什么上面的代码可以工作? 答案 61 答案 A18. (6) 使用interface 的map 函数 Listing 5.4. Go 中更加通用的map 函数 1. package main import "fmt" /* 定义一个空的 interface 类型 */ type e interface{} func mult2(f e) e { switch f.(type ){ 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) } A19. (6) 指针 1. 第一行:var p1Person 分配了Person-值 给p1。p1 的类型是Person。 第二行:p2:= new(Person) 分配了内存并且将指针赋值给p2。p2 的类型是*Person。 2. 在第二个函数中,x 指向一个新的(堆上分配的)变量t,其包含了实际参数值的副 本。 在第一个函数中,x 指向了t 指向的内容,也就是实际上的参数指向的内容。 因此在第二个函数,我们有了“额外”的变量存储了相关值的副本。 62 Chapter 5: 进阶 A20. (6) 链表 1. 2. A21. (6) Cat 1. 下面是cat 的实现,同样支持n 输出每行的行号。 Listing 5.5. cat 程序 package main ..0 import ( "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 ==os.EOF {..4 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)) } 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 答案 63 %s: %s\n", os.Args[0], flag.Arg(i), e.String()) continue } cat(bufio.NewReader(f)) } } ..0 包含所有需要用到的包; ..1 定义新的开关”n”,默认是关闭的。注意很容易写的帮助文本; ..2 实际上读取并且显示文件内容的函数; ..3 每次读一行; ..4 如果到达文件结尾; ..5 如果设定了行号,打印行号然后是内容本身; ..6 否则,仅仅打印该行内容。 A22. (8) 方法调用 1. k1 的类型是vector.IntVector。为什么?这里使用了符号{},因此获得了类型的 值。变量k2 是*vector.IntVector,因为获得了复合语句的地址(&)。而最后的k3 同样是*vector.IntVector 类型,因为new 返回该类型的指针。 2. 在[5] 的“调用”章节,有这样的描述: 当x 的方法集合包含m,并且参数列表可以赋值给m 的参数,方法调 用x.m() 是合法的。如果x 可以被地址化,而&x 的方法集合包含m,x.m() 可以作为(&x).m() 的省略写法。 换句话说,由于k1 可以被地址化,而*vector.IntVector 具有 Push 方法,调用k1. Push(2) 被Go 转换为(&k1).Push(2) 来另类型系统愉悦(也另你愉悦——现在你了 解到这一点)。b b参阅本章的第”方法” 节。 6 接口 I have this phobia about having my body penetrated surgically. You know what I mean? eXistenZ TEDPIKUL In Go, the word interface is overloaded to mean several different things. Every type hasThe following text is from [30]. Written by Ian Lance Taylor — one of the authors of Go. an interface, which is the set of methods defined for that type. This bit of code defines a struct type S with one field, and defines two methods for S. Listing 6.1. Defining a struct and methods on it type S struct { i int } func (p *S) Get() int { return p.i } func (p *S) Put(v int){ p.i = v } You can also define an interface type, which is simply a set of methods. This defines an interface I with two methods: type I interface { Get() int Put(int) } An interface type is a set of methods. S is a valid implementation for interface I, because it defines the two methods which I re- quires. Note that this is true even though there is no explicit declaration that S implements I. A Go program can use this fact via yet another meaning of interface, which is an inter- face value: func f(p I) {..0 fmt.Println(p.Get()) ..1 p.Put(1) ..2 } ..0 Declare a function that takes an interface type as the argument; ..1 As p implements interface I is must have the Get() method; ..2 Same holds for the Put() method. Here the variable p holds a value of interface type. Because S implements I, we can call f passing in a pointer to a value of type S: var s S; f(&s) 接口 65 The reason we need to take the address of s, rather than a value of type S, is because we defined the methods on s to operate on pointers, see the code above in listing 6.1. This is not a requirement — we could have defined the methods to take values — but then the Put method would not work as expected. The fact that you do not need to declare whether or not a type implements an inter- face means that Go implements a form of duck typing[34]. This is not pure duck typing, because when possible the Go compiler will statically check whether the type implements the interface. However, Go does have a purely dynamic aspect, in that you can convert from one interface type to another. In the general case, that conversion is checked at run- time. If the conversion is invalid — if the type of the value stored in the existing interface value does not satisfy the interface to which it is being converted — the program will fail with a runtime error. Interfaces in Go are similar to ideas in several other programming languages: pure ab- stract virtual base classes in C++, typeclasses in Haskell or duck typing in Python. However there is no other language which combines interface values, static type checking, dynamic runtime conversion, and no requirement for explicitly declaring that a type satisfies an interface. The result in Go is powerful, flexible, efficient, and easy to write. Which is what? Lets define another type that also implements the interface I: Listing 6.2. Another type that implements I type R struct { i int } func (p *R) Get() int { return p.i } func (p *R) Put(v int){ p.i = v } The function f can now accept variables of type R and S. Suppose you need to know the actual type in the function f. In Go you can figure that out by using a 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 The type switch. Use (type) in a switch statement. We store the type in the variable t; ..1 The actual type of p is a pointer to S; ..2 The actual type of p is a pointer to R; ..3 The actual type of p is a S; ..4 The actual type of p is a R; 66 Chapter 6: 接口 ..5 It’s another type that implements I. Note that a type switch is the only way to figure out what type of an interface variable is. Using (type) outside a switch is illegal. Empty interface Since every type satisfies the empty interface: interface{}. We can create a generic func- tion which has an empty interface as its argument: Listing 6.3. A function with a empty interface argument func g(any interface{}) int{ return any.(I).Get() } The return any.(I).Get() is the tricky bit in this function. The value any has type inter- face{}, meaning no guarantee of any methods at all: it could contain any type. The .(I) is a type assertion which converts any to an interface of type I. If we have that type we can invoke the Get() function. So if we create a new variable of the type *S, we can just call g(), because *S also implements the empty interface. s = new(S) fmt.Println(g(s)); The call to g will work fine and will print 0. If we however invoke g() with a value that does not implement I we have a problem: Listing 6.4. Failing to implement an interface i := 5 ← Makeia "lousy"int fmt.Println(g(i)) This compiles OK, but when we run this we get slammed with: panic:interfaceconversion:intisnotmain.I:missingmethodGet Which is completely true, the built-in type int does not have a Get() method. Checking for interfaces In your code you want to prevent these kind of errors, therefor Go provides you with a way to check if a variable implements a certain interface, again this uses a type assertion, but now in an if statement. if ok:=any.(I) ; ok { /*any implements theinterface I */ } Methods Methods are functions that have an receiver (see chapter 3). You can define methods on any type (except on non-local types, this includes built-in types: the type int can not have methods). You can however make a new integer type with its own methods. For example: type Foo int Interface names 67 func (self Foo) Emit() { fmt.Printf("%v", self) } type Emitter interface { Emit() } Doing this on non-local (types defined in other packages) types yields: Listing 6.5. Failure extending built-in types func (i int) Emit() { fmt.Printf("%d", i) } cannotdefinenewmethods onnon-localtypeint Listing 6.6. Failure extending non-local types func (a *net.AddrError) Emit() { fmt.Printf("%v", a) } cannotdefinenewmethods onnon-localtypenet.AddrError Methods on interface types An interfaces defines a set of methods. A method contains the actual code. In other words, an interface is the definition and the methods are the implementation. So a receiver can not be a defined for interface types, doing so results in a invalidreceivertype... compiler error. The authoritative word from the language spec [5]: The receiver type must be of the form T or *T where T is a type name. T is called the receiver base type or just base type. The base type must not be a pointer or interface type and must be declared in the same package as the method. Pointers to interfaces Creating a pointer to an interface value is a useless action in Go. It is in fact illegal to TODO Interfaces is not a pointer, not reference type Go release.2010-10- 13. create a pointer to an interface value. The release notes for that release that made them illegal leave no room for doubt: The language change is that uses of pointers to interface values no longer auto- matically dereference the pointer. A pointer to an interface value is more often a beginner’s bug than correct code. From the [4]. If not for this restriction, this code: var buf bytes.Buffer io.Copy(buf, os.Stdin) Would copy standard input into a copy of buf, not into buf itself. This is almost never the desired behavior. Interface names By convention, one-method interfaces are named by the method name plus the -er suffix: Reader, Writer, Formatter etc. 68 Chapter 6: 接口 There are a number of such names and it’s productive to honor them and the function names they capture. Read, Write, Close, Flush, String and so on have canonical signatures and meanings. To avoid confusion, don’t give your method one of those names unless it has the same signature and meaning. Conversely, if your type implements a method with the same meaning as a method on a well-known type, give it the same name and signature; call your string-converter method String not ToString.Text copied from [3]. A sorting example Recall the Bubblesort exercise (Q14), where we sorted an array of integers: 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] } } } } A version that sorts strings is identical except for the signature of the function: func bubblesortString(n []string){/* ... */ } Using this apprach would lead to two functions, one for each type. By using interfaces we can make this more generic. Lets create a new function that will sort both strings and integers, something along the lines of this non-working example: func sort(i []interface{}){..0 switch i.(type){..1 case string :..2 //... case int : //... } ..3 } ..0 Our function will receive a slice of empty interfaces; ..1 Using a type switch we find out what the actual type is of the input; ..2 And then sort accordingly; ..3 Return the sorted slice. But when we call this function with sort([]int{1,4,5 }), it fails with: cannotusei(type []int)astype[]interfaceinfunctionargument This is because Go can not easily convert to a slice of interfaces. Just converting to an interface is easy, but to a slice is much more costly. To keep a long story short: Go does notThe full mailing list discussion on this subject can be found at [25]. A sorting example 69 (implicitly) convert slices for you. So what is the Go way of creating such a ”generic” function? Instead of doing the type inference our selves with a type switch, we let Go do it implicitly: The following steps are required: 1. Define an interface type (called Sorter here) with a number of methods needed for sorting. We will at least need a function to get the length of the slice, a function to compare two values and a swap function; type Sorter interface { Len() int ← len()asa method Less(i, j int ) bool ← p[j] < p[i]asa method Swap(i, j int ) ← p[i],p[j]=p[j],p[i]asa method } 2. Define new types for the slices we want to sort. Note that we declare slice types; type Xi []int type Xs []string 3. Implementation of the methods of the Sorter interface. For integers: 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] } And for strings: 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. Write a generic Sort function that works on the Sorter interface. 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) } } } } ..0 x is now of the Sorter type; ..1 Using the defined functions, we implement Bubblesort. We can now use you generic Sort function as follows: ints := Xi{44, 67,3, 17,89,10,73,9, 14, 8 } strings := Xs{"nut", "ape", "elephant", "zoo", "go"} 70 Chapter 6: 接口 Sort(ints) fmt.Printf("%v\n", ints) Sort(strings) fmt.Printf("%v\n", strings) Introspection In a program, you can discover the dynamic type of an interface variable by using a switch. Such a type assertion uses the syntax of a type assertion with the keyword type inside theType assertion. parentheses. If the switch declares a variable in the expression, the variable will have the corresponding type in each clause. Listing 6.7. Dynamically find out the type package main type PersonAge struct { ..0 name string age int } type PersonShoe struct { ..1 name string shoesize int } func main() { p1 := new(PersonAge) p2 := new(PersonShoe) WhichOne(p1) WhichOne(p2) } func WhichOne(x interface{}){..2 switch t :=x.( type){..3 case *PersonAge: ..4 println("Age person") case *PersonShoe: println("Shoe person") } } ..0 First we define two structures as a new type, PersonAge; ..1 And PersonShoe; ..2 This function must accept both types as valid input, so we use the empty Interface, which every type implements; ..3 The type switch: (type); Introspection 71 ..4 When allocated with new, it’s a pointer. So we check for *PersonAge. If WhichOne() was called with a non pointer value, we should check for PersonAge. The following is another example of performing a type switch, but this time checking for more (built-in) types: Listing 6.8. A more generic type switch switch t := interfaceValue.(type){ ← Thetypeswitch case bool : fmt.Printf("boolean %t\n", t) case int : fmt.Printf("integer %d\n", t) case *bool: fmt.Printf("pointer to boolean %t\n", *t) case *int: fmt.Printf("pointer to integer %d\n", *t) default: fmt.Printf("unexpected type %T", t) // %Tprintstype } Introspection and reflection In the following example we want to look at the ”tag” (here named ”namestr”) defined in the type definition of Person. To do this we need the reflect package (there is no other way in Go). Keep in mind that looking at a tag means going back the type definition. So we use the reflect package to figure out the type of the variable and then access the tag. Listing 6.9. Introspection using reflection . . . .0 . . .1 . . .2 . . .3 . . .4 type Person struct { name string "namestr" ← "namestr"isthetag age int } p1 := new(Person) ← newreturnsapointertoPerson ShowTag(p1) ← ShowTag()isnowcalledwiththispointer func ShowTag(i interface{}){ switch t :=reflect.NewValue(i).( type){ ← Typeasserton reflect value case *reflect.PtrValue: ← Hencethecasefor*reflect.PtrValue tag :=t.Elem().Type().(*reflect.StructType).Field(0).Tag ..0 We are dealing with a PtrValue and according to the documentationa: func(v*PtrValue)Elem()Value Elem returns the value that v points to. If v is a nil pointer, Elem returns a nil Value. agodocreflect 72 Chapter 6: 接口 So on t we use Elem() to get the value the pointer points to. ..1 On Value we use the function Type() which returns reflect.Type. We need to get the type because there is where the tag is defined; ..2 So now we have a reflect.Type: …which returns an object with interface type Type. That contains a pointer to a struct of type *StructType,*IntType, etc. representing the details of the underlying type. A type switch or type assertion can reveal which. So we can access your specific type as a member of this struct. Which we do with (*reflect.StructType); ..3 A StructType has a number of methods, one of which is Field(n) which returns the nth field of a structure. The type of this return is a StructField; ..4 The struct StructField has a Tag member which returns the tag-name as a string. So on the 0th field we can unleash .Tag to access this name: Field(0).Tag. This finally gives us namestr. To make the difference between looking a types and values more clear, that a look at the following code: Listing 6.10. Reflection and the type and value func show(i interface{}){ switch t :=i.( type){ case *Person: r :=reflect.NewValue(i) ← Entertheworldofreflection tag := ..0 r.(*reflect.PtrValue).Elem().Type().(*reflect.StructType).Field (0).tag nam := ..1 r.(*reflect.PtrValue).Elem().(*reflect.StructValue).Field(0). (*reflect.StringValue).Get() } } ..0 Here we want to get the ”tag”, which means going for the type. Thus we need Elem().Type().(*reflect.StructType) to get to it; ..1 Now we want to get access to the value of one of the members and we employ Elem().(*reflect.StructValue) to get to it. Now we have arrived at the structure. Then we go the the first field Field(0), tell reflect is a *reflect.StringValue and invoke the Get() method on it. Exercises 73 Figure 6.1. Peeling away the layers using reflection. Going from a *Person via *reflect.PtrValue using the methods described in godocreflect to get the actual string contained deep within. *reflect.PtrValue .Elem() *reflect.Type .(*reflect.StructValue) *reflect.StructValue .FieldByName("Name")*reflect.StructField .(*reflect.StringValue)*reflect.StringValue .Get() "Albert Einstein""Albert Einstein" Reflection works by peeling off layers once you have got your hands on a Value in the reflection world. Setting a value works similarly as getting a value, but only works on exported members. Again some code: Listing 6.11. Reflect with private member type Person struct { name string "namestr" age int } func Set(i interface{}){ switch t :=i.( type){ case *Person: r := reflect.NewValue(i) r.(*reflect.PtrValue).Elem(). (*reflect.StructValue). FieldByName("name"). (*reflect.StringValue). Set("Albert Einstein") } } Listing 6.12. Reflect with public member type Person struct { Name string "namestr" ← age int } func Set(i interface{}){ switch t := i.(type){ case *Person: r :=reflect.NewValue(i) r.(*reflect.PtrValue).Elem(). (*reflect.StructValue). FieldByName("Name"). ← (*reflect.StringValue). Set("Albert Einstein") } } The code on the left compiles and runs, but when you run it, you are greeted with a stack trace and a runtime error: panic:cannotsetvalueobtainedviaunexportedstructfield The code on the right works OK and sets the member Name to ”Albert Einstein”. Of course this only works when you call Set() with a pointer argument. Exercises Q23. (6) Interfaces and compilation 1. The code in listing 6.4 on page 66 compiles OK — as stated in the text. But when you run it you’ll get a runtime error, so something is wrong. Why does the code 74 Chapter 6: 接口 compile cleanly then? Q24. (5) Pointers and reflection 1. One of the last paragraphs in section ”Introspection and reflection” on page 71, has the following words: The code on the right works OK and sets the member Name to ”Albert Einstein”. Of course this only works when you call Set() with a pointer argument. Why is this the case? Q25. (1) Interfaces and min-max 1. Make min-max generic so that it works for both integers and strings as in the ex- ample in section ”A sorting example”. Answers 75 Answers A23. (6) Interfaces and compilation 1. The code compiles because an integer type implements the empty interface and that is the check that happens at compile time. A proper way to fix this, is to test if such an empty interface can be converted and if so, call the appropriate method. The Go code that defines the function g in listing 6.3 – repeated here: func g(any interface{}) int { return any.(I).Get() } Should be changed to become: func g(any interface{}) int { if v, ok:= any.(I) ; ok {//Checkifanycanbeconverted return v.Get() //IfsoinvokeGet() } return -1 //Justsowereturnanything } If g() is called now there are no run-time errors anymore. The idiom used is called ”comma ok” in Go. A24. (5) Pointers and reflection 1. When called with a non-pointer argument the variable is a copy (call-by-value). So you are doing the reflection voodoo on a copy. And thus you are not changing the original value, but only this copy. A25. (1) Interfaces and min-max 1. 7 并行 • Parallelism is about performance; • Concurrency is about program design. Google IO 2010 ROBEPIKE In this chapter we will show off Go’s ability for concurrent programming using channels and goroutines. Goroutines are the central entity in Go’s ability for concurrency. But what is a goroutine? From [3]: They’re called goroutines because the existing terms — threads, coroutines, processes, and so on — convey inaccurate connotations. A goroutine has a simple model: it is a function executing in parallel with other goroutines in the same address space. It is lightweight, costing little more than the alloca- tion of stack space. And the stacks start small, so they are cheap, and grow by allocating (and freeing) heap storage as required. A goroutine is a normal function, except that you start it with the keyword go. ready("Tee", 2) ← Normalfunctioncall go ready("Tee", 2) ← ready()startedasgoroutine The following idea for a program was taken from [27]. We run a function as two goroutines, the goroutines wait for an amount of time and then print something to the screen. On the lines 14 and 15 we start the goroutines. The main function waits long enough, so that both goroutines will have printed their text. Right now we wait for 5 seconds (time.Sleep() counts in ns) on line 17, but in fact we have no idea how long we should wait until all goroutines have exited. Listing 7.1. Go routines in action 8func ready(w string, sec int64){ 9time.Sleep(sec * 1e9) 10fmt.Println(w, "is ready!") 11} 13func main() { 14go ready("Tee", 2) 15go ready("Coffee", 1) 16fmt.Println("I'm waiting") 17time.Sleep(5 * 1e9) 18} Listing 7.1 outputs: I'mwaiting ← rightaway Coffeeisready! ← after1second Teeisready! ← after2seconds 并行 77 If we did not wait for the goroutines (i.e. remove line 17) the program would be termi- nated immediately and any running goroutines would die with it. To fix this we need some kind of mechanism which allows us to communicate with the goroutines. This mechanism is available to us in the form of channels. A channel can be compared to a two-way pipe in Unix shells: you can send to and receive values from it. Those values can only be of a specific type: the type of the channel. If we define a channel, we must also define the type of the values we can send on the channel. Note that we must use make to create a channel: ci := make(chan int ) cs := make(chan string) cf := make(chan interface{}) Makes ci a channel on which we can send and receive integers, makes cs a channel for strings and cf a channel for types that satisfy the empty interface. Sending on a channel and receiving from it, is done with the same operator: <-. Depending on the operands it figures out what to do: ci <- 1 ← Send theinteger1tothechannelci <-ci ← Receive anintegerfromthechannelci i := <-ci ← Receive fromthechannelciandstoringitini Lets put this to use. Listing 7.2. Go routines and a channel var c chan int ..0 func ready(w string, sec int){ time.Sleep(int64(sec) * 1e9) fmt.Println(w, "is ready!") c <- 1 ..1 } func main() { c = make(chan int)..2 go ready("Tee", 2) ..3 go ready("Coffee", 1) fmt.Println("I'm waiting, but not too long") <-c ..4 <-c ..5 } ..0 Declare c to be a variable that is a channel of ints. That is: this channel can move integers. Note that this variable is global so that the goroutines have access to it; ..1 Send the integer 1 on the channel c; ..2 Initialize c; ..3 Start the goroutines with the keyword go; ..4 Wait until we receive a value from the channel. Note that the value we receive is discarded; 78 Chapter 7: 并行 ..5 Two goroutines, two values to receive. There is still some remaining ugliness; we have to read twice from the channel (lines 14 and 15). This is OK in this case, but what if we don’t know how many goroutines we started? This is where another Go built-in comes in: select. With select you can (among other things) listen for incoming data on a channel. Using select in our program does not really make it shorter, because we run too few goroutines. We remove the lines 14 and 15 and replace them with the following: Listing 7.3. Using select 14L: for { 15select { 16case <- c: 17i++ 18if i > 1 { 19break L 20} 21} 22} Make it run in parallel While our goroutines were running concurrent, they were not running in parallel. When you do not tell Go anything there can only be one goroutine running at a time. With runtime.GOMAXPROCS(n) you can set the number of goroutines that can run in parallel. From the documentation: GOMAXPROCS sets the maximum number of CPUs that can be executing si- multaneously and returns the previous setting. If n < 1, it does not change the current setting. This call will go away when the scheduler improves. If you do not want to change any source code you can also set an environment variable GOMAXPROCS to the desired value. More on channels When you create a channel in Go with ch:= make(chanbool ), an unbuffered channel for bools is created. What does this mean for your program? For one, if you read (value := <-ch) it will block until there is data to receive. Secondly anything sending (ch<-5) will block until there is somebody to read it. Unbuffered channels make a perfect tool for synchronising multiple goroutines. But Go allows you to specify the buffer size of a channel, which is quite simple how many elements a channel can hold. ch:= make(chanbool , 4), creates a buffered chan- nels of bools that can hold 4 elements. The first 4 elements in this channels are written without any blocking. When you write the 5th element, your code will block, until another goroutine reads some elements from the channel to make room. Although reads from channels block, you can perform an non-blocking read with the following syntax:TODO Still need to test this. x, ok= <-ch Exercises 79 Where ok is set to true when there was something to read (otherwise it is false. And if that was the case x get the value read from the channel. In conclusion, the following is true in Go: ch:= make(chantype , value) { value == 0 → unbuffered (blocking) value > 0 → buffered (non-blocking) up to value elements) Closing channels Wrapping functions in goroutines See godns/resolver.go and how to wrap the Query() function in a goroutine with error handling and such. Exercises Q26. (4) Channels 1. Modify the program you created in exercise Q2 to use channels, in other words, the function called in the body should now be a goroutine and communication should happen via channels. You should not worry yourself on how the goroutine terminates. 2. There are a few annoying issues left if you resolve question 1. One of the problems is that the goroutine isn’t neatly cleaned up when main.main() exits. And worse, due to a race condition between the exit of main.main() and main.shower() not all numbers are printed. It should print up until 9, but sometimes it prints only to 8. Adding a second quit-channel you can remedy both issues. Do this.a Q27. (7) Fibonacci II 1. This is the same exercise as the one given page 32 in exercise 11. For completeness the complete question: The Fibonacci sequence starts as follows: 1, 1, 2, 3, 5, 8, 13,... Or in mathematical terms: x1 = 1; x2 = 1; xn = xn−1 + xn−2 ∀n > 2. Write a function that takes an int value and gives that many terms of the Fibonacci sequence. But now the twist: You must use channels. aYou will need the select statement. Answers 81 Answers A26. (4) Channels 1. A possible program is: Listing 7.4. Channels in Go 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) 17} 18} We start of in the usual way, then at line 6 we create a new channel of ints. In the next line we fire off the function shower with the ch variable as it argument, so that we may communicate with it. Next we start our for-loop (lines 8-10) and in the loop we send (with <-) our number to the function (now a goroutine) shower. In the function shower we wait (as this blocks) until we receive a number (line 15). Any received number is printed (line 16) and then continue the endless loop started on line 14. 2. An answer is Listing 7.5. Adding an extra quit 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 11} 12quit <- false // ortrue,doesnotmatter 82 Chapter 7: 并行 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} On line 20 we read from the quit channel and we discard the value we read. We could have used q := <-quit, but then we would have used the variable only once — which is illegal in Go. Another trick you might have pulled out of your hat may be: _ = <-quit. This is valid in Go, but the Go idiom favors the one given on line 20. A27. (7) Fibonacci II 1. The following program calculates the Fibonacci numbers using channels. Listing 7.6. A Fibonacci function in 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) go func (){ x <- 0 x <- 1 <-a for{ x <- <-a+<-b Answers 83 } }() return out } func main() { x := fib() for i :=0 ; i < 10; i++ { fmt.Println( <-x) } } // Seesdh33b.blogspot.com/2009/12/fibonacci-in-go.html 8 通讯 Good communication is as stimulating as black coffee, and just as hard to sleep after. ANNEMORROWLINDBERGH In this chapter we are going to look at the building blocks in Go for communicating with the outside world. Files Reading from (and writing to) files is easy in Go. This program only uses the os package to read data from the file /etc/passwd. Listing 8.1. Reading from a file (unbufferd) 1package main 3import "os" 5func main() { 6buf := make ([]byte, 1024) 7f, _ := os.Open("/etc/passwd", os.O_RDONLY, 0666) 8defer f.Close() 9for { 10n, _ :=f.Read(buf) 11if n == 0 { break } 12os.Stdout.Write(buf[0:n]) 13} 14} If you want to use buffered IO there is the bufio package: Listing 8.2. Reading from a file (bufferd) 1package main 3import ( "os";"bufio") 5func main() { 6buf := make ([]byte, 1024) 7f, _ := os.Open("/etc/passwd", os.O_RDONLY, 0666) 8defer f.Close() 9r :=bufio.NewReader(f) 10w :=bufio.NewWriter(os.Stdout) 11defer w.Flush() 12for { 13n, _ :=r.Read(buf) 14if n == 0 { break } Command line arguments 85 15w.Write(buf[0:n]) 16} 17} On line 9 we create a bufio.Reader from f which is of type *File. NewReader expects an io.Reader, so you might think this will fail. But it doesn’t. An io.Reader is defined as: type Reader interface { Read(p []byte) (n int, erros.Error) } So anything that has such a Read() function implements this interface. And from listing 8.1 (line 10) we can see that *File indeed does so. Command line arguments Arguments from the command line are available inside your program via the string slice os.Args, provided you have imported the package os. The flag package has a more sophis- ticated interface, and also provides a way to parse flags. Take this example from a little DNS query tool: var dnssec *bool = flag.Bool("dnssec", false, "Request DNSSEC records") ..0 var port *string = flag.String("port", "53", "Set thequery port") ..1 flag.Usage = func(){..2 fmt.Fprintf(os.Stderr, "Usage: %s [@server] [qtype] [qclass] [name ...]\n", os.Args[0]) flag.PrintDefaults() ..3 } flag.Parse() ..4 ..0 Define a bool flag, -dnssec. The variable must be a pointer otherwise the package can not set its value; ..1 Idem, but for a port option; ..2 Slightly redefine the Usage function, to be a little more verbose; ..3 For every flag given, PrintDefaults will output the help string; ..4 Parse the flags and fill the variables. Executing commands The exec package has function to run external commands, and it the premier way to exe- cute commands from within a Go program. We start commands with the Run function: func Run(argv0 string, argv, envv [] string, dir string, stdin, stdout, stderr int) (p *Cmd, erros.Error) Run starts the binary prog running with arguments argv and environment envv. It returns a pointer to a new Cmd representing the command or an error. 86 Chapter 8: 通讯 Lets execute ls-l : import "exec" cmd, err:=exec.Run("/bin/ls", [] string{"ls", "-l"}, nil, "", exec. DevNull, exec.DevNull, exec.DevNull) In the os package we find the StartProcess function. This is another way (but more low level) to start executables.a The prototype for StartProcess is: func StartProcess(name string, argv []string , envv [] string, dir string, fd []*File) (pid int, errError) With the following documentation: StartProcess starts a new process with the program, arguments, and environ- ment specified by name, argv, and envv. The fd array specifies the file descriptors to be set up in the new process: fd[0] will be Unix file descriptor 0 (standard input), fd[1] descriptor 1, and so on. A nil entry will cause the child to have no open file descriptor with that index. If dir is not empty, the child chdirs into the directory before execing the program. Suppose we want to execute ls-l again: import "os" pid, err:=os.StartProcess("/bin/ls", [] string{"ls", "-l"}, nil, "", []* os.File{ os.Stdin, os.Stdout, os.Stderr }) defer os.Wait(pid, os.WNOHANG) ← Otherwiseyoucreateazombie Note that os.Wait (among other things) returns the exitcode , with: w := os.Wait(pid, os.WNOHANG) e := w.WaitStatus.ExitStatus() ← ExitStatus()returnsaninteger Networking All network related types and functions can be found in the package net. One of the most important functions in there is Dial. When you Dial into a remote system the function returns a Conn interface type, which can be used to send and receive information. The function Dial neatly abstracts away the network family and transport. So IPv4 or IPv6, TCP or UDP can all share a common interface. Dialing a remote system (port 80) over TCP, then UDP and lastly TCP over IPv6 looks like this:b conn, err:=Dial("tcp", "", "192.0.32.10:80") conn, err:=Dial("udp", "", "192.0.32.10:80") conn, err:=Dial("tcp", "", "[2620:0:2d0:200::10]:80") ← Bracketsaremandatory And with conn you can do read/write .TODO dkls TODO Write echo server aThere is talk on the go-nuts mailing list about separating Fork and Exec. bIn case you are wondering, 192.0.32.10 and 2620:0:2d0:200::10 are www.example.org. Netchan: networking and channels 87 Netchan: networking and channels Exercises Q28. (8) Processes 1. Write a program that takes a list of all running processes and prints how many child processes each parent has spawned. The output should look like: Pid0has2children:[12] Pid490has2children:[119926524] Pid1824has1child:[7293] • For acquiring the process list, you’ll need to capture the output of ps-e-opid,ppid,comm . This output looks like: PIDPPIDCOMMAND 90249023zsh 195609024ps • If a parent has one child you must print child, is there are more than one print children; • The process list must be numerically sorted, so you start with pid 0 and work your way up. Here is a Perl version to help you on your way (or to create complete and utter confusion). Listing 8.3. Processes in Perl #!/usr/bin/perl -l my (%child, $pid, $parent) ; my @ps=`ps -e -opid,ppid,comm`;# Capture theouput from `ps` foreach (@ps[1..$#ps]) {# Discard theheader line ($pid, $parent, undef) = split ;# Split theline, discard 'comm' push @{$child{$parent}}, $pid;# Save thechild PIDs ona list } # Walk through thesorted PPIDs foreach (sort { $a <=>$b } keys %child) { print "Pid ", $_," has", @ {$child{$_}}+0, " child", # Print them @{$child{$_}} == 1 ? ": " : "ren: ", "[@{$child{$_}}]"; } Q29. (5) Word and letter count 1. Write a small program that reads text from standard input and performs the fol- lowing actions: 1. Count the number of characters (including spaces); 2. Count the number of words; 3. Count the numbers of lines. In other words implement wc(1) (check you local manual page), however you only have to read from standard input. 88 Chapter 8: 通讯 Q30. (4) Uniq 1. Write a Go program that mimics the function of the Unix uniq command. This program should work as follows, given a list with the following items: 'a''b''a''a''a''c''d''e''f''g' it should print only those item which don’t have the same successor: 'a''b''a''c''d''e''f' Listing 8.6 is a Perl implementation of the algorithm. Listing 8.6. uniq(1) in Perl #!/usr/bin/perl my @a = qw/abaaacdefg/; print my $first = shift @a; foreach (@a) { if ($first ne $_) { print; $first = $_;} } Q31. (9) Quine A Quine is a program that prints itself. 1. Write a Quine in Go. Q32. (9) Number cruncher • Pick six (6) random numbers from this list: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 25, 50, 75, 100 Numbers may be picked multiple times; • Pick one (1) random number (i) in the range: 1 ... 1000; • Tell how, by combining the first 6 numbers (or a subset thereof) with the operators +,−,∗ and /, you can make i; An example. We have picked the numbers: 1, 6, 7, 8, 8 and 75. And i is 977. This can be done in many different ways, one way is: ((((1 ∗ 6) ∗ 8) + 75) ∗ 8) − 7 = 977 or (8 ∗ (75 + (8 ∗ 6))) − (7/1) = 977 1. Implement a number cruncher that works like that. Make it print the solution in a similar format (i.e. output should be infix with parenthesis) as used above. 2. Calculate all possible solutions and show them (or only show how many there are). In the example above there are 544 ways to do it. Answers 89 Answers A28. (8) Processes 1. There is lots of stuff to do here. We can divide our program up in the following sections: 1. Starting ps and capturing the output; 2. Parsing the output and saving the child PIDs for each PPID; 3. Sorting the PPID list; 4. Printing the sorted list to the screen In the solution presented below, we’ve opted to use container/vector to hold the PIDs. This ”list” grows automatically. The function atoi (lines 19 through 22) is defined to get rid of the multiple return values of the original strconv.Atoi, so that it can be used inside function calls that only accept one argument, as we do on lines 45, 47 and 50. A possible program is: Listing 8.4. Processes in Go 1package main 3import ( 4"os" 5"fmt" 6"sort" 7"bufio" 8"strings" 9"strconv" 10"container/vector" 11) 13const ( 14PID = iota 15PPID 16) 18func atoi(s string) (x int ){ 19x, _ = strconv.Atoi(s) 20return 21} 23func main() { 24pr,pw,_ :=os.Pipe() 25defer pr.Close() 26r := bufio.NewReader(pr) 27w := bufio.NewWriter(os.Stdout) 28defer w.Flush() 29pid, _ :=os.StartProcess("/bin/ps", [] string{"ps", "-e", "- opid,ppid,comm"}, nil, "", []*os.File {nil, pw,nil }) 90 Chapter 8: 通讯 30defer os.Wait(pid, os.WNOHANG) 31pw.Close() 33child := make(map[int]*vector.IntVector) 34s, ok:=r.ReadString('\n') // Discardtheheaderline 35s, ok= r.ReadString('\n') 36for ok ==nil { 37f := strings.Fields(s) 38if _, present := child[atoi(f[PPID])] ;!present { 39v := new(vector.IntVector) 40child[atoi(f[PPID])] = v 41} 42//SavethechildPIDsona vector 43child[atoi(f[PPID])].Push(atoi(f[PID])) 44s, ok = r.ReadString('\n') 45} 47// SortthePPIDs 48schild := make([]int , len (child)) 49i := 0 50for k, _ := range child { 51schild[i] = k 52i++ 53} 54sort.SortInts(schild) 55// Walkthroughtthesortedlist 56for _, ppid := range schild { 57fmt.Printf("Pid %d has%d child", ppid, child[ppid]. Len()) 58if child[ppid].Len() ==1 { 59fmt.Printf(": %v\n", []int(*child[ppid])) 60} else { 61fmt.Printf("ren: %v\n", []int(*child[ppid])) 62} 63} 64} A29. (5) Word and letter count 1. The following program is an implementation of wc(1). Listing 8.5. wc(1) in Go package main import ( "os" "fmt" "bufio" "strings" Answers 91 ) 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++ } } } ..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 because of a EOF. So we print the current values; ..3 Otherwise we count the charaters, words and increment the lines. A30. (4) Uniq 1. The following is a uniq implementation in Go. Listing 8.7. uniq(1) in Go package main import "fmt" func main() { list :=[] string{"a", "b", "a", "a", "c", "d", "e", "f"} first :=list[0] fmt.Printf("%s ", first) for _, v := range list[1:] { if first != v { fmt.Printf("%s ", v) first = v } } } A31. (9) Quine 92 Chapter 8: 通讯 1. The following Quine is from Russ Cox: /* Go quine */ package main import "fmt" func main() { fmt.Printf("%s%c%s%c\n", q, 0x60, q, 0x60) } var q = `/* Goquine */ package main import "fmt" func main() { fmt.Printf("%s%c%s%c\n", q, 0x60, q, 0x60) } varq = ` A32. (9) Number cruncher 1. The following is one possibility. It uses recursion and backtracking to get an an- swer. Listing 8.8. Number cruncher package main import ( "fmt" "strconv" "container/vector" "flag" ) const ( _ = 1000 * iota ADD SUB 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++ } Answers 93 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 } //list:=[]int1,6,7,ADD,SUB,MUL,DIV magic, ok:=strconv.Atoi(flag.Arg(0)) 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 { //itisa number,saveit 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 // resetandgoon } found++ fmt.Printf("%s = %d #%d\n", rpnstr(form[0:index+1]), value, found) //gotoNEXT } if index == MAXPOS-1 { if v < ADD { numberop[i] = tmp // resetandgoon } goto NEXT } solve(form, numberop, index+1, magic) if v < ADD { numberop[i] = tmp //resetandgoon } NEXT: } } // convertrpntoniceinfixnotationandstring // thermustbevalidrpnform func rpnstr(r []int) (ret string ){ s := new(vector.StringVector) for k, t := range r { switch t { case ADD, SUB, MUL, DIV: a :=s.Pop() b :=s.Pop() if k == len(r)-1 { s.Push(b + mop[t] + a) } else { s.Push("(" + b + mop[t] + a + ")") } default: s.Push(strconv.Itoa(t)) } } for _, v := range *s { ret +=v } return } // returnresultfromtherpnform. // iftheexpressionisnotvalid,okisfalse 94 Chapter 8: 通讯 func rpncalc(r []int)(int , bool){ stack.Reset() for _, t := range r { 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 { // disallownegativesubresults if b-a < 0 { return 0, false } stack.Push(b - a) } if t ==MUL { stack.Push(b * a) } if t ==DIV { if a == 0 { return 0, false } // disallowfractions if b%a != 0 { return 0, false } stack.Push(b / a) } default: stack.Push(t) } } if stack.Len() ==1 {// thereisonlyone! return stack.Pop(), true } return 0, false } 2. When starting permrec we give 977 as the first argument: % ./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 A 版权 本作品由LaTEX创作。主文本设置为Google Droid 字体。所有打印文本设置为DejaVu Mono。 中文字体设置为WenQuanYi Zen Hei。 贡献者 下面的成员编写了这本书。 • Miek Gieben ; • JC van Winkel。 下面的成员翻译了这本书。 • 邢兴 。 帮助试读、检查练习和改进文案(部分排名先后)Filip Zaludek,Jonathan Kans,Jaap Akkerhuis,Mayuresh Kathe,Makoto Inoue,Ben Bullock,Bob Cunningham,Dan Ko- rtschak,Sonia Keys,Babu Sreekanth,Haiping Fan,Cecil New,Andrey Mirtchovski,Russel Winder。 Miek Gieben Miek Gieben 从荷兰内梅亨大学取得计算机科学硕士学位。他参与并开发了DNSSEC 协议——下一代DNS以及如核心认证[22]。在玩过了Erlang 后,Go 是他当前迷恋的主要 语言。他将所有业余时间用来对Go 的探索和编码。他是Go DNS 库的维护者:https:// github.com/miekg/godns。他的个人博客是http://www.miek.nl 以及Twitter 帐号@miekg。 博文和推多数情况下都是关于Go 的。 邢兴 在你看到本书的中文版的时候,邢兴应该已经从中山大学获得了软件工程硕士学位。他 的博客是http://mikespook.com 并且拥有Twitter 帐号@mikespook。博文和推大部分情况下 都是用来扯蛋的,而且会时不时的扯疼…… 许可证和版权 97 许可证和版权 本 作 品 依 照 署 名-非 商 业 性 使 用-相 同 方 式 共 享3.0 Unported许 可 证 发 布 。 访 问http:// creativecommons.org/licenses/by-nc-sa/3.0/ 查看该许可证副本,或写信到Creative Com- mons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA。 本书所有实例代码依此方式放入公共领域。 ©Miek Gieben – 2010:,2011。 ©邢兴– 2011。 B 索引 array capacity, 14 length, 14 multidimensional, 13 buffered, 84 built-in append, 13, 16 cap, 12 close, 12 closed, 12 complex, 13 copy, 13, 17 imag, 13 len, 12 make, 12, 53 new, 12, 53 panic, 13 print, 13 println, 13 real, 13 recover, 13 channel, 1, 77 blocking read, 78 blocking write, 78 non-blocking read, 78 non-blocking write, 78 unbuffered, 78 channels, 77 closure, 29 complex numbers, 13 composite literal, 13 deferred list, 29 duck typing, 65 function as values, 30 call, 56 literal, 29 literals, 30 generic, 68 goroutine, 76 goroutines, 1 gotest, 45 interface, 64 set of methods, 64 type, 64 value, 64 keyword break, 8, 9 continue, 10 default, 11 defer, 28 else, 8 fallthrough, 11 for, 9 go, 76 goto, 9 if, 7 import, 44 iota, 4 map, 17 add elements, 17 existence, 17 remove elements, 18 package, 42 range, 10, 17 on maps, 10, 17 on slices, 10 return, 8 select, 78 struct, 55 switch, 11 type, 55 label, 9 method, 24 method call, 56 MixedCaps, 45 named return parameters, 24 networking Dial, 86 nil, 52 operator address-of, 52 and, 6 bit wise xor, 6 Index 99 bitwise and, 6 clear, 6 or, 6 channel, 77 increment, 53 not, 6 or, 6 package bufio, 44, 47, 84 bytes, 44 container/vector, 44 even, 42 exec, 48, 85 flag, 47 fmt, 13, 47 http, 48 io, 47 json, 47 lib, 17 os, 47, 86 reflect, 48, 71 ring, 44 sort, 47 strconv, 47 template, 47 unsafe, 48 parallel assignment, 3, 9 pass-by-reference, 24 pass-by-value, 24 private, 43 public, 43 receiver, 24 reference types, 14 runes, 10 scope local, 25 slice capacity, 14 length, 14 string literal interpreted, 6 raw, 6 type assertion, 66, 70 type switch, 65 variables _, 4 assigning, 3 declaring, 3 underscore, 4 C Bibliography [1] LAMP Group at EPFL. Scala. http://www.scala-lang.org/ , 2010. [2] Go Authors. Defer, panic, and recover. http://blog.golang.org/2010/08/ defer-panic-and-recover.html , 2010. [3] Go Authors. Effective go. http://golang.org/doc/effective_go.html, 2010. [4] Go Authors. Go faq. http://golang.org/doc/go_faq.html, 2010. [5] Go Authors. Go language specification. http://golang.org/doc/go_spec.html, 2010. [6] Go Authors. Go package documentation. http://golang/doc/pkg/, 2010. [7] Go Authors. Go release history. http://golang.org/doc/devel/release.html, 2010. [8] Go Authors. Go tutorial. http://golang.org/doc/go_tutorial.html, 2010. [9] Go Authors. Go website. http://golang.org/, 2010. [10] Haskell Authors. Haskell. http://www.haskell.org/, 2010. [11] Inferno Authors. Inferno. http://www.vitanuova.com/inferno/, 2010. [12] Perl Package Authors. Comprehensive perl archive network. http://cpan.org/, 2010. [13] Plan 9 Authors. Limbo. http://www.vitanuova.com/inferno/papers/limbo.html, 2010. [14] Plan 9 Authors. Plan 9. http://plan9.bell-labs.com/plan9/index.html , 2010. [15] Mark C. Chu-Carroll. Google’s new language: Go. http://scienceblogs.com/ goodmath/2009/11/googles_new_language_go.php, 2010. [16] Go Community. Go issue 65: Compiler can’t spot guaranteed return in if statement. http://code.google.com/p/go/issues/detail?id=65, 2010. [17] Go Community. Go nuts mailing list. http://groups.google.com/group/golang-nuts , 2010. [18] Ericsson Cooperation. Erlang. http://www.erlang.se/, 2010. [19] Brian Kernighan Dennis Ritchie. The c programming language. ..., 2010. [20] James Gosling et al. Java. http://oracle.com/java/, 2010. [21] Larray Wall et al. Perl. http://perl.org/, 2010. [22] Kolkman & Gieben. Dnssec operational practices. http://www.ietf.org/rfc/rfc4641. txt, 2010. [23] C. A. R. Hoare. Communicating sequential processes (csp). ...., 2010. [24] C. A. R. Hoare. Quicksort. http://en.wikipedia.org/wiki/Quicksort, 2010. Bibliography 101 [25] Go Community (SnakE in particular). Function accepting a slice of inter- face types. http://groups.google.com/group/golang-nuts/browse_thread/thread/ 225fad3b5c6d0321, 2010. [26] Rob Pike. The go programming language, day 2. http://golang.org/doc/ GoCourseDay2.pdf, 2010. [27] Rob Pike. The go programming language, day 3. http://golang.org/doc/ GoCourseDay3.pdf, 2010. [28] Rob Pike. Newsqueak: A language for communicating with mice. http://swtch.com/ ~rsc/thread/newsqueak.pdf, 2010. [29] Bjarne Stroustrup. The c++ programming language. ..., 2010. [30] Ian Lance Taylor. Go interfaces. http://www.airs.com/blog/archives/277, 2010. [31] Imran On Tech. Using fizzbuzz to find developers who grok coding. 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. [34] Wikipedia. Duck typing. http://en.wikipedia.org/wiki/Duck_typing, 2010. [35] Wikipedia. Iota. http://en.wikipedia.org/wiki/Iota, 2010. 当前页留空。
还剩114页未读

继续阅读

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

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

需要 10 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

kalaamong

贡献于2013-01-05

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