mirror of
https://github.com/unknwon/the-way-to-go_ZH_CN.git
synced 2025-08-11 23:08:34 +08:00
修改部分描述,添加必要的标点符号,补充代码超链接 (#804)
This commit is contained in:
@@ -97,7 +97,7 @@ import ("fmt"; "os")
|
||||
|
||||
**可见性规则**
|
||||
|
||||
当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 private )。
|
||||
当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是它们在整个包的内部是可见并且可用的(像面向对象语言中的 private )。
|
||||
|
||||
(大写字母可以使用任何 Unicode 编码的字符,比如希腊文,不仅仅是 ASCII 码中的大写字母)。
|
||||
|
||||
@@ -105,7 +105,7 @@ import ("fmt"; "os")
|
||||
|
||||
假设在包 pack1 中我们有一个变量或函数叫做 Thing(以 T 开头,所以它能够被导出),那么在当前包中导入 pack1 包,Thing 就可以像面向对象语言那样使用点标记来调用:`pack1.Thing`(pack1 在这里是不可以省略的)。
|
||||
|
||||
因此包也可以作为命名空间使用,帮助避免命名冲突(名称冲突):两个包中的同名变量的区别在于他们的包名,例如 `pack1.Thing` 和 `pack2.Thing`。
|
||||
因此包也可以作为命名空间使用,帮助避免命名冲突(名称冲突):两个包中的同名变量的区别在于它们的包名,例如 `pack1.Thing` 和 `pack2.Thing`。
|
||||
|
||||
你可以通过使用包的别名来解决包名之间的名称冲突,或者说根据你的个人喜好对包名进行重新设置,如:`import fm "fmt"`。下面的代码展示了如何使用包的别名:
|
||||
|
||||
|
@@ -40,9 +40,9 @@ aVar != 10 -> false
|
||||
|
||||
Go 对于值之间的比较有非常严格的限制,只有两个类型相同的值才可以进行比较,如果值的类型是接口(interface,第 11 章),它们也必须都实现了相同的接口。如果其中一个值是常量,那么另外一个值的类型必须和该常量类型相兼容的。如果以上条件都不满足,则其中一个值的类型必须在被转换为和另外一个值的类型相同之后才可以进行比较。
|
||||
|
||||
布尔型的常量和变量也可以通过和逻辑运算符(非 `!`、和 `&&`、或 `||`)结合来产生另外一个布尔值,这样的逻辑语句就其本身而言,并不是一个完整的 Go 语句。
|
||||
布尔型的常量和变量也可以通过和逻辑运算符(非 `!`、与 `&&`、或 `||`)结合来产生另外一个布尔值,这样的逻辑语句就其本身而言,并不是一个完整的 Go 语句。
|
||||
|
||||
逻辑值可以被用于条件结构中的条件语句(第 5 章),以便测试某个条件是否满足。另外,和 `&&`、或 `||` 与相等 `==` 或不等 `!=` 属于二元运算符,而非 `!` 属于一元运算符。在接下来的内容中,我们会使用 T 来代表条件符合的语句,用 F 来代表条件不符合的语句。
|
||||
逻辑值可以被用于条件结构中的条件语句(第 5 章),以便测试某个条件是否满足。另外,与 `&&`、或 `||` 与相等 `==` 或不等 `!=` 属于二元运算符,而非 `!` 属于一元运算符。在接下来的内容中,我们会使用 T 来代表条件符合的语句,用 F 来代表条件不符合的语句。
|
||||
|
||||
Go 语言中包含以下逻辑运算符:
|
||||
|
||||
@@ -185,7 +185,7 @@ func main() {
|
||||
|
||||
**格式化说明符**
|
||||
|
||||
在格式化字符串里,`%d` 用于格式化整数(`%x` 和 `%X` 用于格式化 16 进制表示的数字),`%g` 用于格式化浮点型(`%f` 输出浮点数,`%e` 输出科学计数表示法),`%0nd` 用于规定输出长度为n的整数,其中开头的数字 0 是必须的。
|
||||
在格式化字符串里,`%d` 用于格式化整数(`%x` 和 `%X` 用于格式化 16 进制表示的数字),`%g` 用于格式化浮点型(`%f` 输出浮点数,`%e` 输出科学计数表示法),`%0nd` 用于规定输出长度为 n 的整数,其中开头的数字 0 是必须的。
|
||||
|
||||
`%n.mg` 用于表示数字 n 并精确到小数点后 m 位,除了使用 g 之外,还可以使用 e 或者 f,例如:使用格式化字符串 `%5.2e` 来输出 3.4 的结果为 `3.40e+00`。
|
||||
|
||||
@@ -228,7 +228,7 @@ Go 拥有以下复数类型:
|
||||
complex64 (32 位实数和虚数)
|
||||
complex128 (64 位实数和虚数)
|
||||
|
||||
复数使用 `re+imI` 来表示,其中 `re` 代表实数部分,`im` 代表虚数部分,I 代表根号负 1。
|
||||
复数使用 `re+imI` 来表示,其中 `re` 代表实数部分,`im` 代表虚数部分,`I` 代表根号负 1。
|
||||
|
||||
示例:
|
||||
|
||||
@@ -369,7 +369,7 @@ b3 := 10 > 5 // b3 is true
|
||||
|
||||
**练习 4.4** 尝试编译 [divby0.go](exercises/chapter_4/divby0.go)。
|
||||
|
||||
你可以将语句 `b = b + a` 简写为 `b+=a`,同样的写法也可用于 `-=`、`*=`、`/=`、`%=`。
|
||||
你可以将语句 `b = b + a` 简写为 `b += a`,同样的写法也可用于 `-=`、`*=`、`/=`、`%=`。
|
||||
|
||||
对于整数和浮点数,你可以使用一元运算符 `++`(递增)和 `--`(递减),但只能用于后缀:
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# 6.11 计算函数执行时间
|
||||
|
||||
有时候,能够知道一个计算执行消耗的时间是非常有意义的,尤其是在对比和基准测试中。最简单的一个办法就是在计算开始之前设置一个起始时候,再由计算结束时的结束时间,最后取出它们的差值,就是这个计算所消耗的时间。想要实现这样的做法,可以使用 `time` 包中的 `Now()` 和 `Sub` 函数:
|
||||
有时候,能够知道一个计算执行消耗的时间是非常有意义的,尤其是在对比和基准测试中。最简单的一个办法就是在计算开始之前设置一个起始时间,再记录计算结束时的结束时间,最后计算它们的差值,就是这个计算所消耗的时间。想要实现这样的做法,可以使用 `time` 包中的 `Now()` 和 `Sub` 函数:
|
||||
|
||||
```go
|
||||
start := time.Now()
|
||||
|
@@ -6,7 +6,7 @@
|
||||
|
||||
而我们要做就是将第 n 个数的值存在数组中索引为 n 的位置(详见第 7 章),然后在数组中查找是否已经计算过,如果没有找到,则再进行计算。
|
||||
|
||||
程序 Listing 6.17 - fibonacci_memoization.go 就是依照这个原则实现的,下面是计算到第 40 位数字的性能对比:
|
||||
程序 Listing 6.17 - [fibonacci_memoization.go](examples/chapter_6/fibonacci_memoization.go) 就是依照这个原则实现的,下面是计算到第 40 位数字的性能对比:
|
||||
|
||||
- 普通写法:4.730270 秒
|
||||
- 内存缓存:0.001000 秒
|
||||
|
@@ -84,7 +84,7 @@ func MultiPly3Nums(a int, b int, c int) int {
|
||||
|
||||
`getX2AndX3` 与 `getX2AndX3_2` 两个函数演示了如何使用非命名返回值与命名返回值的特性。当需要返回多个非命名返回值时,需要使用 `()` 把它们括起来,比如 `(int, int)`。
|
||||
|
||||
命名返回值作为结果形参(result parameters)被初始化为相应类型的零值,当需要返回的时候,我们只需要一条简单的不带参数的return语句。需要注意的是,即使只有一个命名返回值,也需要使用 `()` 括起来(参考 [第 6.6 节](06.6.md)的 fibonacci.go 函数)。
|
||||
命名返回值作为结果形参(result parameters)被初始化为相应类型的零值,当需要返回的时候,我们只需要一条简单的不带参数的 `return` 语句。需要注意的是,即使只有一个命名返回值,也需要使用 `()` 括起来(参考 [第 6.6 节](06.6.md)的 fibonacci.go 函数)。
|
||||
|
||||
示例 6.3 [multiple_return.go](examples/chapter_6/multiple_return.go)
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# 6.7 将函数作为参数
|
||||
|
||||
函数可以作为其它函数的参数进行传递,然后在其它函数内调用执行,一般称之为回调。下面是一个将函数作为参数的简单例子(function_parameter.go):
|
||||
函数可以作为其它函数的参数进行传递,然后在其它函数内调用执行,一般称之为回调。下面是一个将函数作为参数的简单例子([function_parameter.go](examples/chapter_6/function_parameter.go)):
|
||||
|
||||
```go
|
||||
package main
|
||||
|
@@ -20,7 +20,7 @@ func() {
|
||||
|
||||
表示参数列表的第一对括号必须紧挨着关键字 `func`,因为匿名函数没有名称。花括号 `{}` 涵盖着函数体,最后的一对括号表示对该匿名函数的调用。
|
||||
|
||||
下面的例子展示了如何将匿名函数赋值给变量并对其进行调用(function_literal.go):
|
||||
下面的例子展示了如何将匿名函数赋值给变量并对其进行调用([function_literal.go](examples/chapter_6/function_literal.go)):
|
||||
|
||||
```go
|
||||
package main
|
||||
@@ -63,7 +63,7 @@ func (u string) {
|
||||
}(v)
|
||||
```
|
||||
|
||||
请学习以下示例并思考(return_defer.go):函数 `f` 返回时,变量 `ret` 的值是什么?
|
||||
请学习以下示例并思考([return_defer.go](examples/chapter_6/return_defer.go)):函数 `f` 返回时,变量 `ret` 的值是什么?
|
||||
|
||||
```go
|
||||
package main
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# 6.9 应用闭包:将函数作为返回值
|
||||
|
||||
在程序 `function_return.go` 中我们将会看到函数 Add2 和 Adder 均会返回签名为 `func(b int) int` 的函数:
|
||||
在程序 [function_return.go](examples/chapter_6/function_return.go) 中我们将会看到函数 Add2 和 Adder 均会返回签名为 `func(b int) int` 的函数:
|
||||
|
||||
```go
|
||||
func Add2() (func(b int) int)
|
||||
@@ -9,7 +9,7 @@ func Adder(a int) (func(b int) int)
|
||||
|
||||
函数 Add2 不接受任何参数,但函数 Adder 接受一个 int 类型的整数作为参数。
|
||||
|
||||
我们也可以将 Adder 返回的函数存到变量中(function_return.go)。
|
||||
我们也可以将 Adder 返回的函数存到变量中([function_return.go](examples/chapter_6/function_return.go))。
|
||||
|
||||
```go
|
||||
package main
|
||||
@@ -45,7 +45,7 @@ Call Add2 for 3 gives: 5
|
||||
The result is: 5
|
||||
```
|
||||
|
||||
下例为一个略微不同的实现(function_closure.go):
|
||||
下例为一个略微不同的实现([function_closure.go](examples/chapter_6/function_closure.go)):
|
||||
|
||||
```go
|
||||
package main
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# 7.0 数组与切片
|
||||
|
||||
这章我们开始剖析 **容器**, 它是可以包含大量条目(item)的数据结构, 例如数组、切片和 map。从这看到 Go 明显受到 Python 的影响。
|
||||
这章我们开始剖析 **容器**,它是可以包含大量条目(item)的数据结构,例如数组、切片和 map。从这看到 Go 明显受到 Python 的影响。
|
||||
|
||||
以 `[]` 符号标识的数组类型几乎在所有的编程语言中都是一个基本主力。Go 语言中的数组也是类似的,只是有一些特点。Go 没有 C 那么灵活,但是拥有切片(slice)类型。这是一种建立在 Go 语言数组类型之上的抽象,要想理解切片我们必须先理解数组。数组有特定的用处,但是却有一些呆板,所以在 Go 语言的代码里并不是特别常见。相对的,切片确实随处可见的。它们构建在数组之上并且提供更强大的能力和便捷。
|
||||
|
||||
|
@@ -1,11 +1,11 @@
|
||||
# 7.1 声明和初始化
|
||||
|
||||
## 7.1.1 概念
|
||||
数组是具有相同 **唯一类型** 的一组已编号且长度固定的数据项序列(这是一种同构的数据结构);这种类型可以是任意的原始类型例如整型、字符串或者自定义类型。数组长度必须是一个常量表达式,并且必须是一个非负整数。数组长度也是数组类型的一部分,所以[5]int和[10]int是属于不同类型的。数组的编译时值初始化是按照数组顺序完成的(如下)。
|
||||
数组是具有相同 **唯一类型** 的一组已编号且长度固定的数据项序列(这是一种同构的数据结构);这种类型可以是任意的原始类型例如整型、字符串或者自定义类型。数组长度必须是一个常量表达式,并且必须是一个非负整数。数组长度也是数组类型的一部分,所以 `[5]int` 和 `[10]int` 是属于不同类型的。数组的编译时值初始化是按照数组顺序完成的(如下)。
|
||||
|
||||
**注意事项** 如果我们想让数组元素类型为任意类型的话可以使用空接口作为类型(参考 [第 11 章](11.9.md))。当使用值时我们必须先做一个类型判断(参考 [第 11 章](11.3.md))。
|
||||
|
||||
数组元素可以通过 **索引**(位置)来读取(或者修改),索引从 0 开始,第一个元素索引为 0,第二个索引为 1,以此类推。(数组以 0 开始在所有类 C 语言中是相似的)。元素的数目,也称为长度或者数组大小必须是固定的并且在声明该数组时就给出(编译时需要知道数组长度以便分配内存);数组长度最大为 2Gb。
|
||||
数组元素可以通过 **索引**(位置)来读取(或者修改),索引从 0 开始,第一个元素索引为 0,第二个索引为 1,以此类推。(数组以 0 开始在所有类 C 语言中是相似的)。元素的数目(也称为长度或者数组大小)必须是固定的并且在声明该数组时就给出(编译时需要知道数组长度以便分配内存);数组长度最大为 2GB。
|
||||
|
||||
声明的格式是:
|
||||
|
||||
@@ -33,7 +33,7 @@ arr1 的长度是 5,索引范围从 0 到 `len(arr1)-1`。
|
||||
|
||||
runtime error: index out of range
|
||||
|
||||
由于索引的存在,遍历数组的方法自然就是使用 for 结构:
|
||||
由于索引的存在,遍历数组的方法自然就是使用 for 结构:
|
||||
|
||||
- 通过 for 初始化数组项
|
||||
- 通过 for 打印数组元素
|
||||
@@ -99,7 +99,7 @@ for i := range a {
|
||||
|
||||
Go 语言中的数组是一种 **值类型**(不像 C/C++ 中是指向首元素的指针),所以可以通过 `new()` 来创建: `var arr1 = new([5]int)`。
|
||||
|
||||
那么这种方式和 `var arr2 [5]int` 的区别是什么呢?arr1 的类型是 `*[5]int`,而 arr2的类型是 `[5]int`。
|
||||
那么这种方式和 `var arr2 [5]int` 的区别是什么呢?arr1 的类型是 `*[5]int`,而 arr2 的类型是 `[5]int`。
|
||||
|
||||
这样的结果就是当把一个数组赋值给另一个时,需要再做一次数组内存的拷贝操作。例如:
|
||||
|
||||
@@ -181,7 +181,7 @@ var arrAge = [5]int{18, 20, 15, 22, 16}
|
||||
var arrLazy = [...]int{5, 6, 7, 8, 22}
|
||||
```
|
||||
|
||||
`...` 可同样可以忽略,从技术上说它们其实变化成了切片。
|
||||
`...` 同样可以忽略,从技术上说它们其实变成了切片。
|
||||
|
||||
第三种变化:`key: value 语法`
|
||||
|
||||
@@ -258,7 +258,7 @@ func main() {
|
||||
|
||||
## 7.1.4 将数组传递给函数
|
||||
|
||||
把一个大数组传递给函数会消耗很多内存。有两种方法可以避免这种现象:
|
||||
把一个大数组传递给函数会消耗很多内存。有两种方法可以避免这种情况:
|
||||
|
||||
- 传递数组的指针
|
||||
- 使用数组的切片
|
||||
|
@@ -8,11 +8,11 @@
|
||||
|
||||
给定项的切片索引可能比相关数组的相同元素的索引小。和数组不同的是,切片的长度可以在运行时修改,最小为 0 最大为相关数组的长度:切片是一个 **长度可变的数组**。
|
||||
|
||||
切片提供了计算容量的函数 `cap()` 可以测量切片最长可以达到多少:它等于切片的长度 + 数组除切片之外的长度。如果 s 是一个切片,`cap(s)` 就是从 `s[0]` 到数组末尾的数组长度。切片的长度永远不会超过它的容量,所以对于 切片 s 来说该不等式永远成立:`0 <= len(s) <= cap(s)`。
|
||||
切片提供了计算容量的函数 `cap()` 可以测量切片最长可以达到多少:它等于切片的长度 + 数组除切片之外的长度。如果 s 是一个切片,`cap(s)` 就是从 `s[0]` 到数组末尾的数组长度。切片的长度永远不会超过它的容量,所以对于切片 s 来说该不等式永远成立:`0 <= len(s) <= cap(s)`。
|
||||
|
||||
多个切片如果表示同一个数组的片段,它们可以共享数据;因此一个切片和相关数组的其他切片是共享存储的,相反,不同的数组总是代表不同的存储。数组实际上是切片的构建块。
|
||||
|
||||
**优点** 因为切片是引用,所以它们不需要使用额外的内存并且比使用数组更有效率,所以在 Go 代码中 切片比数组更常用。
|
||||
**优点** 因为切片是引用,所以它们不需要使用额外的内存并且比使用数组更有效率,所以在 Go 代码中切片比数组更常用。
|
||||
|
||||
声明切片的格式是: `var identifier []type`(不需要说明长度)。
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
切片的初始化格式是:`var slice1 []type = arr1[start:end]`。
|
||||
|
||||
这表示 slice1 是由数组 arr1 从 start 索引到 `end-1` 索引之间的元素构成的子集(切分数组,start:end 被称为 slice 表达式)。所以 `slice1[0]` 就等于 `arr1[start]`。这可以在 arr1 被填充前就定义好。
|
||||
这表示 slice1 是由数组 arr1 从 `start` 索引到 `end-1` 索引之间的元素构成的子集(切分数组,start:end 被称为 slice 表达式)。所以 `slice1[0]` 就等于 `arr1[start]`。这可以在 arr1 被填充前就定义好。
|
||||
|
||||
如果某个人写:`var slice1 []type = arr1[:]` 那么 slice1 就等于完整的 arr1 数组(所以这种表示方式是 `arr1[0:len(arr1)]` 的一种缩写)。另外一种表述方式是:`slice1 = &arr1`。
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
如果你想去掉 slice1 的最后一个元素,只要 `slice1 = slice1[:len(slice1)-1]`。
|
||||
|
||||
一个由数字 1、2、3 组成的切片可以这么生成:`s := [3]int{1,2,3}[:]`(注: 应先用`s := [3]int{1, 2, 3}`生成数组, 再使用`s[:]`转成切片) 甚至更简单的 `s := []int{1,2,3}`。
|
||||
一个由数字 1、2、3 组成的切片可以这么生成:`s := [3]int{1,2,3}[:]`(注:应先用`s := [3]int{1, 2, 3}`生成数组, 再使用`s[:]`转成切片) 甚至更简单的 `s := []int{1,2,3}`。
|
||||
|
||||
`s2 := s[:]` 是用切片组成的切片,拥有相同的元素,但是仍然指向相同的相关数组。
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
切片也可以用类似数组的方式初始化:`var x = []int{2, 3, 5, 7, 11}`。这样就创建了一个长度为 5 的数组并且创建了一个相关切片。
|
||||
|
||||
切片在内存中的组织方式实际上是一个有 3 个域的结构体:指向相关数组的指针,切片长度以及切片容量。下图给出了一个长度为 2,容量为 4 的切片y。
|
||||
切片在内存中的组织方式实际上是一个有 3 个域的结构体:指向相关数组的指针,切片长度以及切片容量。下图给出了一个长度为 2,容量为 4 的切片 y。
|
||||
|
||||
- `y[0] = 3` 且 `y[1] = 5`。
|
||||
- 切片 `y[0:4]` 由 元素 3,5,7 和 11 组成。
|
||||
@@ -104,13 +104,13 @@ func main() {
|
||||
|
||||
如果 s2 是一个 slice,你可以将 s2 向后移动一位 `s2 = s2[1:]`,但是末尾没有移动。切片只能向后移动,`s2 = s2[-1:]` 会导致编译错误。切片不能被重新分片以获取数组的前一个元素。
|
||||
|
||||
**注意** 绝对不要用指针指向 slice。切片本身已经是一个引用类型,所以它本身就是一个指针!!
|
||||
**注意** 绝对不要用指针指向 slice。切片本身已经是一个引用类型,所以它本身就是一个指针!!
|
||||
|
||||
问题 7.2: 给定切片 `b:= []byte{'g', 'o', 'l', 'a', 'n', 'g'}`,那么 `b[1:4]`、`b[:2]`、`b[2:]` 和 `b[:]` 分别是什么?
|
||||
|
||||
## 7.2.2 将切片传递给函数
|
||||
|
||||
如果你有一个函数需要对数组做操作,你可能总是需要把参数声明为切片。当你调用该函数时,把数组分片,创建为一个 切片引用并传递给该函数。这里有一个计算数组元素和的方法:
|
||||
如果你有一个函数需要对数组做操作,你可能总是需要把参数声明为切片。当你调用该函数时,把数组分片,创建为一个切片引用并传递给该函数。这里有一个计算数组元素和的方法:
|
||||
|
||||
```go
|
||||
func sum(a []int) int {
|
||||
@@ -129,7 +129,7 @@ func main() {
|
||||
|
||||
## 7.2.3 用 make() 创建一个切片
|
||||
|
||||
当相关数组还没有定义时,我们可以使用 make() 函数来创建一个切片 同时创建好相关数组:`var slice1 []type = make([]type, len)`。
|
||||
当相关数组还没有定义时,我们可以使用 make() 函数来创建一个切片,同时创建好相关数组:`var slice1 []type = make([]type, len)`。
|
||||
|
||||
也可以简写为 `slice1 := make([]type, len)`,这里 `len` 是数组的长度并且也是 `slice` 的初始长度。
|
||||
|
||||
@@ -188,7 +188,7 @@ func main() {
|
||||
The length of slice1 is 10
|
||||
The capacity of slice1 is 10
|
||||
|
||||
因为字符串是纯粹不可变的字节数组,它们也可以被切分成 切片。
|
||||
因为字符串是纯粹不可变的字节数组,它们也可以被切分成切片。
|
||||
|
||||
练习 7.4: fobinacci_funcarray.go: 为练习 7.3 写一个新的版本,主函数调用一个使用序列个数作为参数的函数,该函数返回一个大小为序列个数的 Fibonacci 切片。
|
||||
|
||||
@@ -196,8 +196,8 @@ func main() {
|
||||
|
||||
看起来二者没有什么区别,都在堆上分配内存,但是它们的行为不同,适用于不同的类型。
|
||||
|
||||
- new(T) 为每个新的类型T分配一片内存,初始化为 0 并且返回类型为\*T的内存地址:这种方法 **返回一个指向类型为 T,值为 0 的地址的指针**,它适用于值类型如数组和结构体(参见第 10 章);它相当于 `&T{}`。
|
||||
- make(T) **返回一个类型为 T 的初始值**,它只适用于3种内建的引用类型:切片、map 和 channel(参见第 8 章,第 13 章)。
|
||||
- new(T) 为每个新的类型 T 分配一片内存,初始化为 0 并且返回类型为 \*T 的内存地址:这种方法 **返回一个指向类型为 T,值为 0 的地址的指针**,它适用于值类型如数组和结构体(参见第 10 章);它相当于 `&T{}`。
|
||||
- make(T) **返回一个类型为 T 的初始值**,它只适用于 3 种内建的引用类型:切片、map 和 channel(参见第 8 章,第 13 章)。
|
||||
|
||||
换言之,new 函数分配内存,make 函数初始化;下图给出了区别:
|
||||
|
||||
@@ -224,26 +224,26 @@ var v []int = make([]int, 10, 50)
|
||||
v := make([]int, 10, 50)
|
||||
```
|
||||
|
||||
这样分配一个有 50 个 int 值的数组,并且创建了一个长度为 10,容量为 50 的 切片 v,该 切片 指向数组的前 10 个元素。
|
||||
这样分配一个有 50 个 int 值的数组,并且创建了一个长度为 10,容量为 50 的 切片 v,该切片指向数组的前 10 个元素。
|
||||
|
||||
**问题 7.3** 给定 `s := make([]byte, 5)`,len(s) 和 cap(s) 分别是多少?`s = s[2:4]`,len(s) 和 cap(s) 又分别是多少?
|
||||
**问题 7.4** 假设 `s1 := []byte{'p', 'o', 'e', 'm'}` 且 `s2 := s1[2:]`,s2 的值是多少?如果我们执行 `s2[1] = 't'`,s1 和 s2 现在的值又分别是多少?
|
||||
|
||||
*译者注:如何理解new、make、slice、map、channel的关系*
|
||||
*译者注:如何理解 new、make、slice、map、channel 的关系*
|
||||
|
||||
*1.slice、map以及channel都是golang内建的一种引用类型,三者在内存中存在多个组成部分,
|
||||
需要对内存组成部分初始化后才能使用,而make就是对三者进行初始化的一种操作方式*
|
||||
*1.slice、map 以及 channel 都是 golang 内建的一种引用类型,三者在内存中存在多个组成部分,
|
||||
需要对内存组成部分初始化后才能使用,而 make 就是对三者进行初始化的一种操作方式*
|
||||
|
||||
*2. new 获取的是存储指定变量内存地址的一个变量,对于变量内部结构并不会执行相应的初始化操作,
|
||||
所以slice、map、channel需要make进行初始化并获取对应的内存地址,而非new简单的获取内存地址*
|
||||
所以 slice、map、channel 需要 make 进行初始化并获取对应的内存地址,而非 new 简单的获取内存地址*
|
||||
|
||||
## 7.2.5 多维 切片
|
||||
## 7.2.5 多维切片
|
||||
|
||||
和数组一样,切片通常也是一维的,但是也可以由一维组合成高维。通过分片的分片(或者切片的数组),长度可以任意动态变化,所以 Go 语言的多维切片可以任意切分。而且,内层的切片必须单独分配(通过 make 函数)。
|
||||
|
||||
## 7.2.6 bytes 包
|
||||
|
||||
类型 `[]byte` 的切片十分常见,Go 语言有一个 bytes 包专门用来解决这种类型的操作方法。
|
||||
类型 `[]byte` 的切片十分常见,Go 语言有一个 bytes 包专门用来提供这种类型的操作方法。
|
||||
|
||||
bytes 包和字符串包十分类似(参见第 4.7 节)。而且它还包含一个十分有用的类型 Buffer:
|
||||
|
||||
|
@@ -89,7 +89,7 @@ b) 如果 a) 无法正常工作,写一个 for 循环让值可以 double。
|
||||
|
||||
**问题 7.6** 通过使用省略号操作符 `...` 来实现累加方法。
|
||||
|
||||
**练习 7.7** sum_array.go
|
||||
**练习 7.7** [sum_array.go](exercises/chapter_7/sum_array.go)
|
||||
|
||||
a) 写一个 Sum 函数,传入参数为一个 32 位 float 数组成的数组 arrF,返回该数组的所有数字和。
|
||||
|
||||
@@ -97,7 +97,7 @@ a) 写一个 Sum 函数,传入参数为一个 32 位 float 数组成的数组
|
||||
|
||||
b) 写一个 SumAndAverage 方法,返回两个 int 和 float32 类型的未命名变量的和与平均值。
|
||||
|
||||
**练习 7.8** min_max.go
|
||||
**练习 7.8** [min_max.go](exercises/chapter_7/min_max.go)
|
||||
|
||||
写一个 minSlice 方法,传入一个 int 的切片并且返回最小值,再写一个 maxSlice 方法返回最大值。
|
||||
|
||||
|
@@ -22,7 +22,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
`func append(s[]T, x ...T) []T` 其中 append 方法将 0 个或多个具有相同类型 s 的元素追加到切片后面并且返回新的切片;追加的元素必须和原切片的元素同类型。如果 s 的容量不足以存储新增元素,append 会分配新的切片来保证已有切片元素和新增元素的存储。因此,返回的切片可能已经指向一个不同的相关数组了。append 方法总是返回成功,除非系统内存耗尽了。
|
||||
`func append(s[]T, x ...T) []T` 其中 append 方法将 0 个或多个具有相同类型 s 的元素追加到切片后面并且返回新的切片;追加的元素必须和原切片的元素是同类型。如果 s 的容量不足以存储新增元素,append 会分配新的切片来保证已有切片元素和新增元素的存储。因此,返回的切片可能已经指向一个不同的相关数组了。append 方法总是返回成功,除非系统内存耗尽了。
|
||||
|
||||
如果你想将切片 y 追加到切片 x 后面,只要将第二个参数扩展成一个列表即可:`x = append(x, y...)`。
|
||||
|
||||
@@ -60,7 +60,7 @@ func AppendByte(slice []byte, data ...byte) []byte {
|
||||
|
||||
**练习 7.12**
|
||||
|
||||
写一个函数 RemoveStringSlice 将从 start 到 end 索引的元素从切片 中移除。
|
||||
写一个函数 RemoveStringSlice 将从 start 到 end 索引的元素从切片中移除。
|
||||
|
||||
## 链接
|
||||
|
||||
|
@@ -2,9 +2,9 @@
|
||||
|
||||
## 7.6.1 从字符串生成字节切片
|
||||
|
||||
假设 s 是一个字符串(本质上是一个字节数组),那么就可以直接通过 `c := []byte(s)` 来获取一个字节的切片 c。另外,您还可以通过 copy 函数来达到相同的目的:`copy(dst []byte, src string)`。
|
||||
假设 s 是一个字符串(本质上是一个字节数组),那么就可以直接通过 `c := []byte(s)` 来获取一个字节的切片 c 。另外,您还可以通过 copy 函数来达到相同的目的:`copy(dst []byte, src string)`。
|
||||
|
||||
同样的,还可以使用 for-range 来获得每个元素(Listing 7.13—for_string.go):
|
||||
同样的,还可以使用 for-range 来获得每个元素(Listing 7.13 — [for_string.go](examples/chapter_7/for_string.go)):
|
||||
|
||||
```go
|
||||
package main
|
||||
@@ -37,7 +37,7 @@ b = append(b, s...)
|
||||
|
||||
## 7.6.2 获取字符串的某一部分
|
||||
|
||||
使用 `substr := str[start:end]` 可以从字符串 str 获取到从索引 start 开始到 `end-1` 位置的子字符串。同样的,`str[start:]` 则表示获取从 start 开始到 `len(str)-1` 位置的子字符串。而 `str[:end]` 表示获取从 0 开始到 `end-1` 的子字符串。
|
||||
使用 `substr := str[start:end]` 可以从字符串 str 获取到从索引 `start` 开始到 `end-1` 位置的子字符串。同样的,`str[start:]` 则表示获取从 `start` 开始到 `len(str)-1` 位置的子字符串。而 `str[:end]` 表示获取从 0 开始到 `end-1` 的子字符串。
|
||||
|
||||
## 7.6.3 字符串和切片的内存结构
|
||||
|
||||
|
@@ -23,7 +23,7 @@ map 传递给函数的代价很小:在 32 位机器上占 4 个字节,64 位
|
||||
|
||||
map 也可以用函数作为自己的值,这样就可以用来做分支结构(详见第 5 章):key 用来选择要执行的函数。
|
||||
|
||||
如果 key1 是 map1 的key,那么 `map1[key1]` 就是对应 key1 的值,就如同数组索引符号一样(数组可以视为一种简单形式的 map,key 是从 0 开始的整数)。
|
||||
如果 key1 是 map1 的 key,那么 `map1[key1]` 就是对应 key1 的值,就如同数组索引符号一样(数组可以视为一种简单形式的 map,key 是从 0 开始的整数)。
|
||||
|
||||
key1 对应的值可以通过赋值符号来设置为 val1:`map1[key1] = val1`。
|
||||
|
||||
@@ -80,7 +80,7 @@ mapAssigned 也是 mapLit 的引用,对 mapAssigned 的修改也会影响到 m
|
||||
|
||||
**不要使用 new,永远用 make 来构造 map**
|
||||
|
||||
**注意** 如果你错误的使用 new() 分配了一个引用对象,你会获得一个空引用的指针,相当于声明了一个未初始化的变量并且取了它的地址:
|
||||
**注意** 如果你错误地使用 new() 分配了一个引用对象,你会获得一个空引用的指针,相当于声明了一个未初始化的变量并且取了它的地址:
|
||||
|
||||
```go
|
||||
mapCreated := new(map[string]float32)
|
||||
@@ -130,7 +130,7 @@ noteFrequency := map[string]float32 {
|
||||
|
||||
## 8.1.3 用切片作为 map 的值
|
||||
|
||||
既然一个 key 只能对应一个 value,而 value 又是一个原始类型,那么如果一个 key 要对应多个值怎么办?例如,当我们要处理unix机器上的所有进程,以父进程(pid 为整型)作为 key,所有的子进程(以所有子进程的 pid 组成的切片)作为 value。通过将 value 定义为 `[]int` 类型或者其他类型的切片,就可以优雅的解决这个问题。
|
||||
既然一个 key 只能对应一个 value,而 value 又是一个原始类型,那么如果一个 key 要对应多个值怎么办?例如,当我们要处理unix机器上的所有进程,以父进程(pid 为整型)作为 key,所有的子进程(以所有子进程的 pid 组成的切片)作为 value。通过将 value 定义为 `[]int` 类型或者其他类型的切片,就可以优雅地解决这个问题。
|
||||
|
||||
这里有一些定义这种 map 的例子:
|
||||
|
||||
|
@@ -8,7 +8,7 @@ for key, value := range map1 {
|
||||
}
|
||||
```
|
||||
|
||||
第一个返回值 key 是 map 中的 key 值,第二个返回值则是该 key 对应的 value 值;这两个都是仅 for 循环内部可见的局部变量。其中第一个返回值key值是一个可选元素。如果你只关心值,可以这么使用:
|
||||
第一个返回值 key 是 map 中的 key 值,第二个返回值则是该 key 对应的 value 值;这两个都是仅 for 循环内部可见的局部变量。其中第一个返回值 key 值是一个可选元素。如果你只关心值,可以这么使用:
|
||||
|
||||
```go
|
||||
for _, value := range map1 {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# 8.4 map 类型的切片
|
||||
|
||||
假设我们想获取一个 map 类型的切片,我们必须使用两次 `make()` 函数,第一次分配切片,第二次分配 切片中每个 map 元素(参见下面的例子 8.4)。
|
||||
假设我们想获取一个 map 类型的切片,我们必须使用两次 `make()` 函数,第一次分配切片,第二次分配切片中每个 map 元素(参见下面的例子 8.4)。
|
||||
|
||||
示例 8.4 [maps_forrange2.go](examples/chapter_8/maps_forrange2.go):
|
||||
|
||||
|
@@ -50,7 +50,7 @@ func main() {
|
||||
sorted:
|
||||
Key: alpha, Value: 34 / Key: bravo, Value: 56 / Key: charlie, Value: 23 / Key: delta, Value: 87 / Key: echo, Value: 56 / Key: foxtrot, Value: 12 / Key: golf, Value: 34 / Key: hotel, Value: 16 / Key: indio, Value: 87 / Key: juliet, Value: 65 / Key: kili, Value: 43 / Key: lima, Value: 98 /
|
||||
|
||||
但是如果你想要一个排序的列表你最好使用结构体切片,这样会更有效:
|
||||
但是如果你想要一个排序的列表,那么最好使用结构体切片,这样会更有效:
|
||||
|
||||
```go
|
||||
type name struct {
|
||||
|
@@ -6,7 +6,7 @@
|
||||
|
||||
- `unsafe`: 包含了一些打破 Go 语言“类型安全”的命令,一般的程序中不会被使用,可用在 C/C++ 程序的调用中。
|
||||
- `syscall`-`os`-`os/exec`:
|
||||
- `os`: 提供给我们一个平台无关性的操作系统功能接口,采用类Unix设计,隐藏了不同操作系统间差异,让不同的文件系统和操作系统对象表现一致。
|
||||
- `os`: 提供给我们一个平台无关性的操作系统功能接口,采用类 Unix 设计,隐藏了不同操作系统间的差异,让不同的文件系统和操作系统对象表现一致。
|
||||
- `os/exec`: 提供我们运行外部操作系统命令和程序的方式。
|
||||
- `syscall`: 底层的外部包,提供了操作系统底层调用的基本接口。
|
||||
|
||||
@@ -32,7 +32,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
- `archive/tar` 和 `/zip-compress`:压缩(解压缩)文件功能。
|
||||
- `archive/tar` 和 `/zip-compress`:压缩(解压缩)文件功能。
|
||||
- `fmt`-`io`-`bufio`-`path/filepath`-`flag`:
|
||||
- `fmt`: 提供了格式化输入输出功能。
|
||||
- `io`: 提供了基本输入输出功能,大多数是围绕系统功能的封装。
|
||||
@@ -66,10 +66,10 @@ for e := l.Front(); e != nil; e = e.Next() {
|
||||
|
||||
- `time`-`log`:
|
||||
- `time`: 日期和时间的基本操作。
|
||||
- `log`: 记录程序运行时产生的日志,我们将在后面的章节使用它。
|
||||
- `log`: 记录程序运行时产生的日志,我们将在后面的章节使用它。
|
||||
- `encoding/json`-`encoding/xml`-`text/template`:
|
||||
- `encoding/json`: 读取并解码和写入并编码 JSON 数据。
|
||||
- `encoding/xml`:简单的 XML1.0 解析器,有关 JSON 和 XML 的实例请查阅第 12.9/10 章节。
|
||||
- `encoding/xml`: 简单的 XML1.0 解析器,有关 JSON 和 XML 的实例请查阅第 12.9/10 章节。
|
||||
- `text/template`:生成像 HTML 一样的数据与文本混合的数据驱动模板(参见第 15.7 节)。
|
||||
- `net`-`net/http`-`html`:(参见第 15 章)
|
||||
- `net`: 网络数据的基本操作。
|
||||
|
@@ -1,12 +1,12 @@
|
||||
# 9.11 在 Go 程序中使用外部库
|
||||
|
||||
(本节我们将创建一个 Web 应用和它的 Google App Engine 版本,在第 19 和 21 章分别说明,当你阅读到这些章节时可以再回到这个例子。)
|
||||
(本节我们将创建一个 Web 应用和它的 Google App Engine 版本,在第 19 和 21 章分别说明,当你阅读到这些章节时可以再回到这个例子。)
|
||||
|
||||
当开始一个新项目或增加新的功能到现有的项目,你可以通过在应用程序中使用已经存在的库来节省开发时间。为了做到这一点,你必须理解库的 API(应用编程接口),那就是:库中有哪些方法可以调用,如何调用。你可能没有这个库的源代码,但作者肯定有记载的 API 以及详细介绍了如何使用它。
|
||||
|
||||
作为一个例子,我们将使用谷歌的 API 的 urlshortener 编写一个小程序:你可以尝试一下在 http://goo.gl/ 输入一个像 "http://www.destandaard.be" 这样的URL,你会看到一个像 "http://goo.gl/O9SUO" 这样更短的 URL 返回,也就是说,在 Twitter 之类的服务中这是非常容易嵌入的。谷歌 urlshortener 服务的文档可以在 "http://code.google.com/apis/urlshortener/" 找到。(第 19 章,我们将开发自己版本的 urlshortener)。
|
||||
作为一个例子,我们将使用谷歌的 API 的 urlshortener 编写一个小程序:你可以尝试一下在 http://goo.gl/ 输入一个像 "http://www.destandaard.be" 这样的 URL,你会看到一个像 "http://goo.gl/O9SUO" 这样更短的 URL 返回,也就是说,在 Twitter 之类的服务中这是非常容易嵌入的。谷歌 urlshortener 服务的文档可以在 "http://code.google.com/apis/urlshortener/" 找到。(第 19 章,我们将开发自己版本的 urlshortener)。
|
||||
|
||||
谷歌将这项技术提供给其他开发者,作为 API 我们可以在我们自己的应用程序中调用(释放到指定的限制)。他们也生成了一个 Go 语言客户端库使其变得更容易。
|
||||
谷歌将这项技术提供给其他开发者,我们可以在我们自己的应用程序中调用 API (释放到指定的限制)。他们也生成了一个 Go 语言客户端库使调用变得更容易。
|
||||
|
||||
备注:谷歌让通过使用 Google API Go 客户端服务的开发者生活变得更简单,Go 客户端程序自动生成于 Google 库的 JSON 描述。更多详情在 [项目页面](http://code.google.com/p/google-api-go-client/) 查看。
|
||||
|
||||
@@ -24,7 +24,7 @@ go install 将下载源码,编译并安装包
|
||||
|
||||
import "google.golang.org/api/urlshortener/v1"
|
||||
|
||||
现在我们写一个 Web 应用(参见第 15 章 4-8 节)通过表单实现短地址和长地址的相互转换。我们将使用 `template` 包并写三个处理函数:root 函数通过执行表单模板来展示表单。short 函数将长地址转换为短地址,long 函数逆向转换。
|
||||
现在我们写一个 Web 应用(参见第 15 章 4-8 节)通过表单实现短地址和长地址的相互转换。我们将使用 `template` 包并写三个处理函数:root 函数通过执行表单模板来展示表单,short 函数将长地址转换为短地址,long 函数逆向转换。
|
||||
|
||||
要调用 urlshortener 接口必须先通过 http 包中的默认客户端创建一个服务实例 urlshortenerSvc:
|
||||
```go
|
||||
@@ -39,7 +39,7 @@ url, _ := urlshortenerSvc.Url.Insert(&urlshortener.Url{LongUrl: longUrl}).Do()
|
||||
|
||||
返回 `url` 的 `Id` 便是我们需要的短地址。
|
||||
|
||||
我们通过调用服务中的 `Url.Get` 中的 `Do` 方法传入包含短地址的Url数据结构从而获取长地址:
|
||||
我们通过调用服务中的 `Url.Get` 中的 `Do` 方法传入包含短地址的 Url 数据结构从而获取长地址:
|
||||
|
||||
```go
|
||||
url, error := urlshortenerSvc.Url.Get(shwortUrl).Do()
|
||||
@@ -138,7 +138,7 @@ handlers:
|
||||
script: _go_app
|
||||
```
|
||||
|
||||
现在你可以去到你的项目目录并在终端运行:`dev_appserver.py urlshort`
|
||||
现在你可以到你的项目目录并在终端运行:`dev_appserver.py urlshort`
|
||||
|
||||
在浏览器打开你的 Web应用:http://localhost:8080。
|
||||
|
||||
|
@@ -10,7 +10,7 @@
|
||||
ok, _ := regexp.Match(pat, []byte(searchIn))
|
||||
```
|
||||
|
||||
变量 ok 将返回 true 或者 false,我们也可以使用 `MatchString`:
|
||||
变量 ok 将返回 true 或者 false,我们也可以使用 `MatchString`:
|
||||
|
||||
```go
|
||||
ok, _ := regexp.MatchString(pat, searchIn)
|
||||
|
@@ -1,10 +1,10 @@
|
||||
# 9.3 锁和 sync 包
|
||||
|
||||
在一些复杂的程序中,通常通过不同线程执行不同应用来实现程序的并发。当不同线程要使用同一个变量时,经常会出现一个问题:无法预知变量被不同线程修改的顺序!(这通常被称为资源竞争,指不同线程对同一变量使用的竞争)显然这无法让人容忍,那我们该如何解决这个问题呢?
|
||||
在一些复杂的程序中,通常通过不同线程执行不同应用来实现程序的并发。当不同线程要使用同一个变量时,经常会出现一个问题:无法预知变量被不同线程修改的顺序!(这通常被称为资源竞争,指不同线程对同一变量使用的竞争)显然这无法让人容忍,那我们该如何解决这个问题呢?
|
||||
|
||||
经典的做法是一次只能让一个线程对共享变量进行操作。当变量被一个线程改变时(临界区),我们为它上锁,直到这个线程执行完成并解锁后,其他线程才能访问它。
|
||||
经典的做法是一次只能让一个线程对共享变量进行操作。当变量被一个线程改变时(临界区),我们为它上锁,直到这个线程执行完成并解锁后,其他线程才能访问它。
|
||||
|
||||
特别是我们之前章节学习的 map 类型是不存在锁的机制来实现这种效果(出于对性能的考虑),所以 map 类型是非线程安全的。当并行访问一个共享的 map 类型的数据,map 数据将会出错。
|
||||
特别是我们之前章节学习的 map 类型是不存在锁的机制来实现这种效果(出于对性能的考虑),所以 map 类型是非线程安全的。当并行访问一个共享的 map 类型的数据,map 数据将会出错。
|
||||
|
||||
在 Go 语言中这种锁的机制是通过 sync 包中 Mutex 来实现的。sync 来源于 "synchronized" 一词,这意味着线程将有序的对同一变量进行访问。
|
||||
|
||||
@@ -42,7 +42,7 @@ type SyncedBuffer struct {
|
||||
}
|
||||
```
|
||||
|
||||
在 sync 包中还有一个 `RWMutex` 锁:他能通过 `RLock()` 来允许同一时间多个线程对变量进行读操作,但是只能一个线程进行写操作。如果使用 `Lock()` 将和普通的 `Mutex` 作用相同。包中还有一个方便的 `Once` 类型变量的方法 `once.Do(call)`,这个方法确保被调用函数只能被调用一次。
|
||||
在 sync 包中还有一个 `RWMutex` 锁:它能通过 `RLock()` 来允许同一时间多个线程对变量进行读操作,但是只能一个线程进行写操作。如果使用 `Lock()` 将和普通的 `Mutex` 作用相同。包中还有一个方便的 `Once` 类型变量的方法 `once.Do(call)`,这个方法确保被调用函数只能被调用一次。
|
||||
|
||||
相对简单的情况下,通过使用 sync 包可以解决同一时间只能一个线程访问变量或 map 类型数据的问题。如果这种方式导致程序明显变慢或者引起其他问题,我们要重新思考来通过 goroutines 和 channels 来解决问题,这是在 Go 语言中所提倡用来实现并发的技术。我们将在第 14 章对其深入了解,并在第 14.7 节中对这两种方式进行比较。
|
||||
|
||||
|
@@ -2,9 +2,9 @@
|
||||
|
||||
包是 Go 语言中代码组织和代码编译的主要方式。关于它们的很多基本信息已经在 4.2 章节中给出,最引人注目的便是可见性。现在我们来看看具体如何来使用自己写的包。在下一节,我们将回顾一些标准库中的包,自定义的包和标准库以外的包。
|
||||
|
||||
当写自己包的时候,要使用短小的不含有 `_`(下划线)的小写单词来为文件命名。这里有个简单例子来说明包是如何相互调用以及可见性是如何实现的。
|
||||
当写自己包的时候,要使用短小的不含有 `_`(下划线)的小写单词来为文件命名。这里有个简单例子来说明包是如何相互调用以及可见性是如何实现的。
|
||||
|
||||
当前目录下(examples/chapter_9/book/)有一个名为 package_mytest.go 的程序, 它使用了自定义包 pack1 中 pack1.go 的代码。这段程序(连同编译链接生成的 pack1.a)存放在当前目录下一个名为 pack1 的文件夹下。所以链接器将包的对象和主程序对象链接在一起。
|
||||
当前目录下(examples/chapter_9/book/)有一个名为 package_mytest.go 的程序, 它使用了自定义包 pack1 中 pack1.go 的代码。这段程序(连同编译链接生成的 pack1.a)存放在当前目录下一个名为 pack1 的文件夹下。所以链接器将包的对象和主程序对象链接在一起。
|
||||
|
||||
示例 9.4 [pack1.go](examples/chapter_9/book/pack1/pack1.go):
|
||||
|
||||
@@ -74,13 +74,13 @@ fmt.Printf("Float from package1: %f\n", pack1.pack1Float)
|
||||
|
||||
主程序利用的包必须在主程序编写之前被编译。主程序中每个 pack1 项目都要通过包名来使用:`pack1.Item`。具体使用方法请参见示例 4.6 和 4.7。
|
||||
|
||||
因此,按照惯例,子目录和包之间有着密切的联系:为了区分,不同包存放在不同的目录下,每个包(所有属于这个包中的 go 文件)都存放在和包名相同的子目录下:
|
||||
因此,按照惯例,子目录和包之间有着密切的联系:为了区分,不同包存放在不同的目录下,每个包(所有属于这个包中的 go 文件)都存放在和包名相同的子目录下:
|
||||
|
||||
Import with `.` :
|
||||
|
||||
import . "./pack1"
|
||||
|
||||
当使用`.`来做为包的别名时,你可以不通过包名来使用其中的项目。例如:`test := ReturnStr()`。
|
||||
当使用 `.` 作为包的别名时,你可以不通过包名来使用其中的项目。例如:`test := ReturnStr()`。
|
||||
|
||||
在当前的命名空间导入 pack1 包,一般是为了具有更好的测试效果。
|
||||
|
||||
@@ -88,7 +88,7 @@ Import with `_` :
|
||||
|
||||
import _ "./pack1/pack1"
|
||||
|
||||
pack1包只导入其副作用,也就是说,只执行它的init函数并初始化其中的全局变量。
|
||||
pack1 包只导入其副作用,也就是说,只执行它的 init 函数并初始化其中的全局变量。
|
||||
|
||||
**导入外部安装包:**
|
||||
|
||||
@@ -133,11 +133,11 @@ init 函数是不能被调用的。
|
||||
|
||||
通过 `chmod 777 ./Makefile`确保它的可执行性。
|
||||
|
||||
上面脚本内的include语引入了相应的功能,将自动检测机器的架构并调用正确的编译器和链接器。
|
||||
上面脚本内的 include 语句引入了相应的功能,将自动检测机器的架构并调用正确的编译器和链接器。
|
||||
|
||||
然后终端执行 make 或 `gomake` 工具:他们都会生成一个包含静态库 pack1.a 的 _obj 目录。
|
||||
|
||||
go install(参见第 9.7 节,从 Go1 的首选方式)同样复制 pack1.a 到本地的 $GOROOT/pkg 的目录中一个以操作系统为名的子目录下。像 `import "pack1"` 代替 `import "path to pack1"`,这样只通过名字就可以将包在程序中导入。
|
||||
go install(参见第 9.7 节,从 Go1 的首选方式)同样复制 pack1.a 到本地的 $GOROOT/pkg 的目录中一个以操作系统为名的子目录下。像 `import "pack1"` 代替 `import "path to pack1"`,这样只通过名字就可以将包在程序中导入。
|
||||
|
||||
当第 13 章我们遇到使用测试工具进行测试的时候我们将重新回到自己的包的制作和编译这个话题。
|
||||
|
||||
@@ -153,7 +153,7 @@ b)一个源文件是否能包含多个包?
|
||||
|
||||
在同一个包中创建一个 `IsAM` 函数返回一个布尔值用来判断当前时间是 AM 还是 PM,同样创建 `IsAfternoon` 和 `IsEvening` 函数。
|
||||
|
||||
使用 main_greetings 作出合适的问候(提示:使用 time 包)。
|
||||
使用 main_greetings 作出合适的问候(提示:使用 time 包)。
|
||||
|
||||
**练习 9.4** 创建一个程序 main_oddven.go 判断前 100 个整数是不是偶数,将判断所用的函数编写在 even 包里。
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# 9.6 为自定义包使用 godoc
|
||||
|
||||
godoc工具(第 3.6 节)在显示自定义包中的注释也有很好的效果:注释必须以 `//` 开始并无空行放在声明(包,类型,函数)前。godoc 会为每个文件生成一系列的网页。
|
||||
godoc 工具(第 3.6 节)在显示自定义包中的注释也有很好的效果:注释必须以 `//` 开始并无空行放在声明(包,类型,函数)前。godoc 会为每个文件生成一系列的网页。
|
||||
|
||||
例如:
|
||||
|
||||
|
@@ -4,9 +4,9 @@ go install 是 Go 中自动包安装工具:如需要将包安装到本地它
|
||||
|
||||
在包安装前的先决条件是要自动处理包自身依赖关系的安装。被依赖的包也会安装到子目录下,但是没有文档和示例:可以到网上浏览。
|
||||
|
||||
go install 使用了 GOPATH 变量(详见第 2.2 节)。
|
||||
go install 使用了 GOPATH 变量(详见第 2.2 节)。
|
||||
|
||||
远端包(详见第 9.5 节):
|
||||
远端包(详见第 9.5 节):
|
||||
|
||||
假设我们要安装一个有趣的包 tideland(它包含了许多帮助示例,参见 [项目主页](http://code.google.com/p/tideland-cgl))。
|
||||
|
||||
|
@@ -4,16 +4,16 @@
|
||||
|
||||
## 9.8.1 自定义包的目录结构
|
||||
|
||||
下面的结构给了你一个好的示范(uc 代表通用包名, 名字为粗体的代表目录,斜体代表可执行文件):
|
||||
下面的结构给了你一个好的示范(uc 代表通用包名, 名字为粗体的代表目录,斜体代表可执行文件):
|
||||
|
||||
/home/user/goprograms
|
||||
ucmain.go (uc包主程序)
|
||||
Makefile (ucmain的makefile)
|
||||
ucmain.go (uc 包主程序)
|
||||
Makefile (ucmain 的 makefile)
|
||||
ucmain
|
||||
src/uc (包含uc包的go源码)
|
||||
src/uc (包含 uc 包的 go 源码)
|
||||
uc.go
|
||||
uc_test.go
|
||||
Makefile (包的makefile)
|
||||
Makefile (包的 makefile)
|
||||
uc.a
|
||||
_obj
|
||||
uc.a
|
||||
@@ -90,7 +90,7 @@ include $(GOROOT)/src/Make.pkg
|
||||
|
||||
在第 13.8 节我们将给出另外一个测试例子并进行深入研究。
|
||||
|
||||
备注:有可能你当前的用户不具有足够的资格使用 go install(没有权限)。这种情况下,选择 root 用户 su。确保 Go 环境变量和 Go 源码路径也设置给 su,同样也适用你的普通用户(详见第 2.3 节)。
|
||||
备注:有可能你当前的用户不具有足够的资格使用 go install(没有权限)。这种情况下,选择 root 用户 su。确保 Go 环境变量和 Go 源码路径也设置给 su,同样也适用你的普通用户(详见第 2.3 节)。
|
||||
|
||||
接下来我们创建主程序 ucmain.go:
|
||||
|
||||
@@ -122,7 +122,7 @@ GOFILES=\
|
||||
include $(GOROOT)/src/Make.cmd
|
||||
```
|
||||
|
||||
执行 gomake 编译 `ucmain.go` 生成可执行文件ucmain
|
||||
执行 gomake 编译 `ucmain.go` 生成可执行文件 ucmain
|
||||
|
||||
运行 `./ucmain` 显示: `USING PACKAGE UC!`。
|
||||
|
||||
@@ -130,11 +130,11 @@ include $(GOROOT)/src/Make.cmd
|
||||
|
||||
本地包在用户目录下,使用给出的目录结构,以下命令用来从源码安装本地包:
|
||||
|
||||
go install /home/user/goprograms/src/uc # 编译安装uc
|
||||
go install /home/user/goprograms/src/uc # 编译安装 uc
|
||||
cd /home/user/goprograms/uc
|
||||
go install ./uc # 编译安装uc(和之前的指令一样)
|
||||
go install ./uc # 编译安装 uc(和之前的指令一样)
|
||||
cd ..
|
||||
go install . # 编译安装ucmain
|
||||
go install . # 编译安装 ucmain
|
||||
|
||||
安装到 `$GOPATH` 下:
|
||||
|
||||
|
@@ -21,7 +21,7 @@
|
||||
|
||||
如果您还没有账号,可以去注册一个开源项目的免费帐号。输入正确的帐号密码和有效的邮箱地址并进一步创建用户。然后你将获得一个 Git 命令的列表。本地仓库的操作命令已经完成。一个优秀的系统在你遇到任何问题的时候将 [引导你](http://help.github.com/)。
|
||||
|
||||
在云端创建一个新的 uc 仓库;发布的指令为(`NNNN` 替代用户名):
|
||||
在云端创建一个新的 uc 仓库;发布的指令为(`NNNN` 替代用户名):
|
||||
|
||||
```
|
||||
git remote add origin git@github.com:NNNN/uc.git
|
||||
@@ -49,7 +49,7 @@ Gomake(和 go install)将通过 `$GOPATH` 下的本地版本进行工作。
|
||||
- Google Code(hg/Git/svn)
|
||||
- Launchpad(bzr)
|
||||
|
||||
版本控制系统可以选择你熟悉的或者本地使用的代码版本控制。Go 核心代码的仓库是使用 Mercurial(hg) 来控制的,所以它是一个最可能保证你可以得到开发者项目中最好的软件。Git 也很出名,同样也适用。如果你从未使用过版本控制,这些网站有一些很好的帮助并且你可以通过在谷歌搜索 "{name} tutorial",(name为你想要使用的版本控制系统),得到许多很好的教程。
|
||||
版本控制系统可以选择你熟悉的或者本地使用的代码版本控制。Go 核心代码的仓库是使用 Mercurial(hg) 来控制的,所以它是一个最可能保证你可以得到开发者项目中最好的软件。Git 也很出名,同样也适用。如果你从未使用过版本控制,这些网站有一些很好的帮助并且你可以通过在谷歌搜索 "{name} tutorial"(name为你想要使用的版本控制系统)得到许多很好的教程。
|
||||
|
||||
## 链接
|
||||
|
||||
|
@@ -35,7 +35,7 @@ t = new(T)
|
||||
|
||||
写这条语句的惯用方法是:`t := new(T)`,变量 `t` 是一个指向 `T`的指针,此时结构体字段的值是它们所属类型的零值。
|
||||
|
||||
声明 `var t T` 也会给 `t` 分配内存,并零值化内存,但是这个时候 `t` 是类型T。在这两种方式中,`t` 通常被称做类型 T 的一个实例(instance)或对象(object)。
|
||||
声明 `var t T` 也会给 `t` 分配内存,并零值化内存,但是这个时候 `t` 是类型 T 。在这两种方式中,`t` 通常被称做类型 T 的一个实例(instance)或对象(object)。
|
||||
|
||||
示例 10.1 [structs_fields.go](examples/chapter_10/structs_fields.go) 给出了一个非常简单的例子:
|
||||
|
||||
@@ -79,8 +79,8 @@ func main() {
|
||||
|
||||
```go
|
||||
type myStruct struct { i int }
|
||||
var v myStruct // v是结构体类型变量
|
||||
var p *myStruct // p是指向一个结构体类型变量的指针
|
||||
var v myStruct // v 是结构体类型变量
|
||||
var p *myStruct // p 是指向一个结构体类型变量的指针
|
||||
v.i
|
||||
p.i
|
||||
```
|
||||
@@ -89,7 +89,7 @@ p.i
|
||||
|
||||
```go
|
||||
ms := &struct1{10, 15.5, "Chris"}
|
||||
// 此时ms的类型是 *struct1
|
||||
// 此时 ms 的类型是 *struct1
|
||||
```
|
||||
|
||||
或者:
|
||||
@@ -186,7 +186,7 @@ func main() {
|
||||
The name of the person is CHRIS WOODWARD
|
||||
The name of the person is CHRIS WOODWARD
|
||||
|
||||
在上面例子的第二种情况中,可以直接通过指针,像 `pers2.lastName="Woodward"` 这样给结构体字段赋值,没有像 C++ 中那样需要使用 `->` 操作符,Go 会自动做这样的转换。
|
||||
在上面例子的第二种情况中,可以直接通过指针,像 `pers2.lastName = "Woodward"` 这样给结构体字段赋值,没有像 C++ 中那样需要使用 `->` 操作符,Go 会自动做这样的转换。
|
||||
|
||||
注意也可以通过解指针的方式来设置值:`(*pers2).lastName = "Woodward"`
|
||||
|
||||
|
@@ -35,9 +35,9 @@ f := NewFile(10, "./test.txt")
|
||||
|
||||
这可以和大多数面向对象编程语言中笨拙的初始化方式做个比较:`File f = new File(...)`。
|
||||
|
||||
我们可以说是工厂实例化了类型的一个对象,就像在基于类的OO语言中那样。
|
||||
我们可以说是工厂实例化了类型的一个对象,就像在基于类的 OO 语言中那样。
|
||||
|
||||
如果想知道结构体类型T的一个实例占用了多少内存,可以使用:`size := unsafe.Sizeof(T{})`。
|
||||
如果想知道结构体类型 T 的一个实例占用了多少内存,可以使用:`size := unsafe.Sizeof(T{})`。
|
||||
|
||||
**如何强制使用工厂方法**
|
||||
|
||||
@@ -74,7 +74,7 @@ new 和 make 这两个内置函数已经在第 [7.2.4](07.2.md) 节通过切片
|
||||
|
||||
下面的例子说明了在映射上使用 new 和 make 的区别以及可能发生的错误:
|
||||
|
||||
示例 10.4 new_make.go(不能编译)
|
||||
示例 10.4 [new_make.go](examples/chapter_10/new_make.go)(不能编译)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# 10.4 带标签的结构体
|
||||
|
||||
结构体中的字段除了有名字和类型外,还可以有一个可选的标签(tag):它是一个附属于字段的字符串,可以是文档或其他的重要标记。标签的内容不可以在一般的编程中使用,只有包 `reflect` 能获取它。我们将在下一章(第 [11.10 节](11.10.md))中深入的探讨 `reflect`包,它可以在运行时自省类型、属性和方法,比如:在一个变量上调用 `reflect.TypeOf()` 可以获取变量的正确类型,如果变量是一个结构体类型,就可以通过 Field 来索引结构体的字段,然后就可以使用 Tag 属性。
|
||||
结构体中的字段除了有名字和类型外,还可以有一个可选的标签(tag):它是一个附属于字段的字符串,可以是文档或其他的重要标记。标签的内容不可以在一般的编程中使用,只有包 `reflect` 能获取它。我们将在下一章(第 [11.10 节](11.10.md))中深入的探讨 `reflect` 包,它可以在运行时自省类型、属性和方法,比如:在一个变量上调用 `reflect.TypeOf()` 可以获取变量的正确类型,如果变量是一个结构体类型,就可以通过 Field 来索引结构体的字段,然后就可以使用 Tag 属性。
|
||||
|
||||
示例 10.7 [struct_tag.go](examples/chapter_10/struct_tag.go):
|
||||
|
||||
|
@@ -41,7 +41,7 @@ func (_ receiver_type) methodName(parameter_list) (return_value_list) { ... }
|
||||
|
||||
`recv` 就像是面向对象语言中的 `this` 或 `self`,但是 Go 中并没有这两个关键字。随个人喜好,你可以使用 `this` 或 `self` 作为 receiver 的名字。下面是一个结构体上的简单方法的例子:
|
||||
|
||||
示例 10.10 method .go:
|
||||
示例 10.10 [method1 .go](examples/chapter_10/method1.go):
|
||||
|
||||
```go
|
||||
package main
|
||||
@@ -82,7 +82,7 @@ func (tn *TwoInts) AddToParam(param int) int {
|
||||
|
||||
下面是非结构体类型上方法的例子:
|
||||
|
||||
示例 10.11 method2.go:
|
||||
示例 10.11 [method2.go](examples/chapter_10/method2.go):
|
||||
|
||||
```go
|
||||
package main
|
||||
@@ -143,7 +143,7 @@ func (t time.Time) first3Chars() string {
|
||||
|
||||
但是有一个间接的方式:可以先定义该类型(比如:int 或 float)的别名类型,然后再为别名类型定义方法。或者像下面这样将它作为匿名类型嵌入在一个新的结构体中。当然方法只在这个别名类型上有效。
|
||||
|
||||
示例 10.12 method_on_time.go:
|
||||
示例 10.12 [method_on_time.go](examples/chapter_10/method_on_time.go):
|
||||
|
||||
```go
|
||||
package main
|
||||
@@ -162,9 +162,9 @@ func (t myTime) first3Chars() string {
|
||||
}
|
||||
func main() {
|
||||
m := myTime{time.Now()}
|
||||
// 调用匿名Time上的String方法
|
||||
// 调用匿名 Time 上的 String 方法
|
||||
fmt.Println("Full time now:", m.String())
|
||||
// 调用myTime.first3Chars
|
||||
// 调用 myTime.first3Chars
|
||||
fmt.Println("First 3 chars:", m.first3Chars())
|
||||
}
|
||||
|
||||
@@ -198,9 +198,9 @@ First 3 chars: Mon
|
||||
|
||||
如果想要方法改变接收者的数据,就在接收者的指针类型上定义该方法。否则,就在普通的值类型上定义方法。
|
||||
|
||||
下面的例子 `pointer_value.go` 作了说明:`change()`接受一个指向 B 的指针,并改变它内部的成员;`write()` 通过拷贝接受 B 的值并只输出B的内容。注意 Go 为我们做了探测工作,我们自己并没有指出是否在指针上调用方法,Go 替我们做了这些事情。b1 是值而 b2 是指针,方法都支持运行了。
|
||||
下面的例子 `pointer_value.go` 作了说明:`change()`接受一个指向 B 的指针,并改变它内部的成员;`write()` 通过拷贝接受 B 的值并只输出 B 的内容。注意 Go 为我们做了探测工作,我们自己并没有指出是否在指针上调用方法,Go 替我们做了这些事情。b1 是值而 b2 是指针,方法都支持运行了。
|
||||
|
||||
示例 10.13 pointer_value.go:
|
||||
示例 10.13 [pointer_value.go](examples/chapter_10/pointer_value.go):
|
||||
|
||||
```go
|
||||
package main
|
||||
@@ -218,11 +218,11 @@ func (b *B) change() { b.thing = 1 }
|
||||
func (b B) write() string { return fmt.Sprint(b) }
|
||||
|
||||
func main() {
|
||||
var b1 B // b1是值
|
||||
var b1 B // b1 是值
|
||||
b1.change()
|
||||
fmt.Println(b1.write())
|
||||
|
||||
b2 := new(B) // b2是指针
|
||||
b2 := new(B) // b2 是指针
|
||||
b2.change()
|
||||
fmt.Println(b2.write())
|
||||
}
|
||||
@@ -233,7 +233,7 @@ func main() {
|
||||
*/
|
||||
```
|
||||
|
||||
试着在 `write()` 中改变接收者b的值:将会看到它可以正常编译,但是开始的 b 没有被改变。
|
||||
试着在 `write()` 中改变接收者 b 的值:将会看到它可以正常编译,但是开始的 b 没有被改变。
|
||||
|
||||
我们知道方法将指针作为接收者不是必须的,如下面的例子,我们只是需要 `Point3` 的值来做计算:
|
||||
|
||||
@@ -251,7 +251,7 @@ func (p Point3) Abs() float64 {
|
||||
|
||||
可以使用 `p3.Abs()` 来替代 `(*p3).Abs()`。
|
||||
|
||||
像例子 10.10(method1.go)中接收者类型是 `*TwoInts` 的方法 `AddThem()`,它能在类型 `TwoInts` 的值上被调用,这是自动间接发生的。
|
||||
像例子 10.10([method1.go](examples/chapter_10/method1.go))中接收者类型是 `*TwoInts` 的方法 `AddThem()`,它能在类型 `TwoInts` 的值上被调用,这是自动间接发生的。
|
||||
|
||||
因此 `two2.AddThem` 可以替代 `(&two2).AddThem()`。
|
||||
|
||||
@@ -263,7 +263,7 @@ func (p Point3) Abs() float64 {
|
||||
|
||||
**指针方法和值方法都可以在指针或非指针上被调用**,如下面程序所示,类型 `List` 在值上有一个方法 `Len()`,在指针上有一个方法 `Append()`,但是可以看到两个方法都可以在两种类型的变量上被调用。
|
||||
|
||||
示例 10.14 methodset1.go:
|
||||
示例 10.14 [methodset1.go](examples/chapter_10/methodset1.go):
|
||||
|
||||
```go
|
||||
package main
|
||||
@@ -296,7 +296,7 @@ func main() {
|
||||
|
||||
这可以通过面向对象语言一个众所周知的技术来完成:提供 getter 和 setter 方法。对于 setter 方法使用 Set 前缀,对于 getter 方法只使用成员名。
|
||||
|
||||
示例 10.15 person2.go:
|
||||
示例 10.15 [person2.go](examples/chapter_10/person2.go):
|
||||
|
||||
```go
|
||||
package person
|
||||
@@ -315,7 +315,7 @@ func (p *Person) SetFirstName(newName string) {
|
||||
}
|
||||
```
|
||||
|
||||
示例 10.16—use_person2.go:
|
||||
示例 10.16—[use_person2.go](examples/chapter_10/use_person2.go):
|
||||
|
||||
```go
|
||||
package main
|
||||
@@ -368,7 +368,7 @@ func (c *Car) GoToWorkIn() {
|
||||
}
|
||||
```
|
||||
|
||||
下面是 `method3.go` 的完整例子,它展示了内嵌结构体上的方法可以直接在外层类型的实例上调用:
|
||||
下面是 [method3.go](examples/chapter_10/method3.go) 的完整例子,它展示了内嵌结构体上的方法可以直接在外层类型的实例上调用:
|
||||
|
||||
```go
|
||||
package main
|
||||
@@ -393,15 +393,15 @@ type NamedPoint struct {
|
||||
|
||||
func main() {
|
||||
n := &NamedPoint{Point{3, 4}, "Pythagoras"}
|
||||
fmt.Println(n.Abs()) // 打印5
|
||||
fmt.Println(n.Abs()) // 打印 5
|
||||
}
|
||||
```
|
||||
|
||||
内嵌将一个已存在类型的字段和方法注入到了另一个类型里:匿名字段上的方法“晋升”成为了外层类型的方法。当然类型可以有只作用于本身实例而不作用于内嵌“父”类型上的方法,
|
||||
内嵌将一个已存在类型的字段和方法注入到了另一个类型里:匿名字段上的方法“晋升”成为了外层类型的方法。当然类型可以有只作用于本身实例而不作用于内嵌“父”类型上的方法。
|
||||
|
||||
可以覆写方法(像字段一样):和内嵌类型方法具有同样名字的外层类型的方法会覆写内嵌类型对应的方法。
|
||||
|
||||
在示例 10.18 method4.go 中添加:
|
||||
在示例 10.18 [method4.go](examples/chapter_10/method4.go) 中添加:
|
||||
|
||||
```go
|
||||
func (n *NamedPoint) Abs() float64 {
|
||||
@@ -435,7 +435,7 @@ B:内嵌:内嵌(匿名地)所需功能类型,像前一节 10.6.5 所
|
||||
|
||||
方式 A 可以通过如下方法实现(使用了第 10.7 节中的 `String()` 功能):
|
||||
|
||||
示例 10.19 embed_func1.go:
|
||||
示例 10.19 [embed_func1.go](examples/chapter_10/embed_func1.go):
|
||||
|
||||
```go
|
||||
package main
|
||||
@@ -485,7 +485,7 @@ func (c *Customer) Log() *Log {
|
||||
1 - Yes we can!
|
||||
2 - After me the world will be a better place!
|
||||
|
||||
相对的方式 B 可能会像这样:
|
||||
相对的方式 B 可能会像这样([embed_func2.go](examples/chapter_10/embed_func2.go)):
|
||||
|
||||
```go
|
||||
package main
|
||||
@@ -541,7 +541,7 @@ func (c *Customer) String() string {
|
||||
|
||||
作为一个例子,假设有一个类型 `CameraPhone`,通过它可以 `Call()`,也可以 `TakeAPicture()`,但是第一个方法属于类型 `Phone`,第二个方法属于类型 `Camera`。
|
||||
|
||||
只要嵌入这两个类型就可以解决这个问题,如下所示:
|
||||
只要嵌入这两个类型就可以解决这个问题,如下所示([mult_inheritance.go](examples/chapter_10/mult_inheritance.go)):
|
||||
|
||||
```go
|
||||
package main
|
||||
|
@@ -4,7 +4,7 @@
|
||||
|
||||
我们使用第 10.4 节中程序的类型来进行测试:
|
||||
|
||||
示例 10.22 method_string.go:
|
||||
示例 10.22 [method_string.go](examples/chapter_10/method_string.go):
|
||||
|
||||
```go
|
||||
package main
|
||||
@@ -88,7 +88,7 @@ type T struct {
|
||||
|
||||

|
||||
|
||||
它的格子包含数据,比如整数 i、j、k 和 l 等等,格子从底部(索引 0)至顶部(索引 n)来索引。这个例子中假定 `n=3`,那么一共有 4 个格子。
|
||||
它的格子包含数据,比如整数 i、j、k 和 l 等等,格子从底部(索引 0)至顶部(索引 n)来索引。这个例子中假定 `n = 3`,那么一共有 4 个格子。
|
||||
|
||||
一个新栈中所有格子的值都是 0。
|
||||
|
||||
|
@@ -205,7 +205,7 @@ func main() {
|
||||
value := reflect.ValueOf(secret) // <main.NotknownType Value>
|
||||
typ := reflect.TypeOf(secret) // main.NotknownType
|
||||
// alternative:
|
||||
//typ := value.Type() // main.NotknownType
|
||||
// typ := value.Type() // main.NotknownType
|
||||
fmt.Println(typ)
|
||||
knd := value.Kind() // struct
|
||||
fmt.Println(knd)
|
||||
@@ -214,7 +214,7 @@ func main() {
|
||||
for i := 0; i < value.NumField(); i++ {
|
||||
fmt.Printf("Field %d: %v\n", i, value.Field(i))
|
||||
// error: panic: reflect.Value.SetString using value obtained using unexported field
|
||||
//value.Field(i).SetString("C#")
|
||||
// value.Field(i).SetString("C#")
|
||||
}
|
||||
|
||||
// call the first method, which is String():
|
||||
|
@@ -8,7 +8,7 @@ Printf 的函数声明为:
|
||||
func Printf(format string, args ... interface{}) (n int, err error)
|
||||
```
|
||||
|
||||
Printf 中的 `...` 参数为空接口类型。Printf 使用反射包来解析这个参数列表。所以,Printf 能够知道它每个参数的类型。因此格式化字符串中只有%d而没有 %u 和 %ld,因为它知道这个参数是 unsigned 还是 long。这也是为什么 Print 和 Println 在没有格式字符串的情况下还能如此漂亮地输出。
|
||||
Printf 中的 `...` 参数为空接口类型。Printf 使用反射包来解析这个参数列表。所以,Printf 能够知道它每个参数的类型。因此格式化字符串中只有 %d 而没有 %u 和 %ld,因为它知道这个参数是 unsigned 还是 long。这也是为什么 Print 和 Println 在没有格式字符串的情况下还能如此漂亮地输出。
|
||||
|
||||
为了让大家更加具体地了解 Printf 中的反射,我们实现了一个简单的通用输出函数。其中使用了 type-switch 来推导参数类型,并根据类型来输出每个参数的值(这里用了 10.7 节中练习 10.13 的部分代码)
|
||||
|
||||
|
@@ -14,7 +14,7 @@ Go 中的接口跟 Java/C# 类似:都是必须提供一个指定方法集的
|
||||
|
||||
类似于 Python 和 Ruby 这类动态语言中的 `动态类型(duck typing)`;这意味着对象可以根据提供的方法被处理(例如,作为参数传递给函数),而忽略它们的实际类型:它们能做什么比它们是什么更重要。
|
||||
|
||||
这在程序 duck_dance.go 中得以阐明,函数 DuckDance 接受一个 IDuck 接口类型变量。仅当 DuckDance 被实现了 IDuck 接口的类型调用时程序才能编译通过。
|
||||
这在程序 [duck_dance.go](examples/chapter_11/duck_dance.go) 中得以阐明,函数 DuckDance 接受一个 IDuck 接口类型变量。仅当 DuckDance 被实现了 IDuck 接口的类型调用时程序才能编译通过。
|
||||
|
||||
示例 11.16 [duck_dance.go](examples/chapter_11/duck_dance.go):
|
||||
|
||||
@@ -119,7 +119,8 @@ Go 的接口提高了代码的分离度,改善了代码的复用性,使得
|
||||
`提取接口` 是非常有用的设计模式,可以减少需要的类型和方法数量,而且不需要像传统的基于类的面向对象语言那样维护整个的类层次结构。
|
||||
|
||||
Go 接口可以让开发者找出自己写的程序中的类型。假设有一些拥有共同行为的对象,并且开发者想要抽象出这些行为,这时就可以创建一个接口来使用。
|
||||
我们来扩展 11.1 节的示例 11.2 interfaces_poly.go,假设我们需要一个新的接口 `TopologicalGenus`,用来给 shape 排序(这里简单地实现为返回 int)。我们需要做的是给想要满足接口的类型实现 `Rank()` 方法:
|
||||
|
||||
我们来扩展 11.1 节的示例 11.2 [interfaces_poly.go](examples/chapter_11/interfaces_poly.go),假设我们需要一个新的接口 `TopologicalGenus`,用来给 shape 排序(这里简单地实现为返回 int)。我们需要做的是给想要满足接口的类型实现 `Rank()` 方法:
|
||||
|
||||
示例 11.17 [multi_interfaces_poly.go](examples/chapter_11/multi_interfaces_poly.go):
|
||||
|
||||
@@ -229,7 +230,7 @@ func (b Bar) Foo() {}
|
||||
fmt.Printf(format string, a ...interface{}) (n int, errno error)
|
||||
```
|
||||
|
||||
这个函数通过枚举 `slice` 类型的实参动态确定所有参数的类型。并查看每个类型是否实现了 `String()` 方法,如果是就用于产生输出信息。我们可以回到 11.10 节查看这些细节。
|
||||
这个函数通过枚举 `slice` 类型的实参动态确定所有参数的类型,并查看每个类型是否实现了 `String()` 方法,如果是就用于产生输出信息。我们可以回到 11.10 节查看这些细节。
|
||||
|
||||
## 11.12.6 接口的继承
|
||||
|
||||
@@ -275,7 +276,7 @@ type ReaderWriter struct {
|
||||
|
||||
在练习 7.13 中我们定义了一个 map 函数来使用 int 切片 (map_function.go)。
|
||||
|
||||
通过空接口和类型断言,现在我们可以写一个可以应用于许多类型的 `泛型` 的 map 函数,为 int 和 string 构建一个把 int 值加倍和将字符串值与其自身连接(译者注:即`"abc"`变成`"abcabc"`)的 map 函数 `mapFunc`。
|
||||
通过空接口和类型断言,现在我们可以写一个可以应用于许多类型的 `泛型` 的 map 函数,为 int 和 string 构建一个把 int 值加倍和将字符串值与其自身连接(译者注:即 `"abc"` 变成 `"abcabc"` )的 map 函数 `mapFunc`。
|
||||
|
||||
提示:为了可读性可以定义一个 interface{} 的别名,比如:type obj interface{}
|
||||
|
||||
|
@@ -19,7 +19,7 @@ default:
|
||||
|
||||
Type Square *main.Square with value &{5}
|
||||
|
||||
变量 `t` 得到了 `areaIntf` 的值和类型, 所有 `case` 语句中列举的类型(`nil` 除外)都必须实现对应的接口(在上例中即 `Shaper`),如果被检测类型没有在 `case` 语句列举的类型中,就会执行 `default` 语句。
|
||||
变量 `t` 得到了 `areaIntf` 的值和类型,所有 `case` 语句中列举的类型(`nil` 除外)都必须实现对应的接口(在上例中即 `Shaper`),如果被检测类型没有在 `case` 语句列举的类型中,就会执行 `default` 语句。
|
||||
|
||||
可以用 `type-switch` 进行运行时类型分析,但是在 `type-switch` 不允许有 `fallthrough` 。
|
||||
|
||||
@@ -64,7 +64,7 @@ func classifier(items ...interface{}) {
|
||||
|
||||
在处理来自于外部的、类型未知的数据时,比如解析诸如 JSON 或 XML 编码的数据,类型测试和转换会非常有用。
|
||||
|
||||
在示例 12.17(xml.go)中解析 XML 文档时,我们就会用到 `type-switch`。
|
||||
在示例 12.17([xml.go](examples/chapter_12/xml.go))中解析 XML 文档时,我们就会用到 `type-switch`。
|
||||
|
||||
**练习 11.4** simple_interface2.go:
|
||||
|
||||
|
@@ -46,13 +46,13 @@ func main() {
|
||||
// cannot use lst (type List) as type Appender in argument to CountInto:
|
||||
// List does not implement Appender (Append method has pointer receiver)
|
||||
// CountInto(lst, 1, 10)
|
||||
if LongEnough(lst) { // VALID:Identical receiver type
|
||||
if LongEnough(lst) { // VALID: Identical receiver type
|
||||
fmt.Printf("- lst is long enough\n")
|
||||
}
|
||||
|
||||
// A pointer value
|
||||
plst := new(List)
|
||||
CountInto(plst, 1, 10) //VALID:Identical receiver type
|
||||
CountInto(plst, 1, 10) // VALID: Identical receiver type
|
||||
if LongEnough(plst) {
|
||||
// VALID: a *List can be dereferenced for the receiver
|
||||
fmt.Printf("- plst is long enough\n")
|
||||
@@ -68,7 +68,7 @@ func main() {
|
||||
|
||||
**总结**
|
||||
|
||||
在接口上调用方法时,必须有和方法定义时相同的接收者类型或者是可以从具体类型 `P` 直接可以辨识的:
|
||||
在接口上调用方法时,必须有和方法定义时相同的接收者类型或者是可以根据具体类型 `P` 直接辨识的:
|
||||
|
||||
- 指针方法可以通过指针调用
|
||||
- 值方法可以通过值调用
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# 11.7 第一个例子:使用 Sorter 接口排序
|
||||
|
||||
一个很好的例子是来自标准库的 `sort` 包,要对一组数字或字符串排序,只需要实现三个方法:反映元素个数的 `Len()`方法、比较第 `i` 和 `j` 个元素的 `Less(i, j)` 方法以及交换第 `i` 和 `j` 个元素的 `Swap(i, j)` 方法。
|
||||
一个很好的例子是来自标准库的 `sort` 包,要对一组数字或字符串排序,只需要实现三个方法:反映元素个数的 `Len()` 方法、比较第 `i` 和 `j` 个元素的 `Less(i, j)` 方法以及交换第 `i` 和 `j` 个元素的 `Swap(i, j)` 方法。
|
||||
|
||||
排序函数的算法只会使用到这三个方法(可以使用任何排序算法来实现,此处我们使用冒泡排序):
|
||||
|
||||
@@ -202,17 +202,17 @@ b). 定义一个新接口 `PeriInterface`,它有一个 `Perimeter` 方法。
|
||||
|
||||
**练习 11.6** point_interfaces.go:
|
||||
|
||||
继续 10.3 中的练习 point_methods.go,定义接口 `Magnitude`,它有一个方法 `Abs()`。让 `Point`、`Point3` 及`Polar` 实现此接口。通过接口类型变量使用方法做point.go中同样的事情。
|
||||
继续 10.3 中的练习 point_methods.go,定义接口 `Magnitude`,它有一个方法 `Abs()`。让 `Point`、`Point3` 及`Polar` 实现此接口。通过接口类型变量使用方法做 point.go 中同样的事情。
|
||||
|
||||
**练习 11.7** float_sort.go / float_sortmain.go:
|
||||
**练习 11.7** float_sort.go/float_sortmain.go:
|
||||
|
||||
类似11.7和示例11.3/4,定义一个包 `float64`,并在包里定义类型 `Float64Array`,然后让它实现 `Sorter` 接口用来对 `float64` 数组进行排序。
|
||||
类似 11.7 和示例 11.3/4,定义一个包 `float64`,并在包里定义类型 `Float64Array`,然后让它实现 `Sorter` 接口用来对 `float64` 数组进行排序。
|
||||
|
||||
另外提供如下方法:
|
||||
|
||||
- `NewFloat64Array()`:创建一个包含25个元素的数组变量(参考10.2)
|
||||
- `NewFloat64Array()`:创建一个包含 25 个元素的数组变量(参考10.2)
|
||||
- `List()`:返回数组格式化后的字符串,并在 `String()` 方法中调用它,这样就不用显式地调用 `List()` 来打印数组(参考10.7)
|
||||
- `Fill()`:创建一个包含10个随机浮点数的数组(参考4.5.2.6)
|
||||
- `Fill()`:创建一个包含 10 个随机浮点数的数组(参考4.5.2.6)
|
||||
|
||||
在主程序中新建一个此类型的变量,然后对它排序并进行测试。
|
||||
|
||||
|
@@ -14,7 +14,7 @@ type Writer interface {
|
||||
}
|
||||
```
|
||||
|
||||
只要类型实现了读写接口,提供 `Read()` 和 `Write` 方法,就可以从它读取数据,或向它写入数据。一个对象要是可读的,它必须实现 `io.Reader` 接口,这个接口只有一个签名是 `Read(p []byte) (n int, err error)` 的方法,它从调用它的对象上读取数据,并把读到的数据放入参数中的字节切片中,然后返回读取的字节数和一个 `error` 对象,如果没有错误发生返回 `nil`,如果已经到达输入的尾端,会返回 `io.EOF("EOF")`,如果读取的过程中发生了错误,就会返回具体的错误信息。类似地,一个对象要是可写的,它必须实现 `io.Writer` 接口,这个接口也只有一个签名是 `Write(p []byte) (n int, err error)` 的方法,它将指定字节切片中的数据写入调用它的对象里,然后返回实际写入的字节数和一个 `error` 对象(如果没有错误发生就是 `nil`)。
|
||||
只要类型实现了读写接口,提供 `Read` 和 `Write` 方法,就可以从它读取数据,或向它写入数据。一个对象要是可读的,它必须实现 `io.Reader` 接口,这个接口只有一个签名是 `Read(p []byte) (n int, err error)` 的方法,它从调用它的对象上读取数据,并把读到的数据放入参数中的字节切片中,然后返回读取的字节数和一个 `error` 对象,如果没有错误发生返回 `nil`,如果已经到达输入的尾端,会返回 `io.EOF("EOF")`,如果读取的过程中发生了错误,就会返回具体的错误信息。类似地,一个对象要是可写的,它必须实现 `io.Writer` 接口,这个接口也只有一个签名是 `Write(p []byte) (n int, err error)` 的方法,它将指定字节切片中的数据写入调用它的对象里,然后返回实际写入的字节数和一个 `error` 对象(如果没有错误发生就是 `nil`)。
|
||||
|
||||
`io` 包里的 `Readers` 和 `Writers` 都是不带缓冲的,`bufio` 包里提供了对应的带缓冲的操作,在读写 `UTF-8` 编码的文本文件时它们尤其有用。在 第12章 我们会看到很多在实战中使用它们的例子。
|
||||
|
||||
|
@@ -107,7 +107,7 @@ func main() {
|
||||
|
||||
**练习 11.9** simple_interface3.go:
|
||||
|
||||
继续 练习11.2,在它中添加一个 `gI` 函数,它不再接受 `Simpler` 类型的参数,而是接受一个空接口参数。然后通过类型断言判断参数是否是 `Simpler` 类型。最后在 `main` 使用 `gI` 取代 `fI` 函数并调用它。确保你的代码足够安全。
|
||||
继续练习 11.2,在它中添加一个 `gI` 函数,它不再接受 `Simpler` 类型的参数,而是接受一个空接口参数。然后通过类型断言判断参数是否是 `Simpler` 类型。最后在 `main` 使用 `gI` 取代 `fI` 函数并调用它。确保你的代码足够安全。
|
||||
|
||||
## 11.9.2 构建通用类型或包含不同类型变量的数组
|
||||
|
||||
@@ -143,7 +143,7 @@ func (p *Vector) Set(i int, e Element) {
|
||||
|
||||
**练习 11.10** min_interface.go / minmain.go:
|
||||
|
||||
仿照11.7中开发的 `Sorter` 接口,创建一个 `Miner` 接口并实现一些必要的操作。函数 `Min` 接受一个 `Miner` 类型变量的集合,然后计算并返回集合中最小的元素。
|
||||
仿照 11.7 中开发的 `Sorter` 接口,创建一个 `Miner` 接口并实现一些必要的操作。函数 `Min` 接受一个 `Miner` 类型变量的集合,然后计算并返回集合中最小的元素。
|
||||
|
||||
## 11.9.3 复制数据切片至空接口切片
|
||||
|
||||
@@ -170,7 +170,7 @@ for i, d := range dataSlice {
|
||||
|
||||
## 11.9.4 通用类型的节点数据结构
|
||||
|
||||
在10.1中我们遇到了诸如列表和树这样的数据结构,在它们的定义中使用了一种叫节点的递归结构体类型,节点包含一个某种类型的数据字段。现在可以使用空接口作为数据字段的类型,这样我们就能写出通用的代码。下面是实现一个二叉树的部分代码:通用定义、用于创建空节点的 `NewNode` 方法,及设置数据的 `SetData` 方法。
|
||||
在 10.1 中我们遇到了诸如列表和树这样的数据结构,在它们的定义中使用了一种叫节点的递归结构体类型,节点包含一个某种类型的数据字段。现在可以使用空接口作为数据字段的类型,这样我们就能写出通用的代码。下面是实现一个二叉树的部分代码:通用定义、用于创建空节点的 `NewNode` 方法,及设置数据的 `SetData` 方法。
|
||||
|
||||
示例 11.10 [node_structures.go](examples/chapter_11/node_structures.go):
|
||||
|
||||
|
@@ -126,7 +126,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
注意:Unix和Windows的行结束符是不同的!
|
||||
注意:Unix 和 Windows 的行结束符是不同的!
|
||||
|
||||
**练习**
|
||||
|
||||
|
@@ -11,7 +11,7 @@
|
||||
|
||||
如同 json 包一样,也有 `Marshal()` 和 `UnMarshal()` 从 XML 中编码和解码数据;但这个更通用,可以从文件中读取和写入(或者任何实现了 io.Reader 和 io.Writer 接口的类型)
|
||||
|
||||
和 JSON 的方式一样,XML 数据可以序列化为结构,或者从结构反序列化为 XML 数据;这些可以在例子 15.8(twitter_status.go)中看到。
|
||||
和 JSON 的方式一样,XML 数据可以序列化为结构,或者从结构反序列化为 XML 数据;这些可以在例子 15.8([twitter_status.go](examples/chapter_15/twitter_status.go))中看到。
|
||||
|
||||
encoding/xml 包实现了一个简单的 XML 解析器(SAX),用来解析 XML 数据内容。下面的例子说明如何使用解析器:
|
||||
|
||||
|
@@ -45,7 +45,7 @@ func main() {
|
||||
|
||||
接着,我们在一个无限循环中使用 `ReadString('\n')` 或 `ReadBytes('\n')` 将文件的内容逐行(行结束符 '\n')读取出来。
|
||||
|
||||
**注意:** 在之前的例子中,我们看到,Unix和Linux的行结束符是 \n,而Windows的行结束符是 \r\n。在使用 `ReadString` 和 `ReadBytes` 方法的时候,我们不需要关心操作系统的类型,直接使用 \n 就可以了。另外,我们也可以使用 `ReadLine()` 方法来实现相同的功能。
|
||||
**注意:** 在之前的例子中,我们看到,Unix 和 Linux 的行结束符是 \n,而 Windows 的行结束符是 \r\n。在使用 `ReadString` 和 `ReadBytes` 方法的时候,我们不需要关心操作系统的类型,直接使用 \n 就可以了。另外,我们也可以使用 `ReadLine()` 方法来实现相同的功能。
|
||||
|
||||
一旦读取到文件末尾,变量 `readerError` 的值将变成非空(事实上,其值为常量 `io.EOF`),我们就会执行 `return` 语句从而退出循环。
|
||||
|
||||
@@ -160,7 +160,7 @@ filename := filepath.Base(path)
|
||||
```
|
||||
每行的第一个字段为 title,第二个字段为 price,第三个字段为 quantity。内容的格式基本与 示例 12.3c 的相同,除了分隔符改成了分号。请读取出文件的内容,创建一个结构用于存取一行的数据,然后使用结构的切片,并把数据打印出来。
|
||||
|
||||
关于解析 CSV 文件,`encoding/csv` 包提供了相应的功能。具体请参考 [http://golang.org/pkg/encoding/csv/](http://golang.org/pkg/encoding/csv/)
|
||||
关于解析 CSV 文件,`encoding/csv` 包提供了相应的功能。具体请参考 [http://golang.org/pkg/encoding/csv/](http://golang.org/pkg/encoding/csv/) 。
|
||||
|
||||
## 12.2.2 `compress`包:读取压缩文件
|
||||
|
||||
@@ -258,9 +258,9 @@ outputFile, outputError := os.OpenFile("output.dat", os.O_WRONLY|os.O_CREATE, 06
|
||||
- `os.O_RDONLY`:只读
|
||||
- `os.O_WRONLY`:只写
|
||||
- `os.O_CREATE`:创建:如果指定文件不存在,就创建该文件。
|
||||
- `os.O_TRUNC`:截断:如果指定文件已存在,就将该文件的长度截为0。
|
||||
- `os.O_TRUNC`:截断:如果指定文件已存在,就将该文件的长度截为 0 。
|
||||
|
||||
在读文件的时候,文件的权限是被忽略的,所以在使用 `OpenFile` 时传入的第三个参数可以用0。而在写文件时,不管是 Unix 还是 Windows,都需要使用 0666。
|
||||
在读文件的时候,文件的权限是被忽略的,所以在使用 `OpenFile` 时传入的第三个参数可以用 0 。而在写文件时,不管是 Unix 还是 Windows,都需要使用 0666。
|
||||
|
||||
然后,我们创建一个写入器(缓冲区)对象:
|
||||
|
||||
@@ -310,7 +310,7 @@ type Page struct {
|
||||
}
|
||||
```
|
||||
|
||||
请给这个结构编写一个 `save` 方法,将 Title 作为文件名、Body作为文件内容,写入到文本文件中。
|
||||
请给这个结构编写一个 `save` 方法,将 Title 作为文件名、Body 作为文件内容,写入到文本文件中。
|
||||
|
||||
再编写一个 `load` 函数,接收的参数是字符串 title,该函数读取出与 title 对应的文本文件。请使用 `*Page` 做为参数,因为这个结构可能相当巨大,我们不想在内存中拷贝它。请使用 `ioutil` 包里的函数(参考章节12.2.1)。
|
||||
|
||||
|
@@ -36,7 +36,7 @@ func CopyFile(dstName, srcName string) (written int64, err error) {
|
||||
}
|
||||
```
|
||||
|
||||
注意 `defer` 的使用:当打开dst文件时发生了错误,那么 `defer` 仍然能够确保 `src.Close()` 执行。如果不这么做,src文件会一直保持打开状态并占用资源。
|
||||
注意 `defer` 的使用:当打开 dst 文件时发生了错误,那么 `defer` 仍然能够确保 `src.Close()` 执行。如果不这么做,src 文件会一直保持打开状态并占用资源。
|
||||
|
||||
## 链接
|
||||
|
||||
|
@@ -31,11 +31,11 @@ func main() {
|
||||
|
||||
但是我们在命令行加入参数,像这样:`os_args John Bill Marc Luke`,将得到这样的输出:`Good Morning Alice John Bill Marc Luke`
|
||||
|
||||
这个命令行参数会放置在切片 `os.Args[]` 中(以空格分隔),从索引1开始(`os.Args[0]` 放的是程序本身的名字,在本例中是 `os_args`)。函数 `strings.Join` 以空格为间隔连接这些参数。
|
||||
这个命令行参数会放置在切片 `os.Args[]` 中(以空格分隔),从索引 1 开始(`os.Args[0]` 放的是程序本身的名字,在本例中是 `os_args`)。函数 `strings.Join` 以空格为间隔连接这些参数。
|
||||
|
||||
**练习 12.5**:[hello_who.go](exercises/chapter_12/hello_who.go)
|
||||
|
||||
写一个"Hello World"的变种程序:把人的名字作为程序命令行执行的一个参数,比如: `hello_who Evan Michael Laura` 那么会输出`Hello Evan Michael Laura`!
|
||||
写一个"Hello World"的变种程序:把人的名字作为程序命令行执行的一个参数,比如: `hello_who Evan Michael Laura` 那么会输出 `Hello Evan Michael Laura!`
|
||||
|
||||
## 12.4.2 flag 包
|
||||
|
||||
@@ -52,7 +52,7 @@ type Flag struct {
|
||||
}
|
||||
```
|
||||
|
||||
下面的程序 `echo.go` 模拟了 Unix 的 echo 功能:
|
||||
下面的程序 [echo.go](examples/chapter_12/echo.go) 模拟了 Unix 的 echo 功能:
|
||||
|
||||
```go
|
||||
package main
|
||||
@@ -86,7 +86,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
`flag.Parse()` 扫描参数列表(或者常量列表)并设置 flag, `flag.Arg(i)` 表示第i个参数。`Parse()` 之后 `flag.Arg(i)` 全部可用,`flag.Arg(0)` 就是第一个真实的 flag,而不是像 `os.Args(0)` 放置程序的名字。
|
||||
`flag.Parse()` 扫描参数列表(或者常量列表)并设置 flag, `flag.Arg(i)` 表示第 i 个参数。`Parse()` 之后 `flag.Arg(i)` 全部可用,`flag.Arg(0)` 就是第一个真实的 flag,而不是像 `os.Args(0)` 放置程序的名字。
|
||||
|
||||
`flag.Narg()` 返回参数的数量。解析后 flag 或常量就可用了。
|
||||
`flag.Bool()` 定义了一个默认值是 `false` 的 flag:当在命令行出现了第一个参数(这里是 "n"),flag 被设置成 `true`(NewLine 是 `*bool` 类型)。flag 被解引用到 `*NewLine`,所以当值是 `true` 时将添加一个 Newline("\n")。
|
||||
@@ -131,4 +131,4 @@ if *processedFlag { // found flag -proc
|
||||
|
||||
- [目录](directory.md)
|
||||
- 上一节:[文件拷贝](12.3.md)
|
||||
- 下一节:[用buffer读取文件](12.5.md)
|
||||
- 下一节:[用 buffer 读取文件](12.5.md)
|
||||
|
@@ -22,7 +22,7 @@ func cat(f *os.File) {
|
||||
}
|
||||
```
|
||||
|
||||
上面的代码来自于 `cat2.go`,使用了 os 包中的 `os.File` 和 `Read` 方法;`cat2.go` 与 `cat.go` 具有同样的功能。
|
||||
上面的代码来自于 [cat2.go](examples/chapter_12/cat2.go),使用了 os 包中的 `os.File` 和 `Read` 方法;`cat2.go` 与 `cat.go` 具有同样的功能。
|
||||
|
||||
示例 12.14 [cat2.go](examples/chapter_12/cat2.go):
|
||||
|
||||
|
@@ -37,7 +37,8 @@ hello world! - buffered
|
||||
```go
|
||||
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
|
||||
```
|
||||
其不是写入一个文件,而是写入一个 `io.Writer` 接口类型的变量,下面是 `Writer` 接口在 io 包中的定义:
|
||||
|
||||
不是写入一个文件,而是写入一个 `io.Writer` 接口类型的变量,下面是 `Writer` 接口在 io 包中的定义:
|
||||
|
||||
```go
|
||||
type Writer interface {
|
||||
@@ -45,7 +46,7 @@ type Writer interface {
|
||||
}
|
||||
```
|
||||
|
||||
`fmt.Fprintf()` 依据指定的格式向第一个参数内写入字符串,第一个参数必须实现了 `io.Writer` 接口。`Fprintf()` 能够写入任何类型,只要其实现了 `Write` 方法,包括 `os.Stdout`,文件(例如 os.File),管道,网络连接,通道等等,同样的也可以使用 bufio 包中缓冲写入。bufio 包中定义了 `type Writer struct{...}`。
|
||||
`fmt.Fprintf()` 依据指定的格式向第一个参数内写入字符串,第一个参数必须实现了 `io.Writer` 接口。`Fprintf()` 能够写入任何类型,只要其实现了 `Write` 方法,包括 `os.Stdout`,文件(例如 os.File),管道,网络连接,通道等等,同样的也可以使用 bufio 包中缓冲写入。bufio 包中定义了 `type Writer struct{...}` 。
|
||||
|
||||
bufio.Writer 实现了 Write 方法:
|
||||
|
||||
@@ -53,13 +54,13 @@ bufio.Writer 实现了 Write 方法:
|
||||
func (b *Writer) Write(p []byte) (nn int, err error)
|
||||
```
|
||||
|
||||
它还有一个工厂函数:传给它一个 `io.Writer` 类型的参数,它会返回一个带缓冲的 `bufio.Writer` 类型的 `io.Writer`:
|
||||
它还有一个工厂函数:传给它一个 `io.Writer` 类型的参数,它会返回一个带缓冲的 `bufio.Writer` 类型的 `io.Writer` :
|
||||
|
||||
```go
|
||||
func NewWriter(wr io.Writer) (b *Writer)
|
||||
```
|
||||
|
||||
其适合任何形式的缓冲写入。
|
||||
适合任何形式的缓冲写入。
|
||||
|
||||
在缓冲写入的最后千万不要忘了使用 `Flush()`,否则最后的输出不会被写入。
|
||||
|
||||
@@ -107,4 +108,4 @@ func main() {
|
||||
|
||||
- [目录](directory.md)
|
||||
- 上一节:[用 defer 关闭文件](12.7.md)
|
||||
- 下一节:[格式化 Json 数据](12.9.md)
|
||||
- 下一节:[格式化 JSON 数据](12.9.md)
|
||||
|
@@ -98,9 +98,9 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
出于安全考虑,在 web 应用中最好使用 `json.MarshalforHTML()` 函数,其对数据执行HTML转码,所以文本可以被安全地嵌在 HTML `<script>` 标签中。
|
||||
出于安全考虑,在 web 应用中最好使用 `json.MarshalforHTML()` 函数,其对数据执行 HTML 转码,所以文本可以被安全地嵌在 HTML `<script>` 标签中。
|
||||
|
||||
`json.NewEncoder()` 的函数签名是 `func NewEncoder(w io.Writer) *Encoder`,返回的Encoder类型的指针可调用方法 `Encode(v interface{})`,将数据对象 v 的json编码写入 `io.Writer` w 中。
|
||||
`json.NewEncoder()` 的函数签名是 `func NewEncoder(w io.Writer) *Encoder`,返回的 Encoder 类型的指针可调用方法 `Encode(v interface{})`,将数据对象 v 的 json 编码写入 `io.Writer` w 中。
|
||||
|
||||
JSON 与 Go 类型对应如下:
|
||||
|
||||
@@ -109,7 +109,7 @@ JSON 与 Go 类型对应如下:
|
||||
- string 对应 JSON 的 string
|
||||
- nil 对应 JSON 的 null
|
||||
|
||||
不是所有的数据都可以编码为 JSON 类型:只有验证通过的数据结构才能被编码:
|
||||
不是所有的数据都可以编码为 JSON 类型,只有验证通过的数据结构才能被编码:
|
||||
|
||||
- JSON 对象只支持字符串类型的 key;要编码一个 Go map 类型,map 必须是 map[string]T(T是 `json` 包中支持的任何类型)
|
||||
- Channel,复杂类型和函数类型不能被编码
|
||||
@@ -120,11 +120,11 @@ JSON 与 Go 类型对应如下:
|
||||
|
||||
`UnMarshal()` 的函数签名是 `func Unmarshal(data []byte, v interface{}) error` 把 JSON 解码为数据结构。
|
||||
|
||||
示例12.16中对 vc 编码后的数据为 `js` ,对其解码时,我们首先创建结构 VCard 用来保存解码的数据:`var v VCard` 并调用 `json.Unmarshal(js, &v)`,解析 []byte 中的 JSON 数据并将结果存入指针 &v 指向的值。
|
||||
示例 12.16 中对 vc 编码后的数据为 `js` ,对其解码时,我们首先创建结构 VCard 用来保存解码的数据:`var v VCard` 并调用 `json.Unmarshal(js, &v)`,解析 []byte 中的 JSON 数据并将结果存入指针 &v 指向的值。
|
||||
|
||||
虽然反射能够让 JSON 字段去尝试匹配目标结构字段;但是只有真正匹配上的字段才会填充数据。字段没有匹配不会报错,而是直接忽略掉。
|
||||
|
||||
(练习 15.2b [twitter_status_json.go](exercises/chapter_15/twitter_status_json.go) 中用到了 UnMarshal)
|
||||
(练习 15.2b [twitter_status_json.go](exercises/chapter_15/twitter_status_json.go) 中用到了 Unmarshal
|
||||
|
||||
### 解码任意的数据:
|
||||
|
||||
|
@@ -8,11 +8,11 @@ type error interface {
|
||||
}
|
||||
```
|
||||
|
||||
错误值用来表示异常状态;我们可以在 [5.2 节](05.2.md)中看到它的标准用法。处理文件操作的例子可以在 12 章找到;我们将在 15 章看到网络操作的例子。errors 包中有一个 errorString 结构体实现了 error 接口。当程序处于错误状态时可以用 `os.Exit(1)` 来中止运行。
|
||||
错误值用来表示异常状态;我们可以在 [5.2 节](05.2.md) 中看到它的标准用法。处理文件操作的例子可以在 12 章找到;我们将在 15 章看到网络操作的例子。errors 包中有一个 errorString 结构体实现了 error 接口。当程序处于错误状态时可以用 `os.Exit(1)` 来中止运行。
|
||||
|
||||
## 13.1.1 定义错误
|
||||
|
||||
任何时候当你需要一个新的错误类型,都可以用 `errors`(必须先 import)包的 `errors.New` 函数接收合适的错误信息来创建,像下面这样:
|
||||
任何时候当你需要一个新的错误类型,都可以用 `errors` 包(必须先 import)的 `errors.New` 函数接收合适的错误信息来创建,像下面这样:
|
||||
|
||||
```go
|
||||
err := errors.New("math - square root of negative number")
|
||||
@@ -133,7 +133,7 @@ type Error interface {
|
||||
|
||||
正如你所看到的一样,所有的例子都遵循同一种命名规范:错误类型以 “Error” 结尾,错误变量以 “err” 或 “Err” 开头。
|
||||
|
||||
syscall 是低阶外部包,用来提供系统基本调用的原始接口。它们返回封装整数类型错误码的syscall.Errno;类型 syscall.Errno 实现了 Error 接口。
|
||||
syscall 是低阶外部包,用来提供系统基本调用的原始接口。它们返回封装整数类型错误码的 syscall.Errno;类型 syscall.Errno 实现了 Error 接口。
|
||||
|
||||
大部分 syscall 函数都返回一个结果和可能的错误,比如:
|
||||
|
||||
|
@@ -39,13 +39,13 @@ func main() {
|
||||
...
|
||||
```
|
||||
|
||||
代码定义了一个名为 cpuprofile 的 flag,调用 Go flag 库来解析命令行 flag,如果命令行设置了 cpuprofile flag,则开始 CPU 性能分析并把结果重定向到那个文件。(os.Create 用拿到的名字创建了用来写入分析数据的文件)。这个分析程序最后需要在程序退出之前调用 StopCPUProfile 来刷新挂起的写操作到文件中;我们用 defer 来保证这一切会在 main 返回时触发。
|
||||
代码定义了一个名为 cpuprofile 的 flag,调用 Go flag 库来解析命令行 flag,如果命令行设置了 cpuprofile flag,则开始 CPU 性能分析并把结果重定向到那个文件(os.Create 用拿到的名字创建了用来写入分析数据的文件)。这个分析程序最后需要在程序退出之前调用 StopCPUProfile 来刷新挂起的写操作到文件中;我们用 defer 来保证这一切会在 main 返回时触发。
|
||||
|
||||
现在用这个 flag 运行程序:```progexec -cpuprofile=progexec.prof```
|
||||
|
||||
然后可以像这样用 gopprof 工具:```gopprof progexec progexec.prof```
|
||||
|
||||
gopprof 程序是 Google pprofC++ 分析器的一个轻微变种;关于此工具更多的信息,参见[https://github.com/gperftools/gperftools](https://github.com/gperftools/gperftools)。
|
||||
gopprof 程序是 Google pprofC++ 分析器的一个轻微变种;关于此工具更多的信息,参见[https://github.com/gperftools/gperftools](https://github.com/gperftools/gperftools) 。
|
||||
|
||||
如果开启了 CPU 性能分析,Go 程序会以大约每秒 100 次的频率阻塞,并记录当前执行的 goroutine 栈上的程序计数器样本。
|
||||
|
||||
|
@@ -24,9 +24,9 @@ func protect(g func()) {
|
||||
|
||||
这跟 Java 和 .NET 这样的语言中的 catch 块类似。
|
||||
|
||||
log 包实现了简单的日志功能:默认的 log 对象向标准错误输出中写入并打印每条日志信息的日期和时间。除了 `Println` 和 `Printf` 函数,其它的致命性函数都会在写完日志信息后调用 os.Exit(1),那些退出函数也是如此。而 Panic 效果的函数会在写完日志信息后调用 panic;可以在程序必须中止或发生了临界错误时使用它们,就像当 web 服务器不能启动时那样(参见 [15.4 节](15.4.md)中的例子)。
|
||||
log 包实现了简单的日志功能:默认的 log 对象向标准错误输出中写入并打印每条日志信息的日期和时间。除了 `Println` 和 `Printf` 函数,其它的致命性函数都会在写完日志信息后调用 os.Exit(1),那些退出函数也是如此。而 Panic 效果的函数会在写完日志信息后调用 panic;可以在程序必须中止或发生了临界错误时使用它们,就像当 web 服务器不能启动时那样(参见 [15.4 节](15.4.md) 中的例子)。
|
||||
|
||||
log 包用那些方法(methods)定义了一个 Logger 接口类型,如果你想自定义日志系统的话可以参考(参见 [http://golang.org/pkg/log/#Logger](http://golang.org/pkg/log/#Logger))。
|
||||
log 包用那些方法(methods)定义了一个 Logger 接口类型,如果你想自定义日志系统的话可以参考 [http://golang.org/pkg/log/#Logger](http://golang.org/pkg/log/#Logger) 。
|
||||
|
||||
这是一个展示 panic,defer 和 recover 怎么结合使用的完整例子:
|
||||
|
||||
@@ -51,7 +51,7 @@ func test() {
|
||||
}
|
||||
}()
|
||||
badCall()
|
||||
fmt.Printf("After bad call\r\n") // <-- wordt niet bereikt
|
||||
fmt.Printf("After bad call\r\n") // <-- would not reach
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -71,7 +71,7 @@ Test completed
|
||||
|
||||
`defer-panic-recover` 在某种意义上也是一种像 `if`,`for` 这样的控制流机制。
|
||||
|
||||
Go 标准库中许多地方都用了这个机制,例如,json 包中的解码和 regexp 包中的 Complie 函数。Go 库的原则是即使在包的内部使用了 panic,在它的对外接口(API)中也必须用 recover 处理成返回显式的错误。
|
||||
Go 标准库中许多地方都用了这个机制,例如,json 包中的解码和 regexp 包中的 Complie 函数。Go 库的原则是即使在包的内部使用了 panic,在它的对外接口(API)中也必须用 recover 处理成显式返回的错误。
|
||||
|
||||
## 链接
|
||||
|
||||
|
@@ -28,7 +28,7 @@ fType1 = func f(a type1, b type2)
|
||||
func check(err error) { if err != nil { panic(err) } }
|
||||
```
|
||||
|
||||
2)errorhandler:这是一个包装函数。接收一个 fType1 类型的函数 fn 并返回一个调用 fn 的函数。里面就包含有 defer/recover 机制,这在 [13.3 节](13.3.md)中有相应描述。
|
||||
2)errorhandler:这是一个包装函数。接收一个 fType1 类型的函数 fn 并返回一个调用 fn 的函数。里面就包含有 defer/recover 机制,这在 [13.3 节](13.3.md) 中有相应描述。
|
||||
|
||||
```go
|
||||
func errorHandler(fn fType1) fType1 {
|
||||
@@ -60,9 +60,7 @@ func f1(a type1, b type2) {
|
||||
|
||||
通过这种机制,所有的错误都会被 recover,并且调用函数后的错误检查代码也被简化为调用 check(err) 即可。在这种模式下,不同的错误处理必须对应不同的函数类型;它们(错误处理)可能被隐藏在错误处理包内部。可选的更加通用的方式是用一个空接口类型的切片作为参数和返回值。
|
||||
|
||||
我们会在 [15.5 节](15.5.md)的 web 应用中使用这种模式。
|
||||
|
||||
<u>练习</u>
|
||||
我们会在 [15.5 节](15.5.md) 的 web 应用中使用这种模式。
|
||||
|
||||
**练习 13.1**:[recover_dividebyzero.go](exercises/chapter_13/recover_divbyzero.go)
|
||||
|
||||
@@ -124,7 +122,7 @@ Returned normally from f.
|
||||
|
||||
**练习 13.3**:[panic_defer_convint.go](exercises/chapter_13/panic_defer_convint.go)
|
||||
|
||||
写一个 ConvertInt64ToInt 函数把 int64 值转换为 int 值,如果发生错误(提示:参见 [4.5.2.1 节](04.5.md#4521-整型-int-和浮点型-float))就 panic。然后在函数 IntFromInt64 中调用这个函数并 recover,返回一个整数和一个错误。请测试这个函数!
|
||||
写一个 ConvertInt64ToInt 函数把 int64 值转换为 int 值,如果发生错误(提示:参见 [4.5.2.1 节](04.5.md#4521-整型-int-和浮点型-float))就 panic 。然后在函数 IntFromInt64 中调用这个函数并 recover,返回一个整数和一个错误。请测试这个函数!
|
||||
|
||||
## 链接
|
||||
|
||||
|
@@ -20,7 +20,7 @@
|
||||
func TestAbcde(t *testing.T)
|
||||
```
|
||||
|
||||
T 是传给测试函数的结构类型,用来管理测试状态,支持格式化测试日志,如 t.Log,t.Error,t.ErrorF 等。在函数的结尾把输出跟想要的结果对比,如果不等就打印一个错误。成功的测试则直接返回。
|
||||
T 是传给测试函数的结构类型,用来管理测试状态,支持格式化测试日志,如 t.Log,t.Error,t.ErrorF 等。在函数的结尾把输出跟想要的结果对比,如果不等就打印一个错误,成功的测试则直接返回。
|
||||
|
||||
用下面这些函数来通知测试失败:
|
||||
|
||||
@@ -65,9 +65,9 @@ func BenchmarkReverse(b *testing.B) {
|
||||
}
|
||||
```
|
||||
|
||||
命令 ```go test –test.bench=.*``` 会运行所有的基准测试函数;代码中的函数会被调用 N 次(N是非常大的数,如 N = 1000000),并展示 N 的值和函数执行的平均时间,单位为 ns(纳秒,ns/op)。如果是用 testing.Benchmark 调用这些函数,直接运行程序即可。
|
||||
命令 ```go test –test.bench=.*``` 会运行所有的基准测试函数;代码中的函数会被调用 N 次(N 是非常大的数,如 N = 1000000),并展示 N 的值和函数执行的平均时间,单位为 ns(纳秒,ns/op)。如果是用 testing.Benchmark 调用这些函数,直接运行程序即可。
|
||||
|
||||
具体可以参见 [14.16 节](14.16.md)中用 goroutines 运行基准测试的例子以及练习 13.4:[string_reverse_test.go](exercises/chapter_13/string_reverse_test.go)
|
||||
具体可以参见 [14.16 节](14.16.md) 中用 goroutines 运行基准测试的例子以及练习 13.4:[string_reverse_test.go](exercises/chapter_13/string_reverse_test.go)
|
||||
|
||||
## 链接
|
||||
|
||||
|
@@ -119,7 +119,7 @@ FAIL
|
||||
|
||||
为练习 7.14 [string_reverse.go](exercises/chapter_7/string_reverse.go) 写一个单元测试。
|
||||
|
||||
把 string_reverse 放到自己的包 strev 中,只包含一个可导出函数 reverse。
|
||||
把 string_reverse 放到自己的包 strev 中,只包含一个可导出函数 Reverse。
|
||||
|
||||
实现并测试它。
|
||||
|
||||
|
@@ -44,7 +44,7 @@ TestFunction 则变为:
|
||||
func TestFunction(t *testing.T) {
|
||||
for i, tt := range tests {
|
||||
s := FuncToBeTested(tt.in)
|
||||
verify(t, i, “FuncToBeTested: “, tt.in, s, tt.out)
|
||||
verify(t, i, "FuncToBeTested: ", tt.in, s, tt.out)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# 14.0 协程(goroutine)与通道(channel)
|
||||
|
||||
作为一门 21 世纪的语言,Go 原生支持应用之间的通信(网络,客户端和服务端,分布式计算,参见第 15 章)和程序的并发。程序可以在不同的处理器和计算机上同时执行不同的代码段。Go 语言为构建并发程序的基本代码块是 协程 (goroutine) 与通道 (channel)。他们需要语言,编译器,和runtime的支持。Go 语言提供的垃圾回收器对并发编程至关重要。
|
||||
作为一门 21 世纪的语言,Go 原生支持应用之间的通信(网络,客户端和服务端,分布式计算,参见第 15 章)和程序的并发。程序可以在不同的处理器和计算机上同时执行不同的代码段。Go 语言为构建并发程序的基本代码块是协程 (goroutine) 与通道 (channel) 。他们需要语言,编译器,和 runtime 的支持。Go 语言提供的垃圾回收器对并发编程至关重要。
|
||||
|
||||
**不要通过共享内存来通信,而通过通信来共享内存。**
|
||||
|
||||
|
@@ -36,7 +36,7 @@ Go 更倾向于其他的方式,在诸多比较合适的范式中,有个被
|
||||
|
||||
## 14.1.2 并发和并行的差异
|
||||
|
||||
Go 的并发原语提供了良好的并发设计基础:表达程序结构以便表示独立地执行的动作;所以Go的重点不在于并行的首要位置:并发程序可能是并行的,也可能不是。并行是一种通过使用多处理器以提高速度的能力。但往往是,一个设计良好的并发程序在并行方面的表现也非常出色。
|
||||
Go 的并发原语提供了良好的并发设计基础:表达程序结构以便表示独立地执行的动作;所以 Go 的重点不在于并行的首要位置:并发程序可能是并行的,也可能不是。并行是一种通过使用多处理器以提高速度的能力。但往往是,一个设计良好的并发程序在并行方面的表现也非常出色。
|
||||
|
||||
在当前的运行时(2012 年一月)实现中,Go 默认没有并行指令,只有一个独立的核心或处理器被专门用于 Go 程序,不论它启动了多少个协程;所以这些协程是并发运行的,但他们不是并行运行的:同一时间只有一个协程会处在运行状态。
|
||||
|
||||
|
@@ -4,11 +4,11 @@
|
||||
|
||||
客户端-服务器应用正是 goroutines 和 channels 的亮点所在。
|
||||
|
||||
客户端(Client)可以是运行在任意设备上的任意程序,它会按需发送请求(request)至服务器。服务器(Server)接收到这个请求后开始相应的工作,然后再将响应(response)返回给客户端。典型情况下一般是多个客户端(即多个请求)对应一个(或少量)服务器。例如我们日常使用的浏览器客户端,其功能就是向服务器请求网页。而Web服务器则会向浏览器响应网页数据。
|
||||
客户端(Client)可以是运行在任意设备上的任意程序,它会按需发送请求(request)至服务器。服务器(Server)接收到这个请求后开始相应的工作,然后再将响应(response)返回给客户端。典型情况下一般是多个客户端(即多个请求)对应一个(或少量)服务器。例如我们日常使用的浏览器客户端,其功能就是向服务器请求网页。而 Web 服务器则会向浏览器响应网页数据。
|
||||
|
||||
使用Go的服务器通常会在协程中执行向客户端的响应,故而会对每一个客户端请求启动一个协程。一个常用的操作方法是客户端请求自身中包含一个通道,而服务器则向这个通道发送响应。
|
||||
使用 Go 的服务器通常会在协程中执行向客户端的响应,故而会对每一个客户端请求启动一个协程。一个常用的操作方法是客户端请求自身中包含一个通道,而服务器则向这个通道发送响应。
|
||||
|
||||
例如下面这个`Request`结构,其中内嵌了一个`replyc`通道。
|
||||
例如下面这个 `Request` 结构,其中内嵌了一个 `replyc` 通道。
|
||||
```go
|
||||
type Request struct {
|
||||
a, b int
|
||||
@@ -25,7 +25,7 @@ type Request struct{
|
||||
```
|
||||
|
||||
|
||||
接下来先使用简单的形式,服务器会为每一个请求启动一个协程并在其中执行`run()`函数,此举会将类型为`binOp`的`op`操作返回的int值发送到`replyc`通道。
|
||||
接下来先使用简单的形式,服务器会为每一个请求启动一个协程并在其中执行 `run()` 函数,此举会将类型为 `binOp` 的 `op` 操作返回的 int 值发送到 `replyc` 通道。
|
||||
|
||||
|
||||
```go
|
||||
@@ -35,7 +35,7 @@ func run(op binOp, req *Request) {
|
||||
req.replyc <- op(req.a, req.b)
|
||||
}
|
||||
```
|
||||
`server`协程会无限循环以从`chan *Request`接收请求,并且为了避免被长时间操作所堵塞,它将为每一个请求启动一个协程来做具体的工作:
|
||||
`server` 协程会无限循环以从 `chan *Request` 接收请求,并且为了避免被长时间操作所堵塞,它将为每一个请求启动一个协程来做具体的工作:
|
||||
|
||||
```go
|
||||
func server(op binOp, service chan *Request) {
|
||||
@@ -46,7 +46,7 @@ func server(op binOp, service chan *Request) {
|
||||
}
|
||||
}
|
||||
```
|
||||
`server`本身则是以协程的方式在`startServer`函数中启动:
|
||||
`server` 本身则是以协程的方式在 `startServer` 函数中启动:
|
||||
```go
|
||||
func startServer(op binOp) chan *Request {
|
||||
reqChan := make(chan *Request);
|
||||
@@ -54,9 +54,9 @@ func startServer(op binOp) chan *Request {
|
||||
return reqChan;
|
||||
}
|
||||
```
|
||||
`startServer`则会在`main`协程中被调用。
|
||||
`startServer` 则会在 `main` 协程中被调用。
|
||||
|
||||
在以下测试例子中,100个请求会被发送到服务器,只有它们全部被送达后我们才会按相反的顺序检查响应:
|
||||
在以下测试例子中,100 个请求会被发送到服务器,只有它们全部被送达后我们才会按相反的顺序检查响应:
|
||||
```go
|
||||
func main() {
|
||||
adder := startServer(func(a, b int) int { return a + b })
|
||||
@@ -81,7 +81,7 @@ func main() {
|
||||
fmt.Println(“done”)
|
||||
}
|
||||
```
|
||||
这些代码可以在[multiplex_server.go](examples/chapter_14/multiplex_server.go)找到
|
||||
这些代码可以在 [multiplex_server.go](examples/chapter_14/multiplex_server.go) 找到
|
||||
|
||||
输出:
|
||||
|
||||
@@ -93,9 +93,10 @@ func main() {
|
||||
done
|
||||
|
||||
|
||||
这个程序仅启动了100个协程。然而即使执行100,000个协程我们也能在数秒内看到它完成。这说明了Go的协程是如何的轻量:如果我们启动相同数量的真实的线程,程序早就崩溃了。
|
||||
这个程序仅启动了 100 个协程。然而即使执行 100,000 个协程我们也能在数秒内看到它完成。这说明了 Go 的协程是如何的轻量:如果我们启动相同数量的真实的线程,程序早就崩溃了。
|
||||
|
||||
示例: 14.14-[multiplex_server.go](examples/chapter_14/multiplex_server.go)
|
||||
|
||||
示例: [14.14-multiplex_server.go](examples/chapter_14/multiplex_server.go)
|
||||
```go
|
||||
package main
|
||||
|
||||
@@ -151,7 +152,7 @@ func main() {
|
||||
```
|
||||
## 14.10.2 卸载(Teardown):通过信号通道关闭服务器
|
||||
|
||||
在上一个版本中`server`在`main`函数返回后并没有完全关闭,而被强制结束了。为了改进这一点,我们可以提供一个退出通道给`server`:
|
||||
在上一个版本中 `server` 在 `main` 函数返回后并没有完全关闭,而被强制结束了。为了改进这一点,我们可以提供一个退出通道给 `server` :
|
||||
|
||||
```go
|
||||
func startServer(op binOp) (service chan *Request, quit chan bool) {
|
||||
@@ -162,7 +163,7 @@ func startServer(op binOp) (service chan *Request, quit chan bool) {
|
||||
}
|
||||
```
|
||||
|
||||
`server`函数现在则使用`select`在`service`通道和`quit`通道之间做出选择:
|
||||
`server` 函数现在则使用 `select` 在 `service` 通道和 `quit` 通道之间做出选择:
|
||||
|
||||
```go
|
||||
func server(op binOp, service chan *request, quit chan bool) {
|
||||
@@ -176,17 +177,19 @@ func server(op binOp, service chan *request, quit chan bool) {
|
||||
}
|
||||
}
|
||||
```
|
||||
当`quit`通道接收到一个`true`值时,`server`就会返回并结束。
|
||||
当 `quit` 通道接收到一个 `true` 值时,`server` 就会返回并结束。
|
||||
|
||||
在`main`函数中我们做出如下更改:
|
||||
在 `main` 函数中我们做出如下更改:
|
||||
|
||||
```go
|
||||
adder, quit := startServer(func(a, b int) int { return a + b })
|
||||
```
|
||||
|
||||
在`main`函数的结尾处我们放入这一行:`quit <- true`
|
||||
在 `main` 函数的结尾处我们放入这一行:`quit <- true`
|
||||
|
||||
完整的代码在 multiplex_server2.go,输出和上一个版本是一样的。
|
||||
完整的代码在 [multiplex_server2.go](examples/chapter_14/multiplex_server2.go),输出和上一个版本是一样的。
|
||||
|
||||
示例: [14.15-multiplex_server2.go](examples/chapter_14/multiplex_server2.go)
|
||||
示例: 14.15-[multiplex_server2.go](examples/chapter_14/multiplex_server2.go)
|
||||
```go
|
||||
package main
|
||||
|
||||
@@ -244,7 +247,9 @@ func main() {
|
||||
fmt.Println("done")
|
||||
}
|
||||
```
|
||||
练习 [14.13 multiplex_server3.go](exercises/chapter_14/multiplex_server3.go):使用之前的例子,编写一个在`Request`结构上带有`String()`方法的版本,它能决定服务器如何输出;并使用以下两个请求来测试这个程序:
|
||||
|
||||
练习 14.13 [multiplex_server3.go](exercises/chapter_14/multiplex_server3.go):使用之前的例子,编写一个在 `Request` 结构上带有 `String()` 方法的版本,它能决定服务器如何输出;并使用以下两个请求来测试这个程序:
|
||||
|
||||
```go
|
||||
req1 := &Request{3, 4, make(chan int)}
|
||||
req2 := &Request{150, 250, make(chan int)}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
# 14.11 限制同时处理的请求数
|
||||
|
||||
使用带缓冲区的通道很容易实现这一点(参见 [14.2.5](14.2.md#1425-%E5%90%8C%E6%AD%A5%E9%80%9A%E9%81%93-%E4%BD%BF%E7%94%A8%E5%B8%A6%E7%BC%93%E5%86%B2%E7%9A%84%E9%80%9A%E9%81%93)),其缓冲区容量就是同时处理请求的最大数量。程序[max_tasks.go](examples/chapter_14/max_tasks.go)虽然没有做什么有用的事但是却包含了这个技巧:超过`MAXREQS`的请求将不会被同时处理,因为当信号通道表示缓冲区已满时`handle`函数会阻塞且不再处理其他请求,直到某个请求从`sem`中被移除。`sem`就像一个信号量,这一专业术语用于在程序中表示特定条件的标志变量。
|
||||
使用带缓冲区的通道很容易实现这一点(参见 [14.2.5](14.2.md#1425-%E5%90%8C%E6%AD%A5%E9%80%9A%E9%81%93-%E4%BD%BF%E7%94%A8%E5%B8%A6%E7%BC%93%E5%86%B2%E7%9A%84%E9%80%9A%E9%81%93)),其缓冲区容量就是同时处理请求的最大数量。程序 [max_tasks.go](examples/chapter_14/max_tasks.go) 虽然没有做什么有用的事但是却包含了这个技巧:超过 `MAXREQS` 的请求将不会被同时处理,因为当信号通道表示缓冲区已满时 `handle` 函数会阻塞且不再处理其他请求,直到某个请求从 `sem` 中被移除。`sem` 就像一个信号量,这一专业术语用于在程序中表示特定条件的标志变量。
|
||||
|
||||
示例:[14.16-max_tasks.go](examples/chapter_14/max_tasks.go)
|
||||
示例:14.16-[max_tasks.go](examples/chapter_14/max_tasks.go)
|
||||
```go
|
||||
package main
|
||||
|
||||
|
@@ -1,13 +1,13 @@
|
||||
# 14.12 链式协程
|
||||
|
||||
下面的演示程序 [chaining.go](examples/chapter_14/chaining.go) 再次展示了启动巨量的Go协程是多么容易。这些协程已全部在 main 函数中的 for
|
||||
循环里启动。当循环完成之后,一个0被写入到最右边的通道里,于是100,000个协程开始执行,接着`1000000`这个结果会在1.5秒之内被打印出来。
|
||||
下面的演示程序 [chaining.go](examples/chapter_14/chaining.go) 再次展示了启动巨量的 Go 协程是多么容易。这些协程已全部在 main 函数中的 for
|
||||
循环里启动。当循环完成之后,一个 0 被写入到最右边的通道里,于是 100,000 个协程开始执行,接着 `1000000` 这个结果会在 1.5 秒之内被打印出来。
|
||||
|
||||
|
||||
这个程序同时也展示了如何通过`flag.Int`来解析命令行中的参数以指定协程数量,例如:`chaining -n=7000`会生成7000个协程。
|
||||
这个程序同时也展示了如何通过 `flag.Int` 来解析命令行中的参数以指定协程数量,例如:`chaining -n=7000` 会生成 7000 个协程。
|
||||
|
||||
|
||||
[示例 14.17 - chaining.go](examples/chapter_14/chaining.go)
|
||||
示例 14.17-[chaining.go](examples/chapter_14/chaining.go)
|
||||
```go
|
||||
package main
|
||||
|
||||
@@ -30,16 +30,16 @@ func main() {
|
||||
}
|
||||
right <- 0 // bang!
|
||||
x := <-leftmost // wait for completion
|
||||
fmt.Println(x) // 100000, ongeveer 1,5 s
|
||||
fmt.Println(x) // 100000, about 1.5 s
|
||||
}
|
||||
```
|
||||
*译者注:原本认为leftmost的结果为1,认为只在最初做了一次赋值,实际结果为100000(无缓存信道具有同步阻塞的特性)*
|
||||
*译者注:原本认为 leftmost 的结果为 1 ,认为只在最初做了一次赋值,实际结果为 100000(无缓存信道具有同步阻塞的特性)*
|
||||
|
||||
*1.主线程的right <- 0,right不是最初循环的那个right,而是最终循环的right*
|
||||
*1. 主线程的 right <- 0,right 不是最初循环的那个 right,而是最终循环的 right*
|
||||
|
||||
*2.for循环中最初的go f(left, right)因为没有发送者一直处于等待状态*
|
||||
*2. for 循环中最初的 go f(left, right) 因为没有发送者一直处于等待状态*
|
||||
|
||||
*3.当主线程的right <- 0执行时,类似于递归函数在最内层产生返回值一般*
|
||||
*3. 当主线程的 right <- 0 执行时,类似于递归函数在最内层产生返回值一般*
|
||||
|
||||
## 链接
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# 14.13 在多核心上并行计算
|
||||
|
||||
假设我们有`NCPU`个CPU核心:`const NCPU = 4 //对应一个四核处理器` 然后我们想把计算量分成`NCPU`个部分,每一个部分都和其他部分并行运行。
|
||||
假设我们有 `NCPU` 个 CPU 核心:`const NCPU = 4 //对应一个四核处理器` 然后我们想把计算量分成 `NCPU` 个部分,每一个部分都和其他部分并行运行。
|
||||
|
||||
这可以通过以下代码所示的方式完成(我们且省略具体参数)
|
||||
|
||||
@@ -26,13 +26,13 @@ func main() {
|
||||
runtime.GOMAXPROCS(NCPU) // runtime.GOMAXPROCS = NCPU
|
||||
DoAll()
|
||||
}
|
||||
|
||||
```
|
||||
- `DoAll()`函数创建了一个`sem`通道,每个并行计算都将在对其发送完成信号;在一个 for 循环中`NCPU`个协程被启动了,每个协程会承担`1/NCPU`的工作量。每一个`DoPart()`协程都会向`sem`通道发送完成信号。
|
||||
|
||||
- `DoAll()`会在 for 循环中等待`NCPU`个协程完成:`sem`通道就像一个信号量,这份代码展示了一个经典的信号量模式。(参见 [14.2.7](14.2.md#1427-%E4%BF%A1%E5%8F%B7%E9%87%8F%E6%A8%A1%E5%BC%8F))
|
||||
- `DoAll()` 函数创建了一个 `sem` 通道,每个并行计算都将在对其发送完成信号;在一个 for 循环中 `NCPU` 个协程被启动了,每个协程会承担 `1/NCPU` 的工作量。每一个 `DoPart()` 协程都会向 `sem` 通道发送完成信号。
|
||||
|
||||
在以上运行模型中,您还需将`GOMAXPROCS`设置为`NCPU`(参见 [14.1.3](14.1.md#1413-%E4%BD%BF%E7%94%A8-gomaxprocs))。
|
||||
- `DoAll()` 会在 for 循环中等待 `NCPU` 个协程完成:`sem` 通道就像一个信号量,这份代码展示了一个经典的信号量模式。(参见 [14.2.7](14.2.md#1427-%E4%BF%A1%E5%8F%B7%E9%87%8F%E6%A8%A1%E5%BC%8F))
|
||||
|
||||
在以上运行模型中,您还需将 `GOMAXPROCS` 设置为 `NCPU`(参见 [14.1.3](14.1.md#1413-%E4%BD%BF%E7%94%A8-gomaxprocs))。
|
||||
|
||||
|
||||
## 链接
|
||||
|
@@ -1,8 +1,9 @@
|
||||
# 14.14 并行化大量数据的计算
|
||||
|
||||
假设我们需要处理一些数量巨大且互不相关的数据项,它们从一个`in`通道被传递进来,当我们处理完以后又要将它们放入另一个`out`通道,就像一个工厂流水线一样。处理每个数据项也可能包含许多步骤:Preprocess(预处理) / StepA(步骤A) / StepB(步骤B) / ... / PostProcess(后处理)
|
||||
假设我们需要处理一些数量巨大且互不相关的数据项,它们从一个 `in` 通道被传递进来,当我们处理完以后又要将它们放入另一个 `out` 通道,就像一个工厂流水线一样。处理每个数据项也可能包含许多步骤:Preprocess(预处理) / StepA(步骤A) / StepB(步骤B) / ... / PostProcess(后处理)
|
||||
|
||||
一个典型的用于解决按顺序执行每个步骤的顺序流水线算法可以写成下面这样:
|
||||
|
||||
```go
|
||||
func SerialProcessData(in <-chan *Data, out chan<- *Data) {
|
||||
for data := range in {
|
||||
@@ -14,11 +15,12 @@ func SerialProcessData(in <-chan *Data, out chan<- *Data) {
|
||||
}
|
||||
```
|
||||
|
||||
一次只执行一个步骤,并且按顺序处理每个项目:在第1个项目没有被`PostProcess`并放入`out`通道之前绝不会处理第2个项目。
|
||||
一次只执行一个步骤,并且按顺序处理每个项目:在第 1 个项目没有被 `PostProcess` 并放入 `out` 通道之前绝不会处理第 2 个项目。
|
||||
|
||||
如果你仔细想想,你很快就会发现这将会造成巨大的时间浪费。
|
||||
|
||||
一个更高效的计算方式是让每一个处理步骤作为一个协程独立工作。每一个步骤从上一步的输出通道中获得输入数据。这种方式仅有极少数时间会被浪费,而大部分时间所有的步骤都在一直执行中:
|
||||
|
||||
```go
|
||||
func ParallelProcessData (in <-chan *Data, out chan<- *Data) {
|
||||
// make channels:
|
||||
@@ -34,6 +36,7 @@ func ParallelProcessData (in <-chan *Data, out chan<- *Data) {
|
||||
go PostProcessData(StepCOut,out)
|
||||
}
|
||||
```
|
||||
|
||||
通道的缓冲区大小可以用来进一步优化整个过程。
|
||||
|
||||
|
||||
|
@@ -2,34 +2,37 @@
|
||||
|
||||
(译者注:翻译遵照原文,但是对于完全没听过这个算法的人来说比较晦涩,请配合代码片段理解)
|
||||
|
||||
考虑以下的客户端-服务器结构:客户端协程执行一个无限循环从某个源头(也许是网络)接收数据;数据读取到`Buffer`类型的缓冲区。为了避免分配过多的缓冲区以及释放缓冲区,它保留了一份空闲缓冲区列表,并且使用一个缓冲通道来表示这个列表:`var freeList = make(chan *Buffer,100)`
|
||||
考虑以下的客户端-服务器结构:客户端协程执行一个无限循环从某个源头(也许是网络)接收数据;数据读取到 `Buffer` 类型的缓冲区。为了避免分配过多的缓冲区以及释放缓冲区,它保留了一份空闲缓冲区列表,并且使用一个缓冲通道来表示这个列表:`var freeList = make(chan *Buffer,100)`
|
||||
|
||||
这个可重用的缓冲区队列(freeList)与服务器是共享的。 当接收数据时,客户端尝试从`freeList`获取缓冲区; 但如果此时通道为空,则会分配新的缓冲区。 一旦消息被加载后,它将被发送到服务器上的`serverChan`通道:
|
||||
这个可重用的缓冲区队列(freeList)与服务器是共享的。 当接收数据时,客户端尝试从 `freeList` 获取缓冲区;但如果此时通道为空,则会分配新的缓冲区。一旦消息被加载后,它将被发送到服务器上的 `serverChan` 通道:
|
||||
|
||||
```go
|
||||
var serverChan = make(chan *Buffer)
|
||||
```
|
||||
|
||||
以下是客户端的算法代码:
|
||||
|
||||
```go
|
||||
func client() {
|
||||
for {
|
||||
var b *Buffer
|
||||
// Grab a buffer if available; allocate if not
|
||||
select {
|
||||
case b = <-freeList:
|
||||
// Got one; nothing more to do
|
||||
default:
|
||||
// None free, so allocate a new one
|
||||
b = new(Buffer)
|
||||
}
|
||||
loadInto(b) // Read next message from the network
|
||||
serverChan <- b // Send to server
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func client() {
|
||||
for {
|
||||
var b *Buffer
|
||||
// Grab a buffer if available; allocate if not
|
||||
select {
|
||||
case b = <-freeList:
|
||||
// Got one; nothing more to do
|
||||
default:
|
||||
// None free, so allocate a new one
|
||||
b = new(Buffer)
|
||||
}
|
||||
loadInto(b) // Read next message from the network
|
||||
serverChan <- b // Send to server
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
服务器的循环则接收每一条来自客户端的消息并处理它,之后尝试将缓冲返回给共享的空闲缓冲区:
|
||||
|
||||
```go
|
||||
func server() {
|
||||
for {
|
||||
@@ -45,11 +48,12 @@ func server() {
|
||||
}
|
||||
}
|
||||
```
|
||||
但是这种方法在`freeList`通道已满的时候是行不通的,因为无法放入空闲`freeList`通道的缓冲区会被“丢到地上”由垃圾收集器回收(故名:漏桶算法)
|
||||
|
||||
但是这种方法在 `freeList` 通道已满的时候是行不通的,因为无法放入空闲 `freeList` 通道的缓冲区会被“丢到地上”由垃圾收集器回收(故名:漏桶算法)
|
||||
|
||||
|
||||
## 链接
|
||||
|
||||
- [目录](directory.md)
|
||||
- 上一节:[并行化大量数据的计算](14.14.md)
|
||||
- 下一节:[对Go协程进行基准测试](14.16.md)
|
||||
- 下一节:[对 Go 协程进行基准测试](14.16.md)
|
||||
|
@@ -1,10 +1,10 @@
|
||||
# 14.16 对Go协程进行基准测试
|
||||
# 14.16 对 Go 协程进行基准测试
|
||||
|
||||
在 [13.7 节](13.7.md) 我们提到了在Go语言中对你的函数进行基准测试。在此我们将其应用到一个用协程向通道写入整数再读出的实例中。这个函数将通过`testing.Benchmark`调用`N`次(例如:`N = 1,000,000`),`BenchMarkResult`有一个`String()`方法来输出其结果。`N`的值将由`gotest`来判断并取得一个足够大的数字,以获得合理的基准测试结果。当然同样的基准测试方法也适用于普通函数。
|
||||
在 [13.7 节](13.7.md) 我们提到了在 Go 语言中对你的函数进行基准测试。在此我们将其应用到一个用协程向通道写入整数再读出的实例中。这个函数将通过 `testing.Benchmark` 调用 `N` 次(例如:`N = 1,000,000`),`BenchMarkResult` 有一个 `String()` 方法来输出其结果。`N` 的值将由 `gotest` 来判断并取得一个足够大的数字,以获得合理的基准测试结果。当然同样的基准测试方法也适用于普通函数。
|
||||
|
||||
如果你想排除指定部分的代码或者更具体的指定要测试的部分,可以使用`testing.B.startTimer()`和`testing.B.stopTimer()`来开始或结束计时器。基准测试只有在所有的测试通过后才能运行!
|
||||
如果你想排除指定部分的代码或者更具体的指定要测试的部分,可以使用 `testing.B.startTimer()` 和 `testing.B.stopTimer()` 来开始或结束计时器。基准测试只有在所有的测试通过后才能运行!
|
||||
|
||||
示例:[14.18-benchmark_channels.go](examples/chapter_14/benchmark_channels.go)
|
||||
示例:14.18-[benchmark_channels.go](examples/chapter_14/benchmark_channels.go)
|
||||
|
||||
```go
|
||||
package main
|
||||
@@ -43,7 +43,9 @@ func BenchmarkChannelBuffered(b *testing.B) {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出:
|
||||
|
||||
```
|
||||
Output:Windows: N Time 1 op Operations per sec
|
||||
sync 1000000 2443 ns/op --> 409 332 / s
|
||||
@@ -57,5 +59,3 @@ func BenchmarkChannelBuffered(b *testing.B) {
|
||||
- [目录](directory.md)
|
||||
- 上一节:[漏桶算法](14.15.md)
|
||||
- 下一节:[使用通道并发访问对象](14.17.md)
|
||||
|
||||
|
||||
|
@@ -1,13 +1,13 @@
|
||||
# 14.17 使用通道并发访问对象
|
||||
|
||||
为了保护对象被并发访问修改,我们可以使用协程在后台顺序执行匿名函数来替代使用同步互斥锁。在下面的程序中我们有一个类型`Person`其中包含一个字段`chF`,这是一个用于存放匿名函数的通道。
|
||||
为了保护对象被并发访问修改,我们可以使用协程在后台顺序执行匿名函数来替代使用同步互斥锁。在下面的程序中我们有一个类型 `Person` 中包含一个字段 `chF` ,这是一个用于存放匿名函数的通道。
|
||||
|
||||
这个结构在构造函数`NewPerson()`中初始化的同时会启动一个后台协程`backend()`。`backend()`方法会在一个无限循环中执行`chF`中放置的所有函数,有效的将它们序列化从而提供了安全的并发访问。更改和读取`salary`的方法会通过将一个匿名函数写入`chF`通道中,然后让`backend()`按顺序执行以达到其目的。需注意的是`Salary`方法创建的闭包函数是如何将`fChan`通道包含在其中的。
|
||||
这个结构在构造函数 `NewPerson()` 中初始化的同时会启动一个后台协程 `backend()`。`backend()` 方法会在一个无限循环中执行 `chF` 中放置的所有函数,有效地将它们序列化从而提供了安全的并发访问。更改和读取 `salary` 的方法会通过将一个匿名函数写入 `chF` 通道中,然后让`backend()`按顺序执行以达到其目的。需注意的是 `Salary` 方法创建的闭包函数是如何将 `fChan` 通道包含在其中的。
|
||||
|
||||
当然,这是一个简化的例子,它不应该被用在这种案例下。但是它却向我们展示了在更复杂的场景中该如何解决这种问题。
|
||||
|
||||
示例:14.19-[conc_access.go](examples/chapter_14/conc_access.go)
|
||||
|
||||
示例:[14.19-conc_access.go](examples/chapter_14/conc_access.go)
|
||||
```go
|
||||
package main
|
||||
|
||||
@@ -58,7 +58,9 @@ func main() {
|
||||
fmt.Println(bs)
|
||||
}
|
||||
```
|
||||
|
||||
输出:
|
||||
|
||||
```
|
||||
Person - name is: Smith Bill - salary is: 2500.50
|
||||
Salary changed:
|
||||
|
@@ -14,9 +14,9 @@
|
||||
|
||||
通常使用这样的格式来声明通道:`var identifier chan datatype`
|
||||
|
||||
未初始化的通道的值是nil。
|
||||
未初始化的通道的值是 nil 。
|
||||
|
||||
所以通道只能传输一种类型的数据,比如 `chan int` 或者 `chan string`,所有的类型都可以用于通道,空接口 `interface{}` 也可以。甚至可以(有时非常有用)创建通道的通道。
|
||||
所以通道只能传输一种类型的数据,比如 `chan int` 或者 `chan string`,所有的类型都可以用于通道,空接口 `interface{}` 也可以,甚至可以(有时非常有用)创建通道的通道。
|
||||
|
||||
通道实际上是类型化消息的队列:使数据得以传输。它是先进先出(FIFO)的结构所以可以保证发送给他们的元素的顺序(有些人知道,通道可以比作 Unix shells 中的双向管道(two-way pipe))。通道也是引用类型,所以我们使用 `make()` 函数来给它分配内存。这里先声明了一个字符串通道 ch1,然后创建了它(实例化):
|
||||
|
||||
@@ -27,7 +27,7 @@ ch1 = make(chan string)
|
||||
|
||||
当然可以更短: `ch1 := make(chan string)`。
|
||||
|
||||
这里我们构建一个int通道的通道: `chanOfChans := make(chan chan int)`。
|
||||
这里我们构建一个 int 通道的通道: `chanOfChans := make(chan chan int)`。
|
||||
|
||||
或者函数通道:`funcChan := make(chan func())`(相关示例请看第 [14.17](14.17.md) 节)。
|
||||
|
||||
@@ -53,7 +53,7 @@ if <- ch != 1000{
|
||||
}
|
||||
```
|
||||
|
||||
同一个操作符 `<-` 既用于**发送**也用于**接收**,但Go会根据操作对象弄明白该干什么 。虽非强制要求,但为了可读性通道的命名通常以 `ch` 开头或者包含 `chan`。通道的发送和接收都是原子操作:它们总是互不干扰的完成的。下面的示例展示了通信操作符的使用。
|
||||
同一个操作符 `<-` 既用于**发送**也用于**接收**,但 Go 会根据操作对象弄明白该干什么 。虽非强制要求,但为了可读性通道的命名通常以 `ch` 开头或者包含 `chan` 。通道的发送和接收都是原子操作:它们总是互不干扰地完成。下面的示例展示了通信操作符的使用。
|
||||
|
||||
示例 14.2-[goroutine2.go](examples/chapter_14/goroutine2.go)
|
||||
|
||||
@@ -124,13 +124,13 @@ Washington Tripoli London Beijing tokyo
|
||||
|
||||
默认情况下,通信是同步且无缓冲的:在有接受者接收数据之前,发送不会结束。可以想象一个无缓冲的通道在没有空间来保存数据的时候:必须要一个接收者准备好接收通道的数据然后发送者可以直接把数据发送给接收者。所以通道的发送/接收操作在对方准备好之前是阻塞的:
|
||||
|
||||
1)对于同一个通道,发送操作(协程或者函数中的),在接收者准备好之前是阻塞的:如果ch中的数据无人接收,就无法再给通道传入其他数据:新的输入无法在通道非空的情况下传入。所以发送操作会等待 ch 再次变为可用状态:就是通道值被接收时(可以传入变量)。
|
||||
1)对于同一个通道,发送操作(协程或者函数中的),在接收者准备好之前是阻塞的:如果 ch 中的数据无人接收,就无法再给通道传入其他数据:新的输入无法在通道非空的情况下传入。所以发送操作会等待 ch 再次变为可用状态:就是通道值被接收时(可以传入变量)。
|
||||
|
||||
2)对于同一个通道,接收操作是阻塞的(协程或函数中的),直到发送者可用:如果通道中没有数据,接收者就阻塞了。
|
||||
|
||||
尽管这看上去是非常严格的约束,实际在大部分情况下工作的很不错。
|
||||
|
||||
程序 `channel_block.go` 验证了以上理论,一个协程在无限循环中给通道发送整数数据。不过因为没有接收者,只输出了一个数字 0。
|
||||
程序 [channel_block.go](examples/chapter_14/channel_block.go) 验证了以上理论,一个协程在无限循环中给通道发送整数数据。不过因为没有接收者,只输出了一个数字 0。
|
||||
|
||||
示例 14.3-[channel_block.go](examples/chapter_14/channel_block.go)
|
||||
|
||||
@@ -226,12 +226,12 @@ buf 是通道可以同时容纳的元素(这里是 string)个数
|
||||
|
||||
缓冲容量和类型无关,所以可以(尽管可能导致危险)给一些通道设置不同的容量,只要他们拥有同样的元素类型。内置的 `cap` 函数可以返回缓冲区的容量。
|
||||
|
||||
如果容量大于 0,通道就是异步的了:缓冲满载(发送)或变空(接收)之前通信不会阻塞,元素会按照发送的顺序被接收。如果容量是0或者未设置,通信仅在收发双方准备好的情况下才可以成功。
|
||||
如果容量大于 0,通道就是异步的了:缓冲满载(发送)或变空(接收)之前通信不会阻塞,元素会按照发送的顺序被接收。如果容量是 0 或者未设置,通信仅在收发双方准备好的情况下才可以成功。
|
||||
|
||||
同步:`ch :=make(chan type, value)`
|
||||
|
||||
- value == 0 -> synchronous, unbuffered (阻塞)
|
||||
- value > 0 -> asynchronous, buffered(非阻塞)取决于value元素
|
||||
- value > 0 -> asynchronous, buffered(非阻塞)取决于 value 元素
|
||||
|
||||
若使用通道的缓冲,你的程序会在“请求”激增的时候表现更好:更具弹性,专业术语叫:更具有伸缩性(scalable)。在设计算法时首先考虑使用无缓冲通道,只在不确定的情况下使用缓冲。
|
||||
|
||||
@@ -285,7 +285,7 @@ doSomethingElseForAWhile()
|
||||
<- ch // Wait for goroutine to finish; discard sent value.
|
||||
```
|
||||
|
||||
或者等待两个协程完成,每一个都会对切片s的一部分进行排序,片段如下:
|
||||
或者等待两个协程完成,每一个都会对切片 s 的一部分进行排序,片段如下:
|
||||
|
||||
```go
|
||||
done := make(chan bool)
|
||||
@@ -301,7 +301,7 @@ go doSort(s[i:])
|
||||
<-done
|
||||
```
|
||||
|
||||
下边的代码,用完整的信号量模式对长度为N的 float64 切片进行了 N 个` doSomething()` 计算并同时完成,通道 sem 分配了相同的长度(且包含空接口类型的元素),待所有的计算都完成后,发送信号(通过放入值)。在循环中从通道 sem 不停的接收数据来等待所有的协程完成。
|
||||
下边的代码,用完整的信号量模式对长度为 N 的 float64 切片进行了 N 个 `doSomething()` 计算并同时完成,通道 sem 分配了相同的长度(且包含空接口类型的元素),待所有的计算都完成后,发送信号(通过放入值)。在循环中从通道 sem 不停的接收数据来等待所有的协程完成。
|
||||
|
||||
```go
|
||||
type Empty interface {}
|
||||
@@ -321,7 +321,7 @@ for i, xi := range data {
|
||||
for i := 0; i < N; i++ { <-sem }
|
||||
```
|
||||
|
||||
注意上述代码中闭合函数的用法:`i`、`xi` 都是作为参数传入闭合函数的,这一做法使得每个协程(译者注:在其启动时)获得一份 `i` 和 `xi` 的单独拷贝,从而向闭合函数内部屏蔽了外层循环中的 `i` 和 `xi`变量;否则,for 循环的下一次迭代会更新所有协程中 `i` 和 `xi` 的值。另一方面,切片 `res` 没有传入闭合函数,因为协程不需要`res`的单独拷贝。切片 `res` 也在闭合函数中但并不是参数。
|
||||
注意上述代码中闭合函数的用法:`i`、`xi` 都是作为参数传入闭合函数的,这一做法使得每个协程(译者注:在其启动时)获得一份 `i` 和 `xi` 的单独拷贝,从而向闭合函数内部屏蔽了外层循环中的 `i` 和 `xi` 变量;否则,for 循环的下一次迭代会更新所有协程中 `i` 和 `xi` 的值。另一方面,切片 `res` 没有传入闭合函数,因为协程不需要 `res` 的单独拷贝。切片 `res` 也在闭合函数中但并不是参数。
|
||||
|
||||
|
||||
## 14.2.8 实现并行的 for 循环
|
||||
@@ -347,7 +347,7 @@ for i, v := range data {
|
||||
- 通道的长度(当前存放的元素个数)与当前资源被使用的数量相同
|
||||
- 容量减去通道的长度就是未处理的资源个数(标准信号量的整数值)
|
||||
|
||||
不用管通道中存放的是什么,只关注长度;因此我们创建了一个长度可变但容量为0(字节)的通道:
|
||||
不用管通道中存放的是什么,只关注长度;因此我们创建了一个长度可变但容量为 0(字节)的通道:
|
||||
|
||||
```go
|
||||
type Empty interface {}
|
||||
@@ -397,7 +397,7 @@ func (s semaphore) Signal() {
|
||||
}
|
||||
```
|
||||
|
||||
练习 14.5:[gosum.go](exercises/chapter_14/gosum.go):用这种习惯用法写一个程序,开启一个协程来计算2个整数的和并等待计算结果并打印出来。
|
||||
练习 14.5:[gosum.go](exercises/chapter_14/gosum.go):用这种习惯用法写一个程序,开启一个协程来计算 2 个整数的和并等待计算结果并打印出来。
|
||||
|
||||
练习 14.6:[producer_consumer.go](exercises/chapter_14/producer_consumer.go):用这种习惯用法写一个程序,有两个协程,第一个提供数字 0,10,20,...90 并将他们放入通道,第二个协程从通道中读取并打印。`main()` 等待两个协程完成后再结束。
|
||||
|
||||
@@ -486,7 +486,7 @@ func suck(ch chan int) {
|
||||
|
||||
习惯用法:通道迭代模式
|
||||
|
||||
这个模式用到了后边14.6章示例 [producer_consumer.go](exercises/chapter_14/producer_consumer.go) 的生产者-消费者模式,通常,需要从包含了地址索引字段 items 的容器给通道填入元素。为容器的类型定义一个方法 `Iter()`,返回一个只读的通道(参见第 [14.2.11](14.2.md#14211-通道的方向) 节)items,如下:
|
||||
这个模式用到了后边 14.6 章示例 [producer_consumer.go](exercises/chapter_14/producer_consumer.go) 的生产者-消费者模式,通常,需要从包含了地址索引字段 items 的容器给通道填入元素。为容器的类型定义一个方法 `Iter()`,返回一个只读的通道(参见第 [14.2.11](14.2.md#14211-通道的方向) 节)items,如下:
|
||||
|
||||
```go
|
||||
func (c *container) Iter () <- chan item {
|
||||
@@ -508,7 +508,7 @@ func (c *container) Iter () <- chan item {
|
||||
for x := range container.Iter() { ... }
|
||||
```
|
||||
|
||||
其运行在自己启动的协程中,所以上边的迭代用到了一个通道和两个协程(可能运行在不同的线程上)。 这样我们就有了一个典型的生产者-消费者模式。如果在程序结束之前,向通道写值的协程未完成工作,则这个协程不会被垃圾回收;这是设计使然。这种看起来并不符合预期的行为正是由通道这种线程安全的通信方式所导致的。如此一来,一个协程为了写入一个永远无人读取的通道而被挂起就成了一个bug,而并非你预想中的那样被悄悄回收掉(garbage-collected)了。
|
||||
其运行在自己启动的协程中,所以上边的迭代用到了一个通道和两个协程(可能运行在不同的线程上)。 这样我们就有了一个典型的生产者-消费者模式。如果在程序结束之前,向通道写值的协程未完成工作,则这个协程不会被垃圾回收;这是设计使然。这种看起来并不符合预期的行为正是由通道这种线程安全的通信方式所导致的。如此一来,一个协程为了写入一个永远无人读取的通道而被挂起就成了一个 bug ,而并非你预想中的那样被悄悄回收掉(garbage-collected)了。
|
||||
|
||||
习惯用法:生产者消费者模式
|
||||
|
||||
|
@@ -52,7 +52,7 @@ default:
|
||||
}
|
||||
```
|
||||
|
||||
在示例程序 14.2 中使用这些可以改进为版本 goroutine3.go,输出相同。
|
||||
在示例程序 14.2 中使用这些可以改进为版本 [goroutine3.go](examples/chapter_14/goroutine3.go),输出相同。
|
||||
|
||||
实现非阻塞通道的读取,需要使用 select(参见第 [14.4](14.4.md) 节)。
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# 14.4 使用 select 切换协程
|
||||
|
||||
从不同的并发执行的协程中获取值可以通过关键字`select`来完成,它和`switch`控制语句非常相似(章节5.3)也被称作通信开关;它的行为像是“你准备好了吗”的轮询机制;`select`监听进入通道的数据,也可以是用通道发送值的时候。
|
||||
从不同的并发执行的协程中获取值可以通过关键字 `select` 来完成,它和 `switch` 控制语句非常相似(章节5.3)也被称作通信开关;它的行为像是“你准备好了吗”的轮询机制;`select` 监听进入通道的数据,也可以是用通道发送值的时候。
|
||||
|
||||
```go
|
||||
select {
|
||||
@@ -91,13 +91,13 @@ Received on channel 1: 94346
|
||||
Received on channel 1: 94348
|
||||
```
|
||||
|
||||
一秒内的输出非常惊人,如果我们给它计数(goroutine_select2.go),得到了 90000 个左右的数字。
|
||||
一秒内的输出非常惊人,如果我们给它计数([goroutine_select2.go](examples/chapter_14/goroutine_select2.go)),得到了 90000 个左右的数字。
|
||||
|
||||
## 练习:
|
||||
|
||||
练习 14.7:
|
||||
|
||||
- a)在练习 5.4 的 for_loop.go 中,有一个常见的 for 循环打印数字。在函数 `tel` 中实现一个 for 循环,用协程开始这个函数并在其中给通道发送数字。`main()` 线程从通道中获取并打印。不要使用 `time.Sleep()` 来同步:[goroutine_panic.go](exercises/chapter_14/goroutine_panic.go)
|
||||
- a)在练习 5.4 的 [for_loop.go](exercises/chapter_5/for_loop.go) 中,有一个常见的 for 循环打印数字。在函数 `tel` 中实现一个 for 循环,用协程开始这个函数并在其中给通道发送数字。`main()` 线程从通道中获取并打印。不要使用 `time.Sleep()` 来同步:[goroutine_panic.go](exercises/chapter_14/goroutine_panic.go)
|
||||
- b)也许你的方案有效,可能会引发运行时的 panic:`throw:all goroutines are asleep-deadlock!` 为什么会这样?你如何解决这个问题?[goroutine_close.go](exercises/chapter_14/goroutine_close.go)
|
||||
- c)解决 a)的另外一种方式:使用一个额外的通道传递给协程,然后在结束的时候随便放点什么进去。`main()` 线程检查是否有数据发送给了这个通道,如果有就停止:[goroutine_select.go](exercises/chapter_14/goroutine_select.go)
|
||||
|
||||
|
@@ -16,7 +16,7 @@ type Ticker struct {
|
||||
|
||||
在协程周期性的执行一些事情(打印状态日志,输出,计算等等)的时候非常有用。
|
||||
|
||||
调用 `Stop()` 使计时器停止,在 `defer` 语句中使用。这些都很好的适应 `select` 语句:
|
||||
调用 `Stop()` 使计时器停止,在 `defer` 语句中使用。这些都很好地适应 `select` 语句:
|
||||
|
||||
```go
|
||||
ticker := time.NewTicker(updateInterval)
|
||||
@@ -34,7 +34,7 @@ default: // no value ready to be received
|
||||
}
|
||||
```
|
||||
|
||||
`time.Tick()` 函数声明为 `Tick(d Duration) <-chan Time`,当你想返回一个通道而不必关闭它的时候这个函数非常有用:它以 d 为周期给返回的通道发送时间,d是纳秒数。如果需要像下边的代码一样,限制处理频率(函数 `client.Call()` 是一个 RPC 调用,这里暂不赘述(参见第 [15.9](15.9.md) 节):
|
||||
`time.Tick()` 函数声明为 `Tick(d Duration) <-chan Time`,当你想返回一个通道而不必关闭它的时候这个函数非常有用:它以 d 为周期给返回的通道发送时间,d 是纳秒数。如果需要像下边的代码一样,限制处理频率(函数 `client.Call()` 是一个 RPC 调用,这里暂不赘述(参见第 [15.9](15.9.md) 节):
|
||||
|
||||
```go
|
||||
import "time"
|
||||
@@ -113,7 +113,7 @@ BOOM!
|
||||
|
||||
习惯用法:简单超时模式
|
||||
|
||||
要从通道 `ch` 中接收数据,但是最多等待1秒。先创建一个信号通道,然后启动一个 `lambda` 协程,协程在给通道发送数据之前是休眠的:
|
||||
要从通道 `ch` 中接收数据,但是最多等待 1 秒。先创建一个信号通道,然后启动一个 `lambda` 协程,协程在给通道发送数据之前是休眠的:
|
||||
|
||||
```go
|
||||
timeout := make(chan bool, 1)
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# 14.7 新旧模型对比:任务和worker
|
||||
|
||||
假设我们需要处理很多任务;一个worker处理一项任务。任务可以被定义为一个结构体(具体的细节在这里并不重要):
|
||||
假设我们需要处理很多任务;一个 worker 处理一项任务。任务可以被定义为一个结构体(具体的细节在这里并不重要):
|
||||
|
||||
```go
|
||||
type Task struct {
|
||||
@@ -10,7 +10,7 @@ type Task struct {
|
||||
|
||||
旧模式:使用共享内存进行同步
|
||||
|
||||
由各个任务组成的任务池共享内存;为了同步各个worker以及避免资源竞争,我们需要对任务池进行加锁保护:
|
||||
由各个任务组成的任务池共享内存;为了同步各个 worker 以及避免资源竞争,我们需要对任务池进行加锁保护:
|
||||
|
||||
```go
|
||||
type Pool struct {
|
||||
@@ -18,7 +18,7 @@ type Task struct {
|
||||
Tasks []*Task
|
||||
}
|
||||
```
|
||||
sync.Mutex([参见9.3](09.3.md)是互斥锁:它用来在代码中保护临界区资源:同一时间只有一个go协程(goroutine)可以进入该临界区。如果出现了同一时间多个go协程都进入了该临界区,则会产生竞争:Pool结构就不能保证被正确更新。在传统的模式中(经典的面向对象的语言中应用得比较多,比如C++,JAVA,C#),worker代码可能这样写:
|
||||
sync.Mutex([参见9.3](09.3.md))是互斥锁:它用来在代码中保护临界区资源:同一时间只有一个 go 协程(goroutine)可以进入该临界区。如果出现了同一时间多个 go 协程都进入了该临界区,则会产生竞争:Pool 结构就不能保证被正确更新。在传统的模式中(经典的面向对象的语言中应用得比较多,比如 C++,JAVA,C#),worker 代码可能这样写:
|
||||
|
||||
```go
|
||||
func Worker(pool *Pool) {
|
||||
@@ -34,13 +34,13 @@ func Worker(pool *Pool) {
|
||||
}
|
||||
```
|
||||
|
||||
这些worker有许多都可以并发执行;他们可以在go协程中启动。一个worker先将pool锁定,从pool获取第一项任务,再解锁和处理任务。加锁保证了同一时间只有一个go协程可以进入到pool中:一项任务有且只能被赋予一个worker。如果不加锁,则工作协程可能会在`task:=pool.Tasks[0]`发生切换,导致`pool.Tasks=pool.Tasks[1:]`结果异常:一些worker获取不到任务,而一些任务可能被多个worker得到。加锁实现同步的方式在工作协程比较少时可以工作的很好,但是当工作协程数量很大,任务量也很多时,处理效率将会因为频繁的加锁/解锁开销而降低。当工作协程数增加到一个阈值时,程序效率会急剧下降,这就成为了瓶颈。
|
||||
这些 worker 有许多都可以并发执行;他们可以在 go 协程中启动。一个 worker 先将 pool 锁定,从 pool 获取第一项任务,再解锁和处理任务。加锁保证了同一时间只有一个 go 协程可以进入到 pool 中:一项任务有且只能被赋予一个 worker 。如果不加锁,则工作协程可能会在 `task:=pool.Tasks[0]` 发生切换,导致 `pool.Tasks=pool.Tasks[1:]` 结果异常:一些 worker 获取不到任务,而一些任务可能被多个 worker 得到。加锁实现同步的方式在工作协程比较少时可以工作得很好,但是当工作协程数量很大,任务量也很多时,处理效率将会因为频繁的加锁/解锁开销而降低。当工作协程数增加到一个阈值时,程序效率会急剧下降,这就成为了瓶颈。
|
||||
|
||||
新模式:使用通道
|
||||
|
||||
使用通道进行同步:使用一个通道接受需要处理的任务,一个通道接受处理完成的任务(及其结果)。worker在协程中启动,其数量N应该根据任务数量进行调整。
|
||||
使用通道进行同步:使用一个通道接受需要处理的任务,一个通道接受处理完成的任务(及其结果)。worker 在协程中启动,其数量 N 应该根据任务数量进行调整。
|
||||
|
||||
主线程扮演着Master节点角色,可能写成如下形式:
|
||||
主线程扮演着 Master 节点角色,可能写成如下形式:
|
||||
|
||||
```go
|
||||
func main() {
|
||||
@@ -53,7 +53,7 @@ func Worker(pool *Pool) {
|
||||
}
|
||||
```
|
||||
|
||||
worker的逻辑比较简单:从pending通道拿任务,处理后将其放到done通道中:
|
||||
worker 的逻辑比较简单:从 pending 通道拿任务,处理后将其放到done通道中:
|
||||
|
||||
```go
|
||||
func Worker(in, out chan *Task) {
|
||||
@@ -65,11 +65,11 @@ worker的逻辑比较简单:从pending通道拿任务,处理后将其放到d
|
||||
}
|
||||
```
|
||||
|
||||
这里并不使用锁:从通道得到新任务的过程没有任何竞争。随着任务数量增加,worker数量也应该相应增加,同时性能并不会像第一种方式那样下降明显。在pending通道中存在一份任务的拷贝,第一个worker从pending通道中获得第一个任务并进行处理,这里并不存在竞争(对一个通道读数据和写数据的整个过程是原子性的:参见[14.2.2](14.2.md))。某一个任务会在哪一个worker中被执行是不可知的,反过来也是。worker数量的增多也会增加通信的开销,这会对性能有轻微的影响。
|
||||
这里并不使用锁:从通道得到新任务的过程没有任何竞争。随着任务数量增加,worker 数量也应该相应增加,同时性能并不会像第一种方式那样下降明显。在 pending 通道中存在一份任务的拷贝,第一个 worker 从 pending 通道中获得第一个任务并进行处理,这里并不存在竞争(对一个通道读数据和写数据的整个过程是原子性的:参见 [14.2.2](14.2.md))。某一个任务会在哪一个 worker 中被执行是不可知的,反过来也是。worker 数量的增多也会增加通信的开销,这会对性能有轻微的影响。
|
||||
|
||||
从这个简单的例子中可能很难看出第二种模式的优势,但含有复杂锁运用的程序不仅在编写上显得困难,也不容易编写正确,使用第二种模式的话,就无需考虑这么复杂的东西了。
|
||||
|
||||
因此,第二种模式对比第一种模式而言,不仅性能是一个主要优势,而且还有个更大的优势:代码显得更清晰、更优雅。一个更符合go语言习惯的worker写法:
|
||||
因此,第二种模式对比第一种模式而言,不仅性能是一个主要优势,而且还有个更大的优势:代码显得更清晰、更优雅。一个更符合 go 语言习惯的 worker 写法:
|
||||
|
||||
**IDIOM: Use an in- and out-channel instead of locking**
|
||||
|
||||
@@ -83,11 +83,11 @@ worker的逻辑比较简单:从pending通道拿任务,处理后将其放到d
|
||||
}
|
||||
```
|
||||
|
||||
对于任何可以建模为Master-Worker范例的问题,一个类似于worker使用通道进行通信和交互、Master进行整体协调的方案都能完美解决。如果系统部署在多台机器上,各个机器上执行Worker协程,Master和Worker之间使用netchan或者RPC进行通信(参见15章)。
|
||||
对于任何可以建模为 Master-Worker 范例的问题,一个类似于 worker 使用通道进行通信和交互、Master 进行整体协调的方案都能完美解决。如果系统部署在多台机器上,各个机器上执行 Worker 协程,Master 和 Worker 之间使用 netchan 或者 RPC 进行通信(参见 15 章)。
|
||||
|
||||
怎么选择是该使用锁还是通道?
|
||||
|
||||
通道是一个较新的概念,本节我们着重强调了在go协程里通道的使用,但这并不意味着经典的锁方法就不能使用。go语言让你可以根据实际问题进行选择:创建一个优雅、简单、可读性强、在大多数场景性能表现都能很好的方案。如果你的问题适合使用锁,也不要忌讳使用它。go语言注重实用,什么方式最能解决你的问题就用什么方式,而不是强迫你使用一种编码风格。下面列出一个普遍的经验法则:
|
||||
通道是一个较新的概念,本节我们着重强调了在 go 协程里通道的使用,但这并不意味着经典的锁方法就不能使用。go 语言让你可以根据实际问题进行选择:创建一个优雅、简单、可读性强、在大多数场景性能表现都能很好的方案。如果你的问题适合使用锁,也不要忌讳使用它。go语言注重实用,什么方式最能解决你的问题就用什么方式,而不是强迫你使用一种编码风格。下面列出一个普遍的经验法则:
|
||||
|
||||
* 使用锁的情景:
|
||||
- 访问共享数据结构中的缓存信息
|
||||
|
@@ -9,9 +9,9 @@
|
||||
....
|
||||
```
|
||||
|
||||
生成器每次返回的是序列中下一个值而非整个序列;这种特性也称之为惰性求值:只在你需要时进行求值,同时保留相关变量资源(内存和cpu):这是一项在需要时对表达式进行求值的技术。例如,生成一个无限数量的偶数序列:要产生这样一个序列并且在一个一个的使用可能会很困难,而且内存会溢出!但是一个含有通道和go协程的函数能轻易实现这个需求。
|
||||
生成器每次返回的是序列中下一个值而非整个序列;这种特性也称之为惰性求值:只在你需要时进行求值,同时保留相关变量资源(内存和 CPU):这是一项在需要时对表达式进行求值的技术。例如,生成一个无限数量的偶数序列:要产生这样一个序列并且在一个一个的使用可能会很困难,而且内存会溢出!但是一个含有通道和 go 协程的函数能轻易实现这个需求。
|
||||
|
||||
在14.12的例子中,我们实现了一个使用 int 型通道来实现的生成器。通道被命名为`yield`和`resume`,这些词经常在协程代码中使用。
|
||||
在 14.12 的例子中,我们实现了一个使用 int 型通道来实现的生成器。通道被命名为 `yield` 和 `resume` ,这些词经常在协程代码中使用。
|
||||
|
||||
示例 14.12 [lazy_evaluation.go](examples/chapter_14/lazy_evaluation.go):
|
||||
|
||||
@@ -48,9 +48,9 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
有一个细微的区别是从通道读取的值可能会是稍早前产生的,并不是在程序被调用时生成的。如果确实需要这样的行为,就得实现一个请求响应机制。当生成器生成数据的过程是计算密集型且各个结果的顺序并不重要时,那么就可以将生成器放入到go协程实现并行化。但是得小心,使用大量的go协程的开销可能会超过带来的性能增益。
|
||||
有一个细微的区别是从通道读取的值可能会是稍早前产生的,并不是在程序被调用时生成的。如果确实需要这样的行为,就得实现一个请求响应机制。当生成器生成数据的过程是计算密集型且各个结果的顺序并不重要时,那么就可以将生成器放入到 go 协程实现并行化。但是得小心,使用大量的 go 协程的开销可能会超过带来的性能增益。
|
||||
|
||||
这些原则可以概括为:通过巧妙地使用空接口、闭包和高阶函数,我们能实现一个通用的惰性生产器的工厂函数`BuildLazyEvaluator`(这个应该放在一个工具包中实现)。工厂函数需要一个函数和一个初始状态作为输入参数,返回一个无参、返回值是生成序列的函数。传入的函数需要计算出下一个返回值以及下一个状态参数。在工厂函数中,创建一个通道和无限循环的go协程。返回值被放到了该通道中,返回函数稍后被调用时从该通道中取得该返回值。每当取得一个值时,下一个值即被计算。在下面的例子中,定义了一个`evenFunc`函数,其是一个惰性生成函数:在main函数中,我们创建了前10个偶数,每个都是通过调用`even()`函数取得下一个值的。为此,我们需要在`BuildLazyIntEvaluator`函数中具体化我们的生成函数,然后我们能够基于此做出定义。
|
||||
这些原则可以概括为:通过巧妙地使用空接口、闭包和高阶函数,我们能实现一个通用的惰性生产器的工厂函数 `BuildLazyEvaluator`(这个应该放在一个工具包中实现)。工厂函数需要一个函数和一个初始状态作为输入参数,返回一个无参、返回值是生成序列的函数。传入的函数需要计算出下一个返回值以及下一个状态参数。在工厂函数中,创建一个通道和无限循环的 go 协程。返回值被放到了该通道中,返回函数稍后被调用时从该通道中取得该返回值。每当取得一个值时,下一个值即被计算。在下面的例子中,定义了一个 `evenFunc` 函数,其是一个惰性生成函数:在 main 函数中,我们创建了前 10 个偶数,每个都是通过调用 `even()` 函数取得下一个值的。为此,我们需要在 `BuildLazyIntEvaluator` 函数中具体化我们的生成函数,然后我们能够基于此做出定义。
|
||||
|
||||
示例 14.13 [general_lazy_evalution1.go](examples/chapter_14/general_lazy_evalution1.go):
|
||||
|
||||
@@ -118,10 +118,10 @@ func BuildLazyIntEvaluator(evalFunc EvalFunc, initState Any) func() int {
|
||||
```
|
||||
|
||||
练习14.12:[general_lazy_evaluation2.go](exercises/chapter_14/general_lazy_evalution2.go)
|
||||
通过使用14.12中工厂函数生成前10个斐波那契数
|
||||
通过使用 14.12 中工厂函数生成前 10 个斐波那契数
|
||||
|
||||
提示:因为斐波那契数增长很迅速,使用`uint64`类型。
|
||||
注:这种计算通常被定义为递归函数,但是在没有尾递归的语言中,例如go语言,这可能会导致栈溢出,但随着go语言中堆栈可扩展的优化,这个问题就不那么严重。这里的诀窍是使用了惰性求值。gccgo编译器在某些情况下会实现尾递归。
|
||||
提示:因为斐波那契数增长很迅速,使用 `uint64` 类型。
|
||||
注:这种计算通常被定义为递归函数,但是在没有尾递归的语言中,例如 go 语言,这可能会导致栈溢出,但随着 go 语言中堆栈可扩展的优化,这个问题就不那么严重。这里的诀窍是使用了惰性求值。gccgo 编译器在某些情况下会实现尾递归。
|
||||
|
||||
## 链接
|
||||
|
||||
|
@@ -1,10 +1,10 @@
|
||||
# 14.9 实现 Futures 模式
|
||||
|
||||
所谓Futures就是指:有时候在你使用某一个值之前需要先对其进行计算。这种情况下,你就可以在另一个处理器上进行该值的计算,到使用时,该值就已经计算完毕了。
|
||||
所谓 Futures 就是指:有时候在你使用某一个值之前需要先对其进行计算。这种情况下,你就可以在另一个处理器上进行该值的计算,到使用时,该值就已经计算完毕了。
|
||||
|
||||
Futures模式通过闭包和通道可以很容易实现,类似于生成器,不同地方在于Futures需要返回一个值。
|
||||
Futures 模式通过闭包和通道可以很容易实现,类似于生成器,不同地方在于 Futures 需要返回一个值。
|
||||
|
||||
参考条目文献给出了一个很精彩的例子:假设我们有一个矩阵类型,我们需要计算两个矩阵A和B乘积的逆,首先我们通过函数`Inverse(M)`分别对其进行求逆运算,再将结果相乘。如下函数`InverseProduct()`实现了如上过程:
|
||||
参考条目文献给出了一个很精彩的例子:假设我们有一个矩阵类型,我们需要计算两个矩阵 A 和 B 乘积的逆,首先我们通过函数 `Inverse(M)` 分别对其进行求逆运算,再将结果相乘。如下函数 `InverseProduct()` 实现了如上过程:
|
||||
|
||||
```go
|
||||
func InverseProduct(a Matrix, b Matrix) {
|
||||
@@ -14,7 +14,7 @@ func InverseProduct(a Matrix, b Matrix) {
|
||||
}
|
||||
```
|
||||
|
||||
在这个例子中,a和b的求逆矩阵需要先被计算。那么为什么在计算b的逆矩阵时,需要等待a的逆计算完成呢?显然不必要,这两个求逆运算其实可以并行执行的。换句话说,调用`Product`函数只需要等到`a_inv`和`b_inv`的计算完成。如下代码实现了并行计算方式:
|
||||
在这个例子中,a 和 b 的求逆矩阵需要先被计算。那么为什么在计算 b 的逆矩阵时,需要等待 a 的逆计算完成呢?显然不必要,这两个求逆运算其实可以并行执行的。换句话说,调用 `Product` 函数只需要等到 `a_inv` 和 `b_inv` 的计算完成。如下代码实现了并行计算方式:
|
||||
|
||||
```go
|
||||
func InverseProduct(a Matrix, b Matrix) {
|
||||
@@ -26,7 +26,7 @@ func InverseProduct(a Matrix, b Matrix) {
|
||||
}
|
||||
```
|
||||
|
||||
`InverseFuture`函数以`goroutine`的形式起了一个闭包,该闭包会将矩阵求逆结果放入到future通道中:
|
||||
`InverseFuture` 函数以 `goroutine` 的形式起了一个闭包,该闭包会将矩阵求逆结果放入到 future 通道中:
|
||||
|
||||
```go
|
||||
func InverseFuture(a Matrix) chan Matrix {
|
||||
@@ -38,7 +38,7 @@ func InverseFuture(a Matrix) chan Matrix {
|
||||
}
|
||||
```
|
||||
|
||||
当开发一个计算密集型库时,使用Futures模式设计API接口是很有意义的。在你的包使用Futures模式,且能保持友好的API接口。此外,Futures可以通过一个异步的API暴露出来。这样你可以以最小的成本将包中的并行计算移到用户代码中。(参见参考文件18:[http://www.golangpatterns.info/concurrency/futures](http://www.golangpatterns.info/concurrency/futures))
|
||||
当开发一个计算密集型库时,使用 Futures 模式设计 API 接口是很有意义的。在你的包使用 Futures 模式,且能保持友好的 API 接口。此外,Futures 可以通过一个异步的 API 暴露出来。这样你可以以最小的成本将包中的并行计算移到用户代码中。(参见参考文件 18:[http://www.golangpatterns.info/concurrency/futures](http://www.golangpatterns.info/concurrency/futures))
|
||||
|
||||
## 链接
|
||||
|
||||
|
@@ -1,9 +1,9 @@
|
||||
# 15.0 网络、模板与网页应用
|
||||
|
||||
Go 在编写 web 应用方面非常得力。因为目前它还没有GUI(Graphic User Interface 即图形化用户界面)的框架,通过文本或者模板展现的 html 页面是目前 Go 编写界面应用程序的唯一方式。(**译者注:实际上在翻译的时候,已经有了一些不太成熟的GUI库例如:go ui。**)
|
||||
Go 在编写 web 应用方面非常得力。因为目前它还没有GUI(Graphic User Interface 即图形化用户界面)的框架,通过文本或者模板展现的 html 页面是目前 Go 编写界面应用程序的唯一方式。(**译者注:实际上在翻译的时候,已经有了一些不太成熟的 GUI 库,例如:go ui 。**)
|
||||
|
||||
## 链接
|
||||
|
||||
- [目录](directory.md)
|
||||
- 上一节:[使用通道并发访问对象](14.17.md)
|
||||
- 下一节:[tcp服务器](15.1.md)
|
||||
- 下一节:[tcp 服务器](15.1.md)
|
||||
|
@@ -46,7 +46,7 @@ func doServerStuff(conn net.Conn) {
|
||||
}
|
||||
```
|
||||
|
||||
在 `main()` 中创建了一个 `net.Listener` 类型的变量 `listener`,他实现了服务器的基本功能:用来监听和接收来自客户端的请求(在 localhost 即 IP 地址为 127.0.0.1 端口为 50000 基于TCP协议)。`Listen()` 函数可以返回一个 `error` 类型的错误变量。用一个无限 for 循环的 `listener.Accept()` 来等待客户端的请求。客户端的请求将产生一个 `net.Conn` 类型的连接变量。然后一个独立的协程使用这个连接执行 `doServerStuff()`,开始使用一个 512 字节的缓冲 `data` 来读取客户端发送来的数据,并且把它们打印到服务器的终端,`len` 获取客户端发送的数据字节数;当客户端发送的所有数据都被读取完成时,协程就结束了。这段程序会为每一个客户端连接创建一个独立的协程。必须先运行服务器代码,再运行客户端代码。
|
||||
在 `main()` 中创建了一个 `net.Listener` 类型的变量 `listener`,他实现了服务器的基本功能:用来监听和接收来自客户端的请求(在 localhost 即 IP 地址为 127.0.0.1 端口为 50000 基于 TCP 协议)。`Listen()` 函数可以返回一个 `error` 类型的错误变量。用一个无限 for 循环的 `listener.Accept()` 来等待客户端的请求。客户端的请求将产生一个 `net.Conn` 类型的连接变量。然后一个独立的协程使用这个连接执行 `doServerStuff()`,开始使用一个 512 字节的缓冲 `data` 来读取客户端发送来的数据,并且把它们打印到服务器的终端,`len` 获取客户端发送的数据字节数;当客户端发送的所有数据都被读取完成时,协程就结束了。这段程序会为每一个客户端连接创建一个独立的协程。必须先运行服务器代码,再运行客户端代码。
|
||||
|
||||
客户端代码写在另一个文件 client.go 中:
|
||||
|
||||
@@ -99,7 +99,7 @@ func main() {
|
||||
|
||||
如果在服务器没有开始监听的情况下运行客户端程序,客户端会停止并打印出以下错误信息:`对tcp 127.0.0.1:50000发起连接时产生错误:由于目标计算机的积极拒绝而无法创建连接`。
|
||||
|
||||
打开命令提示符并转到服务器和客户端可执行程序所在的目录,Windows 系统下输入server.exe(或者只输入server),Linux系统下输入./server。
|
||||
打开命令提示符并转到服务器和客户端可执行程序所在的目录,Windows 系统下输入 `server.exe`(或者只输入 `server` ),Linux 系统下输入 `./server` 。
|
||||
|
||||
接下来控制台出现以下信息:`Starting the server ...`
|
||||
|
||||
@@ -175,9 +175,9 @@ func main() {
|
||||
read = true
|
||||
count = 0
|
||||
)
|
||||
// 创建一个socket
|
||||
// 创建一个 socket
|
||||
con, err := net.Dial("tcp", remote)
|
||||
// 发送我们的消息,一个http GET请求
|
||||
// 发送我们的消息,一个 http GET 请求
|
||||
io.WriteString(con, msg)
|
||||
// 读取服务器的响应
|
||||
for read {
|
||||
@@ -193,7 +193,7 @@ func main() {
|
||||
|
||||
编写新版本的客户端和服务器([client1.go](exercises/chapter_15/client1.go) / [server1.go](exercises/chapter_15/server1.go)):
|
||||
|
||||
* 增加一个检查错误的函数 `checkError(error)`;讨论如下方案的利弊:为什么这个重构可能并没有那么理想?看看在 [示例15.14](examples/chapter_15/template_validation.go) 中它是如何被解决的
|
||||
* 增加一个检查错误的函数 `checkError(error)`;讨论如下方案的利弊:为什么这个重构可能并没有那么理想?看看在 [示例 15.14](examples/chapter_15/template_validation.go) 中它是如何被解决的
|
||||
* 使客户端可以通过发送一条命令 SH 来关闭服务器
|
||||
* 让服务器可以保存已经连接的客户端列表(他们的名字);当客户端发送 WHO 指令的时候,服务器将显示如下列表:
|
||||
```
|
||||
@@ -204,7 +204,7 @@ User CHRIS is 1
|
||||
```
|
||||
注意:当服务器运行的时候,你无法编译/连接同一个目录下的源码来产生一个新的版本,因为 `server.exe` 正在被操作系统使用而无法被替换成新的版本。
|
||||
|
||||
下边这个版本的 simple_tcp_server.go 从很多方面优化了第一个tcp服务器的示例 server.go 并且拥有更好的结构,它只用了 80 行代码!
|
||||
下边这个版本的 simple_tcp_server.go 从很多方面优化了第一个 tcp 服务器的示例 server.go 并且拥有更好的结构,它只用了 80 行代码!
|
||||
|
||||
示例 15.5 [simple_tcp_server.go](examples/chapter_15/simple_tcp_server.go):
|
||||
|
||||
@@ -292,11 +292,11 @@ func checkError(error error, info string) {
|
||||
}
|
||||
}
|
||||
```
|
||||
(**译者注:应该是由于go版本的更新,会提示os.EAGAIN undefined ,修改后的代码:[simple_tcp_server_v1.go](examples/chapter_15/simple_tcp_server_v1.go)**)
|
||||
(**译者注:应该是由于 go 版本的更新,会提示 os.EAGAIN undefined ,修改后的代码:[simple_tcp_server_v1.go](examples/chapter_15/simple_tcp_server_v1.go)**)
|
||||
|
||||
都有哪些改进?
|
||||
|
||||
* 服务器地址和端口不再是硬编码,而是通过命令行参数传入,并通过 `flag` 包来读取这些参数。这里使用了 `flag.NArg()` 检查是否按照期望传入了2个参数:
|
||||
* 服务器地址和端口不再是硬编码,而是通过命令行参数传入,并通过 `flag` 包来读取这些参数。这里使用了 `flag.NArg()` 检查是否按照期望传入了 2 个参数:
|
||||
|
||||
```go
|
||||
if flag.NArg() != 2 {
|
||||
|
@@ -75,7 +75,7 @@ func checkError(err error) {
|
||||
|
||||
2011/09/30 11:24:15 Get: Get http://www.google.bex: dial tcp www.google.bex:80:GetHostByName: No such host is known.
|
||||
|
||||
***译者注*** 和上一个例子相似,你可以把google.com更换为一个国内可以顺畅访问的网址进行测试
|
||||
***译者注*** 和上一个例子相似,你可以把 google.com 更换为一个国内可以顺畅访问的网址进行测试
|
||||
|
||||
在下边的程序中,我们获取一个 twitter 用户的状态,通过 `xml` 包将这个状态解析成为一个结构:
|
||||
|
||||
@@ -91,7 +91,7 @@ import (
|
||||
)
|
||||
|
||||
/*这个结构会保存解析后的返回数据。
|
||||
他们会形成有层级的XML,可以忽略一些无用的数据*/
|
||||
他们会形成有层级的 XML,可以忽略一些无用的数据*/
|
||||
type Status struct {
|
||||
Text string
|
||||
}
|
||||
@@ -102,11 +102,11 @@ type User struct {
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 发起请求查询推特Goodland用户的状态
|
||||
// 发起请求查询推特 Goodland 用户的状态
|
||||
response, _ := http.Get("http://twitter.com/users/Googland.xml")
|
||||
// 初始化XML返回值的结构
|
||||
// 初始化 XML 返回值的结构
|
||||
user := User{xml.Name{"", "user"}, Status{""}}
|
||||
// 将XML解析为我们的结构
|
||||
// 将 XML 解析为我们的结构
|
||||
xml.Unmarshal(response.Body, &user)
|
||||
fmt.Printf("status: %s", user.Status.Text)
|
||||
}
|
||||
@@ -116,16 +116,16 @@ func main() {
|
||||
|
||||
status: Robot cars invade California, on orders from Google: Google has been testing self-driving cars ... http://bit.ly/cbtpUN http://retwt.me/97p<exit code="0" msg="process exited normally"/>
|
||||
|
||||
**译者注** 和上边的示例相似,你可能无法获取到xml数据,另外由于go版本的更新,`xml.Unmarshal` 函数的第一个参数需是[]byte类型,而无法传入 `Body`。
|
||||
**译者注** 和上边的示例相似,你可能无法获取到 xml 数据,另外由于 go 版本的更新,`xml.Unmarshal` 函数的第一个参数必需是 []byte 类型,而无法传入 `Body`。
|
||||
|
||||
我们会在 [15.4节](15.4.md) 中用到 `http` 包中的其他重要的函数:
|
||||
我们会在 [15.4 节](15.4.md) 中用到 `http` 包中的其他重要的函数:
|
||||
|
||||
* `http.Redirect(w ResponseWriter, r *Request, url string, code int)`:这个函数会让浏览器重定向到 `url`(可以是基于请求 url 的相对路径),同时指定状态码。
|
||||
* `http.NotFound(w ResponseWriter, r *Request)`:这个函数将返回网页没有找到,HTTP 404错误。
|
||||
* `http.NotFound(w ResponseWriter, r *Request)`:这个函数将返回网页没有找到,HTTP 404 错误。
|
||||
* `http.Error(w ResponseWriter, error string, code int)`:这个函数返回特定的错误信息和 HTTP 代码。
|
||||
* 另一个 `http.Request` 对象 `req` 的重要属性:`req.Method`,这是一个包含 `GET` 或 `POST` 字符串,用来描述网页是以何种方式被请求的。
|
||||
|
||||
go为所有的HTTP状态码定义了常量,比如:
|
||||
go 为所有的 HTTP 状态码定义了常量,比如:
|
||||
```go
|
||||
http.StatusContinue = 100
|
||||
http.StatusOK = 200
|
||||
@@ -141,7 +141,7 @@ http.StatusInternalServerError = 500
|
||||
|
||||
比如在网页应用发送 html 字符串的时候,在输出之前执行 `w.Header().Set("Content-Type", "text/html")`(通常不是必要的)。
|
||||
|
||||
练习 15.4:扩展 http_fetch.go 使之可以从控制台读取url,使用 [12.1节](12.1.md) 学到的接收控制台输入的方法([http_fetch2.go](examples/chapter_15/http_fetch2.go))
|
||||
练习 15.4:扩展 http_fetch.go 使之可以从控制台读取 url,使用 [12.1节](12.1.md) 学到的接收控制台输入的方法([http_fetch2.go](examples/chapter_15/http_fetch2.go))
|
||||
|
||||
练习 15.5:获取 json 格式的推特状态,就像示例 15.9([twitter_status_json.go](exercises/chapter_15/twitter_status_json.go))
|
||||
|
||||
|
@@ -137,7 +137,7 @@ func load(title string) (*Page, error) {
|
||||
```go
|
||||
templates = make(map[string]*template.Template)
|
||||
```
|
||||
此种技术被称为*模板缓存*,是推荐的最佳实践。
|
||||
这种技术被称为*模板缓存*,是推荐的最佳实践。
|
||||
|
||||
3. 为了真正从模板和结构体构建出页面,必须使用:
|
||||
```go
|
||||
|
@@ -158,7 +158,8 @@ Logger | http://localhost:12345/ (根) | oops
|
||||
|
||||
`Logger` 处理函数用 `w.WriteHeader(404)` 来输出 “404 Not Found”头部。
|
||||
|
||||
此项技术通常很有用,无论何时服务器执行代码产生错误,都可以应用类似这样的代码:
|
||||
这项技术通常很有用,无论何时服务器执行代码产生错误,都可以应用类似这样的代码:
|
||||
|
||||
```go
|
||||
if err != nil {
|
||||
w.WriteHeader(400)
|
||||
@@ -209,6 +210,7 @@ Channel | http://localhost:12345/chan | channel send #1
|
||||
Channel | 刷新 | channel send #2
|
||||
|
||||
每当有新请求到达,通道的 `ServeHTTP` 方法从通道获取下一个整数并显示。由此可见,网页服务器可以从通道中获取要发送的响应,它可以由另一个函数产生(甚至是客户端)。下面的代码片段正是一个这样的处理函数,但会在 30 秒后超时:
|
||||
|
||||
```go
|
||||
func ChanResponse(w http.ResponseWriter, req *http.Request) {
|
||||
timeout := make (chan bool)
|
||||
|
@@ -24,7 +24,7 @@ func (t *Args) Multiply(args *Args, reply *int) net.Error {
|
||||
}
|
||||
```
|
||||
|
||||
(**译注:Go 当前版本要求此方法返回类型为 `error`,以上示例中返回 `net.Error` 已无法通过编译,见更新后的[rpc_objects.go](examples/chapter_15/rpc_updated/rpc_objects/rpc_objects.go)。**)
|
||||
(**译注:Go 当前版本要求此方法返回类型为 `error`,以上示例中返回 `net.Error` 已无法通过编译,见更新后的 [rpc_objects.go](examples/chapter_15/rpc_updated/rpc_objects/rpc_objects.go)。**)
|
||||
|
||||
服务器端产生一个 `rpc_objects.Args` 类型的对象 `calc`,并用 `rpc.Register(object)` 注册。调用 `HandleHTTP()`,然后用 `net.Listen` 在指定的地址上启动监听。也可以按名称来注册对象,例如:`rpc.RegisterName("Calculator", calc)`。
|
||||
|
||||
|
@@ -1,27 +1,27 @@
|
||||
# 16.0 常见的陷阱与错误
|
||||
|
||||
在之前的内容中,有时候使用`!!...!!`标记警告go语言中的一些错误使用方式。当你在编程时候遇到的一个困难,可以确定本书特定的章节能找到类似的主题。为了方便起见,这里列出了一些常见陷阱,以便于你能发现更多的解释和例子:
|
||||
在之前的内容中,有时候使用 `!!...!!` 标记警告 go 语言中的一些错误使用方式。当你在编程时候遇到的一个困难,可以确定本书特定的章节能找到类似的主题。为了方便起见,这里列出了一些常见陷阱,以便于你能发现更多的解释和例子:
|
||||
|
||||
- 永远不要使用形如 `var p*a` 声明变量,这会混淆指针声明和乘法运算(参考[4.9小节](04.9.md))
|
||||
- 永远不要在`for`循环自身中改变计数器变量(参考[5.4小节](05.4.md))
|
||||
- 永远不要在`for-range`循环中使用一个值去改变自身的值(参考[5.4.4小节](05.4.md))
|
||||
- 永远不要将`goto`和前置标签一起使用(参考[5.6小节](05.6.md))
|
||||
- 永远不要在 `for` 循环自身中改变计数器变量(参考[5.4小节](05.4.md))
|
||||
- 永远不要在 `for-range` 循环中使用一个值去改变自身的值(参考[5.4.4小节](05.4.md))
|
||||
- 永远不要将 `goto` 和前置标签一起使用(参考[5.6小节](05.6.md))
|
||||
- 永远不要忘记在函数名(参考[第6章](06.0.md))后加括号(),尤其调用一个对象的方法或者使用匿名函数启动一个协程时
|
||||
- 永远不要使用`new()`一个map,一直使用make(参考[第8章](08.0.md))
|
||||
- 当为一个类型定义一个String()方法时,不要使用`fmt.Print`或者类似的代码(参考[10.7小节](10.7.md))
|
||||
- 永远不要忘记当终止缓存写入时,使用`Flush`函数(参考[12.2.3小节](12.2.md))
|
||||
- 永远不要使用 `new()` 一个 map,一直使用 make(参考[第8章](08.0.md))
|
||||
- 当为一个类型定义一个 String() 方法时,不要使用 `fmt.Print` 或者类似的代码(参考[10.7小节](10.7.md))
|
||||
- 永远不要忘记当终止缓存写入时,使用 `Flush` 函数(参考[12.2.3小节](12.2.md))
|
||||
- 永远不要忽略错误提示,忽略错误会导致程序崩溃(参考[13.1小节](13.1.md))
|
||||
- 不要使用全局变量或者共享内存,这会使并发执行的代码变得不安全(参考[14.1小节](14.1.md))
|
||||
- `println`函数仅仅是用于调试的目的
|
||||
- `println` 函数仅仅是用于调试的目的
|
||||
|
||||
最佳实践:对比以下使用方式:
|
||||
|
||||
- 使用正确的方式初始化一个元素是切片的映射,例如`map[type]slice`(参考[8.1.3小节](08.1.md))
|
||||
- 一直使用逗号,ok或者checked形式作为类型断言(参考[11.3小节](11.3.md))
|
||||
- 使用正确的方式初始化一个元素是切片的映射,例如 `map[type]slice`(参考[8.1.3小节](08.1.md))
|
||||
- 一直使用逗号,ok 或者 checked 形式作为类型断言(参考[11.3小节](11.3.md))
|
||||
- 使用一个工厂函数创建并初始化自己定义类型(参考[10.2小节](10.2.md)-[18.4小节](18.4.md))
|
||||
- 仅当一个结构体的方法想改变结构体时,使用结构体指针作为方法的接受者,否则使用一个结构体值类型[10.6.3小节](10.6.md)
|
||||
|
||||
本章主要汇总了go语言使用过程中最常见的错误和注意事项。在之前的章节已经涉及到了完整的示例和解释,你应该做的不仅仅是阅读这段的标题。
|
||||
本章主要汇总了 go 语言使用过程中最常见的错误和注意事项。在之前的章节已经涉及到了完整的示例和解释,你应该做的不仅仅是阅读这段的标题。
|
||||
|
||||
## 链接
|
||||
|
||||
|
@@ -8,7 +8,7 @@ if something {
|
||||
// 使用remember
|
||||
```
|
||||
|
||||
在此代码段中,`remember`变量永远不会在`if`语句外面变成`true`,如果`something`为`true`,由于使用了短声明`:=`,`if`语句内部的新变量`remember`将覆盖外面的`remember`变量,并且该变量的值为`true`,但是在`if`语句外面,变量`remember`的值变成了`false`,所以正确的写法应该是:
|
||||
在此代码段中,`remember` 变量永远不会在 `if` 语句外面变成 `true`,如果 `something` 为 `true`,由于使用了短声明 `:=`,`if` 语句内部的新变量 `remember` 将覆盖外面的 `remember` 变量,并且该变量的值为 `true`,但是在 `if` 语句外面,变量 `remember` 的值变成了 `false`,所以正确的写法应该是:
|
||||
|
||||
```go
|
||||
if something {
|
||||
@@ -16,17 +16,17 @@ if something {
|
||||
}
|
||||
```
|
||||
|
||||
此类错误也容易在`for`循环中出现,尤其当函数返回一个具名变量时难于察觉
|
||||
此类错误也容易在 `for` 循环中出现,尤其当函数返回一个具名变量时难于察觉
|
||||
,例如以下的代码段:
|
||||
|
||||
```go
|
||||
func shadow() (err error) {
|
||||
x, err := check1() // x是新创建变量,err是被赋值
|
||||
x, err := check1() // x 是新创建变量,err 是被赋值
|
||||
if err != nil {
|
||||
return // 正确返回err
|
||||
return // 正确返回 err
|
||||
}
|
||||
if y, err := check2(x); err != nil { // y和if语句中err被创建
|
||||
return // if语句中的err覆盖外面的err,所以错误的返回nil!
|
||||
if y, err := check2(x); err != nil { // y 和 if 语句中 err 被创建
|
||||
return // if 语句中的 err 覆盖外面的 err,所以错误的返回 nil !
|
||||
} else {
|
||||
fmt.Println(y)
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# 16.10 糟糕的错误处理
|
||||
|
||||
译者注:该小结关于错误处理的观点,译者并不完全赞同,关于本小结的部分想法请参考[关于16.10.2小节错误处理的一些见解](Discussion_about_16.10.md)
|
||||
译者注:该小结关于错误处理的观点,译者并不完全赞同,关于本小结的部分想法请参考[关于 16.10.2 小节错误处理的一些见解](Discussion_about_16.10.md)
|
||||
|
||||
|
||||
依附于[第13章](13.0.md)模式的描述和[第17.1小节](17.1.md)与[第17.2.4小节](17.2.md)的总结。
|
||||
@@ -41,7 +41,7 @@ if err2 != nil {
|
||||
}
|
||||
```
|
||||
|
||||
首先,包括在一个初始化的`if`语句中对函数的调用。但即使代码中到处都是以`if`语句的形式通知错误(通过打印错误信息)。通过这种方式,很难分辨什么是正常的程序逻辑,什么是错误检测或错误通知。还需注意的是,大部分代码都是致力于错误的检测。通常解决此问题的好办法是尽可能以闭包的形式封装你的错误检测,例如下面的代码:
|
||||
首先,包括在一个初始化的 `if` 语句中对函数的调用。但即使代码中到处都是以 `if` 语句的形式通知错误(通过打印错误信息)。通过这种方式,很难分辨什么是正常的程序逻辑,什么是错误检测或错误通知。还需注意的是,大部分代码都是致力于错误的检测。通常解决此问题的好办法是尽可能以闭包的形式封装你的错误检测,例如下面的代码:
|
||||
|
||||
```go
|
||||
func httpRequestHandler(w http.ResponseWriter, req *http.Request) {
|
||||
@@ -65,11 +65,11 @@ func httpRequestHandler(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
这种方法可以很容易分辨出错误检测、错误通知和正常的程序逻辑(更详细的方式参考[第13.5小节](13.5.md))。
|
||||
|
||||
**在开始阅读[第17章](17.0.md)前,先回答下列2个问题:**
|
||||
**在开始阅读[第17章](17.0.md)前,先回答下列 2 个问题:**
|
||||
|
||||
- 问题 16.1:总结你能记住的所有关于`,ok`模式的情况。
|
||||
- 问题 16.1:总结你能记住的所有关于 `,ok` 模式的情况。
|
||||
|
||||
- 问题 16.2:总结你能记住的所有关于`defer`模式的情况。
|
||||
- 问题 16.2:总结你能记住的所有关于 `defer` 模式的情况。
|
||||
|
||||
|
||||
## 链接
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# 16.2 误用字符串
|
||||
|
||||
当需要对一个字符串进行频繁的操作时,谨记在go语言中字符串是不可变的(类似java和c#)。使用诸如`a += b`形式连接字符串效率低下,尤其在一个循环内部使用这种形式。这会导致大量的内存开销和拷贝。**应该使用一个字符数组代替字符串,将字符串内容写入一个缓存中。** 例如以下的代码示例:
|
||||
当需要对一个字符串进行频繁的操作时,谨记在 go 语言中字符串是不可变的(类似 java 和 c#)。使用诸如 `a += b` 形式连接字符串效率低下,尤其在一个循环内部使用这种形式。这会导致大量的内存开销和拷贝。**应该使用一个字符数组代替字符串,将字符串内容写入一个缓存中。** 例如以下的代码示例:
|
||||
|
||||
```go
|
||||
var b bytes.Buffer
|
||||
@@ -11,10 +11,10 @@ for condition {
|
||||
return b.String()
|
||||
```
|
||||
|
||||
注意:由于编译优化和依赖于使用缓存操作的字符串大小,当循环次数大于15时,效率才会更佳。
|
||||
注意:由于编译优化和依赖于使用缓存操作的字符串大小,当循环次数大于 15 时,效率才会更佳。
|
||||
|
||||
## 链接
|
||||
|
||||
- [目录](directory.md)
|
||||
- 上一节:[误用短声明导致变量覆盖](16.1.md)
|
||||
- 下一节:[发生错误时使用defer关闭一个文件](16.3.md)
|
||||
- 下一节:[发生错误时使用 defer 关闭一个文件](16.3.md)
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# 16.3 发生错误时使用 defer 关闭一个文件
|
||||
|
||||
如果你在一个for循环内部处理一系列文件,你需要使用defer确保文件在处理完毕后被关闭,例如:
|
||||
如果你在一个 for 循环内部处理一系列文件,你需要使用 defer 确保文件在处理完毕后被关闭,例如:
|
||||
|
||||
```go
|
||||
for _, file := range files {
|
||||
@@ -14,7 +14,7 @@ for _, file := range files {
|
||||
}
|
||||
```
|
||||
|
||||
但是在循环结尾处的defer没有执行,所以文件一直没有关闭!垃圾回收机制可能会自动关闭文件,但是这会产生一个错误,更好的做法是:
|
||||
但是在循环结尾处的 defer 没有执行,所以文件一直没有关闭!垃圾回收机制可能会自动关闭文件,但是这会产生一个错误,更好的做法是:
|
||||
|
||||
```go
|
||||
for _, file := range files {
|
||||
@@ -28,10 +28,10 @@ for _, file := range files {
|
||||
}
|
||||
```
|
||||
|
||||
**defer仅在函数返回时才会执行,在循环的结尾或其他一些有限范围的代码内不会执行。**
|
||||
**defer 仅在函数返回时才会执行,在循环的结尾或其他一些有限范围的代码内不会执行。**
|
||||
|
||||
## 链接
|
||||
|
||||
- [目录](directory.md)
|
||||
- 上一节:[误用字符串](16.2.md)
|
||||
- 下一节:[何时使用new()和make()](16.4.md)
|
||||
- 下一节:[何时使用 new() 和 make()](16.4.md)
|
||||
|
@@ -1,12 +1,12 @@
|
||||
# 16.4 何时使用 new() 和 make()
|
||||
|
||||
在第[7.2.1小节](07.2.md)和第[10.2.2](10.2.md)小节,我们已经讨论过此问题,并使用代码进行详细说明,观点如下:
|
||||
在第[7.2.1小节](07.2.md)和第[10.2.2小节](10.2.md),我们已经讨论过此问题,并使用代码进行详细说明,观点如下:
|
||||
|
||||
- 切片、映射和通道,使用make
|
||||
- 数组、结构体和所有的值类型,使用new
|
||||
- 切片、映射和通道,使用 make
|
||||
- 数组、结构体和所有的值类型,使用 new
|
||||
|
||||
## 链接
|
||||
|
||||
- [目录](directory.md)
|
||||
- 上一节:[发生错误时使用defer关闭一个文件](16.3.md)
|
||||
- 上一节:[发生错误时使用 defer 关闭一个文件](16.3.md)
|
||||
- 下一节:[不需要将一个指向切片的指针传递给函数](16.5.md)
|
||||
|
@@ -1,19 +1,23 @@
|
||||
# 16.5 不需要将一个指向切片的指针传递给函数
|
||||
|
||||
在[第4.9小节](04.9.md),我们已经知道,切片实际是一个指向潜在数组的指针。我们常常需要把切片作为一个参数传递给函数是因为:实际就是传递一个指向变量的指针,在函数内可以改变这个变量,而不是传递数据的拷贝。
|
||||
在第[4.9小节](04.9.md),我们已经知道,切片实际是一个指向潜在数组的指针。我们常常需要把切片作为一个参数传递给函数是因为:实际就是传递一个指向变量的指针,在函数内可以改变这个变量,而不是传递数据的拷贝。
|
||||
|
||||
因此应该这样做:
|
||||
|
||||
```go
|
||||
func findBiggest( listOfNumbers []int ) int {}
|
||||
```
|
||||
|
||||
而不是:
|
||||
|
||||
```go
|
||||
func findBiggest( listOfNumbers *[]int ) int {}
|
||||
```
|
||||
|
||||
**当切片作为参数传递时,切记不要解引用切片。**
|
||||
|
||||
## 链接
|
||||
|
||||
- [目录](directory.md)
|
||||
- 上一节:[何时使用new()和make()](16.4.md)
|
||||
- 上一节:[何时使用 new() 和 make()](16.4.md)
|
||||
- 下一节:[使用指针指向接口类型](16.6.md)
|
||||
|
@@ -1,9 +1,9 @@
|
||||
# 16.6 使用指针指向接口类型
|
||||
|
||||
查看如下程序:`nexter`是一个接口类型,并且定义了一个`next()`方法读取下一字节。函数`nextFew1`将`nexter`接口作为参数并读取接下来的`num`个字节,并返回一个切片:这是正确做法。但是`nextFew2`使用一个指向`nexter`接口类型的指针作为参数传递给函数:当使用`next()`函数时,系统会给出一个编译错误:**n.next undefined (type *nexter has no
|
||||
field or method next)** (译者注:n.next未定义(*nexter类型没有next成员或next方法))
|
||||
查看如下程序:`nexter` 是一个接口类型,并且定义了一个 `next()` 方法读取下一字节。函数 `nextFew1` 将 `nexter` 接口作为参数并读取接下来的 `num` 个字节,并返回一个切片:这是正确做法。但是 `nextFew2` 使用一个指向 `nexter` 接口类型的指针作为参数传递给函数:当使用 `next()` 函数时,系统会给出一个编译错误:**n.next undefined (type *nexter has no
|
||||
field or method next)** (译者注:n.next 未定义(*nexter 类型没有 next 成员或 next 方法))
|
||||
|
||||
例 16.1 pointer_interface.go (不能通过编译):
|
||||
例 16.1 [pointer_interface.go](examples/chapter_16/pointer_interface.go) (不能通过编译):
|
||||
|
||||
```go
|
||||
package main
|
||||
@@ -23,7 +23,7 @@ func nextFew1(n nexter, num int) []byte {
|
||||
func nextFew2(n *nexter, num int) []byte {
|
||||
var b []byte
|
||||
for i:=0; i < num; i++ {
|
||||
b[i] = n.next() // 编译错误:n.next未定义(*nexter类型没有next成员或next方法)
|
||||
b[i] = n.next() // 编译错误:n.next 未定义(*nexter 类型没有 next 成员或 next 方法)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# 16.7 使用值类型时误用指针
|
||||
|
||||
将一个值类型作为一个参数传递给函数或者作为一个方法的接收者,似乎是对内存的滥用,因为值类型一直是传递拷贝。但是另一方面,值类型的内存是在栈上分配,内存分配快速且开销不大。如果你传递一个指针,而不是一个值类型,go编译器大多数情况下会认为需要创建一个对象,并将对象移动到堆上,所以会导致额外的内存分配:因此当使用指针代替值类型作为参数传递时,我们没有任何收获。
|
||||
将一个值类型作为一个参数传递给函数或者作为一个方法的接收者,似乎是对内存的滥用,因为值类型一直是传递拷贝。但是另一方面,值类型的内存是在栈上分配,内存分配快速且开销不大。如果你传递一个指针,而不是一个值类型,go 编译器大多数情况下会认为需要创建一个对象,并将对象移动到堆上,所以会导致额外的内存分配:因此当使用指针代替值类型作为参数传递时,我们没有任何收获。
|
||||
|
||||
## 链接
|
||||
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
由于教学需要和对协程的工作原理有一个直观的了解,在[第14章](14.0.md)使用了一些简单的算法,举例说明了协程和通道的使用,例如生产者或者迭代器。在实际应用中,你不需要并发执行,或者你不需要关注协程和通道的开销,在大多数情况下,通过栈传递参数会更有效率。
|
||||
|
||||
但是,如果你使用`break`、`return`或者`panic`去跳出一个循环,很有可能会导致内存溢出,因为协程正处理某些事情而被阻塞。在实际代码中,通常仅需写一个简单的过程式循环即可。**当且仅当代码中并发执行非常重要,才使用协程和通道。**
|
||||
但是,如果你使用 `break`、`return` 或者 `panic` 去跳出一个循环,很有可能会导致内存溢出,因为协程正处理某些事情而被阻塞。在实际代码中,通常仅需写一个简单的过程式循环即可。**当且仅当代码中并发执行非常重要,才使用协程和通道。**
|
||||
|
||||
## 链接
|
||||
|
||||
|
@@ -13,14 +13,14 @@ import (
|
||||
var values = [5]int{10, 11, 12, 13, 14}
|
||||
|
||||
func main() {
|
||||
// 版本A:
|
||||
for ix := range values { // ix是索引值
|
||||
// 版本 A:
|
||||
for ix := range values { // ix 是索引值
|
||||
func() {
|
||||
fmt.Print(ix, " ")
|
||||
}() // 调用闭包打印每个索引值
|
||||
}
|
||||
fmt.Println()
|
||||
// 版本B: 和A版本类似,但是通过调用闭包作为一个协程
|
||||
// 版本 B:和 A 版本类似,但是通过调用闭包作为一个协程
|
||||
for ix := range values {
|
||||
go func() {
|
||||
fmt.Print(ix, " ")
|
||||
@@ -28,7 +28,7 @@ func main() {
|
||||
}
|
||||
fmt.Println()
|
||||
time.Sleep(5e9)
|
||||
// 版本C: 正确的处理方式
|
||||
// 版本 C:正确的处理方式
|
||||
for ix := range values {
|
||||
go func(ix interface{}) {
|
||||
fmt.Print(ix, " ")
|
||||
@@ -36,7 +36,7 @@ func main() {
|
||||
}
|
||||
fmt.Println()
|
||||
time.Sleep(5e9)
|
||||
// 版本D: 输出值:
|
||||
// 版本 D:输出值:
|
||||
for ix := range values {
|
||||
val := values[ix]
|
||||
go func() {
|
||||
@@ -45,7 +45,6 @@ func main() {
|
||||
}
|
||||
time.Sleep(1e9)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
输出:
|
||||
@@ -60,13 +59,13 @@ func main() {
|
||||
10 11 12 13 14
|
||||
```
|
||||
|
||||
版本A调用闭包5次打印每个索引值,版本B也做相同的事,但是通过协程调用每个闭包。按理说这将执行得更快,因为闭包是并发执行的。如果我们阻塞足够多的时间,让所有协程执行完毕,版本B的输出是:`4 4 4 4 4`。为什么会这样?在版本B的循环中,`ix`变量实际是一个单变量,表示每个数组元素的索引值。因为这些闭包都只绑定到一个变量,这是一个比较好的方式,当你运行这段代码时,你将看见每次循环都打印最后一个索引值`4`,而不是每个元素的索引值。因为协程可能在循环结束后还没有开始执行,而此时`ix`值是`4`。
|
||||
版本 A 调用闭包 5 次打印每个索引值,版本 B 也做相同的事,但是通过协程调用每个闭包。按理说这将执行得更快,因为闭包是并发执行的。如果我们阻塞足够多的时间,让所有协程执行完毕,版本 B 的输出是:`4 4 4 4 4`。为什么会这样?在版本 B 的循环中,`ix` 变量实际是一个单变量,表示每个数组元素的索引值。因为这些闭包都只绑定到一个变量,这是一个比较好的方式,当你运行这段代码时,你将看见每次循环都打印最后一个索引值 `4`,而不是每个元素的索引值。因为协程可能在循环结束后还没有开始执行,而此时 `ix` 值是 `4`。
|
||||
|
||||
版本C的循环写法才是正确的:调用每个闭包时将`ix`作为参数传递给闭包。`ix`在每次循环时都被重新赋值,并将每个协程的`ix`放置在栈中,所以当协程最终被执行时,每个索引值对协程都是可用的。注意这里的输出可能是`0 2 1 3 4`或者`0 3 1 2 4`或者其他类似的序列,这主要取决于每个协程何时开始被执行。
|
||||
版本 C 的循环写法才是正确的:调用每个闭包时将 `ix` 作为参数传递给闭包。`ix` 在每次循环时都被重新赋值,并将每个协程的 `ix` 放置在栈中,所以当协程最终被执行时,每个索引值对协程都是可用的。注意这里的输出可能是 `0 2 1 3 4` 或者 `0 3 1 2 4` 或者其他类似的序列,这主要取决于每个协程何时开始被执行。
|
||||
|
||||
在版本D中,我们输出这个数组的值,为什么版本B不能而版本D可以呢?
|
||||
在版本 D 中,我们输出这个数组的值,为什么版本 B 不能而版本 D 可以呢?
|
||||
|
||||
因为版本D中的变量声明是在循环体内部,所以在每次循环时,这些变量相互之间是不共享的,所以这些变量可以单独的被每个闭包使用。
|
||||
因为版本 D 中的变量声明是在循环体内部,所以在每次循环时,这些变量相互之间是不共享的,所以这些变量可以单独的被每个闭包使用。
|
||||
|
||||
## 链接
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# 17.1 逗号 ok 模式
|
||||
|
||||
在学习本书第二部分和第三部分时,我们经常在一个表达式返回2个参数时使用这种模式:`,ok`,第一个参数是一个值或者`nil`,第二个参数是`true`/`false`或者一个错误`error`。在一个需要赋值的`if`条件语句中,使用这种模式去检测第二个参数值会让代码显得优雅简洁。这种模式在go语言编码规范中非常重要。下面总结了所有使用这种模式的例子:
|
||||
在学习本书第二部分和第三部分时,我们经常在一个表达式返回 2 个参数时使用这种模式:`,ok`,第一个参数是一个值或者 `nil`,第二个参数是 `true`/`false` 或者一个错误 `error`。在一个需要赋值的 `if` 条件语句中,使用这种模式去检测第二个参数值会让代码显得优雅简洁。这种模式在 go 语言编码规范中非常重要。下面总结了所有使用这种模式的例子:
|
||||
|
||||
(1)在函数返回时检测错误(参考[第5.2小节](05.2.md)):
|
||||
|
||||
@@ -18,7 +18,7 @@ Process(value)
|
||||
e.g.: os.Open(file) strconv.Atoi(str)
|
||||
```
|
||||
|
||||
这段代码中的函数将错误返回给它的调用者,当函数执行成功时,返回的错误是`nil`,所以使用这种写法:
|
||||
这段代码中的函数将错误返回给它的调用者,当函数执行成功时,返回的错误是 `nil`,所以使用这种写法:
|
||||
|
||||
```go
|
||||
func SomeFunc() error {
|
||||
@@ -32,30 +32,30 @@ func SomeFunc() error {
|
||||
}
|
||||
```
|
||||
|
||||
这种模式也常用于通过`defer`使程序从`panic`中恢复执行(参考[第17.2(4)小节](17.2.md))。
|
||||
这种模式也常用于通过 `defer` 使程序从 `panic` 中恢复执行(参考[第17.2(4)小节](17.2.md))。
|
||||
|
||||
要实现简洁的错误检测代码,更好的方式是使用闭包,参考[第16.10.2小节](16.10.md)
|
||||
|
||||
(2)检测映射中是否存在一个键值(参考[第8.2小节](08.2.md)):`key1`在映射`map1`中是否有值?
|
||||
(2)检测映射中是否存在一个键值(参考[第8.2小节](08.2.md)):`key1` 在映射 `map1` 中是否有值?
|
||||
|
||||
```go
|
||||
if value, isPresent = map1[key1]; isPresent {
|
||||
Process(value)
|
||||
}
|
||||
// key1不存在
|
||||
// key1 不存在
|
||||
…
|
||||
```
|
||||
|
||||
(3)检测一个接口类型变量`varI`是否包含了类型`T`:类型断言(参考[第11.3小节](11.3.md)):
|
||||
(3)检测一个接口类型变量 `varI` 是否包含了类型 `T`:类型断言(参考[第11.3小节](11.3.md)):
|
||||
|
||||
```go
|
||||
if value, ok := varI.(T); ok {
|
||||
Process(value)
|
||||
}
|
||||
// 接口类型varI没有包含类型T
|
||||
// 接口类型 varI 没有包含类型 T
|
||||
```
|
||||
|
||||
(4)检测一个通道`ch`是否关闭(参考[第14.3小节](14.3.md)):
|
||||
(4)检测一个通道 `ch` 是否关闭(参考[第14.3小节](14.3.md)):
|
||||
|
||||
```go
|
||||
for input := range ch {
|
||||
|
@@ -15,7 +15,7 @@ s2:= string(c) // s2 == "cello"
|
||||
substr := str[n:m]
|
||||
```
|
||||
|
||||
(3)如何使用`for`或者`for-range`遍历一个字符串:
|
||||
(3)如何使用 `for` 或者 `for-range` 遍历一个字符串:
|
||||
|
||||
```go
|
||||
// gives only the bytes:
|
||||
@@ -43,7 +43,7 @@ for ix, ch := range str {
|
||||
|
||||
`Strings.Join()`(参考[章节4.7](04.7.md))
|
||||
|
||||
使用`+=`:
|
||||
使用 `+=`:
|
||||
|
||||
```go
|
||||
str1 := "Hello "
|
||||
@@ -51,7 +51,7 @@ for ix, ch := range str {
|
||||
str1 += str2 //str1 == "Hello World!"
|
||||
```
|
||||
|
||||
(6)如何解析命令行参数:使用`os`或者`flag`包
|
||||
(6)如何解析命令行参数:使用 `os` 或者 `flag` 包
|
||||
|
||||
(参考[例12.4](examples/chapter_12/fileinput.go))
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# 18.11 出于性能考虑的最佳实践和建议
|
||||
|
||||
(1)尽可能的使用`:=`去初始化声明一个变量(在函数内部);
|
||||
(1)尽可能的使用 `:=` 去初始化声明一个变量(在函数内部);
|
||||
|
||||
(2)尽可能的使用字符代替字符串;
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
|
||||
(4)尽可能的使用数组和切片代替映射(详见参考文献15);
|
||||
|
||||
(5)如果只想获取切片中某项值,不需要值的索引,尽可能的使用`for range`去遍历切片,这比必须查询切片中的每个元素要快一些;
|
||||
(5)如果只想获取切片中某项值,不需要值的索引,尽可能的使用 `for range` 去遍历切片,这比必须查询切片中的每个元素要快一些;
|
||||
|
||||
(6)当数组元素是稀疏的(例如有很多`0`值或者空值`nil`),使用映射会降低内存消耗;
|
||||
(6)当数组元素是稀疏的(例如有很多 `0` 值或者空值 `nil`),使用映射会降低内存消耗;
|
||||
|
||||
(7)初始化映射时指定其容量;
|
||||
|
||||
|
@@ -18,7 +18,7 @@
|
||||
|
||||
`line = line[:len(line)-1]`
|
||||
|
||||
(2)如何使用`for`或者`for-range`遍历一个数组(或者切片):
|
||||
(2)如何使用 `for` 或者 `for-range` 遍历一个数组(或者切片):
|
||||
|
||||
```go
|
||||
for i:=0; i < len(arr); i++ {
|
||||
@@ -29,7 +29,7 @@ for ix, value := range arr {
|
||||
}
|
||||
```
|
||||
|
||||
(3)如何在一个二维数组或者切片`arr2Dim`中查找一个指定值`V`:
|
||||
(3)如何在一个二维数组或者切片 `arr2Dim` 中查找一个指定值 `V`:
|
||||
|
||||
```go
|
||||
found := false
|
||||
@@ -47,4 +47,4 @@ Found: for row := range arr2Dim {
|
||||
|
||||
- [目录](directory.md)
|
||||
- 上一节:[字符串](18.1.md)
|
||||
- 下一节:[映射](18.3.md)
|
||||
- 下一节:[映射](18.3.md)
|
||||
|
@@ -4,7 +4,7 @@
|
||||
|
||||
初始化: `map1 := map[string]int{"one": 1, "two": 2}`
|
||||
|
||||
(1)如何使用`for`或者`for-range`遍历一个映射:
|
||||
(1)如何使用 `for` 或者 `for-range` 遍历一个映射:
|
||||
|
||||
```go
|
||||
for key, value := range map1 {
|
||||
@@ -12,11 +12,11 @@ for key, value := range map1 {
|
||||
}
|
||||
```
|
||||
|
||||
(2)如何在一个映射中检测键`key1`是否存在:
|
||||
(2)如何在一个映射中检测键 `key1` 是否存在:
|
||||
|
||||
`val1, isPresent = map1[key1]`
|
||||
|
||||
返回值:键`key1`对应的值或者`0`, `true`或者`false`
|
||||
返回值:键 `key1` 对应的值或者 `0`,`true` 或者 `false`
|
||||
|
||||
(3)如何在映射中删除一个键:
|
||||
|
||||
@@ -26,4 +26,4 @@ for key, value := range map1 {
|
||||
|
||||
- [目录](directory.md)
|
||||
- 上一节:[数组和切片](18.2.md)
|
||||
- 下一节:[结构体](18.4.md)
|
||||
- 下一节:[结构体](18.4.md)
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# 18.5 接口
|
||||
|
||||
(1)如何检测一个值`v`是否实现了接口`Stringer`:
|
||||
(1)如何检测一个值 `v` 是否实现了接口 `Stringer`:
|
||||
|
||||
```go
|
||||
if v, ok := v.(Stringer); ok {
|
||||
@@ -35,4 +35,4 @@ func classifier(items ...interface{}) {
|
||||
|
||||
- [目录](directory.md)
|
||||
- 上一节:[结构体](18.4.md)
|
||||
- 下一节:[函数](18.6.md)
|
||||
- 下一节:[函数](18.6.md)
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# 18.6 函数
|
||||
|
||||
如何使用内建函数`recover`终止`panic`过程(参考[章节13.3](13.3.md)):
|
||||
如何使用内建函数 `recover` 终止 `panic` 过程(参考[章节13.3](13.3.md)):
|
||||
|
||||
```go
|
||||
func protect(g func()) {
|
||||
@@ -20,4 +20,4 @@ func protect(g func()) {
|
||||
|
||||
- [目录](directory.md)
|
||||
- 上一节:[接口](18.5.md)
|
||||
- 下一节:[文件](18.7.md)
|
||||
- 下一节:[文件](18.7.md)
|
||||
|
@@ -6,15 +6,15 @@
|
||||
|
||||
1 出于性能考虑建议使用带缓存的通道:
|
||||
|
||||
使用带缓存的通道可以很轻易成倍提高它的吞吐量,某些场景其性能可以提高至10倍甚至更多。通过调整通道的容量,甚至可以尝试着更进一步的优化其性能。
|
||||
使用带缓存的通道可以很轻易成倍提高它的吞吐量,某些场景其性能可以提高至 10 倍甚至更多。通过调整通道的容量,甚至可以尝试着更进一步的优化其性能。
|
||||
|
||||
2 限制一个通道的数据数量并将它们封装成一个数组:
|
||||
|
||||
如果使用通道传递大量单独的数据,那么通道将变成性能瓶颈。然而,将数据块打包封装成数组,在接收端解压数据时,性能可以提高至10倍。
|
||||
如果使用通道传递大量单独的数据,那么通道将变成性能瓶颈。然而,将数据块打包封装成数组,在接收端解压数据时,性能可以提高至 10 倍。
|
||||
|
||||
创建:`ch := make(chan type,buf)`
|
||||
|
||||
(1)如何使用`for`或者`for-range`遍历一个通道:
|
||||
(1)如何使用 `for` 或者 `for-range` 遍历一个通道:
|
||||
|
||||
```go
|
||||
for v := range ch {
|
||||
@@ -22,7 +22,7 @@ for v := range ch {
|
||||
}
|
||||
```
|
||||
|
||||
(2)如何检测一个通道`ch`是否关闭:
|
||||
(2)如何检测一个通道 `ch` 是否关闭:
|
||||
|
||||
```go
|
||||
//read channel until it closes or error-condition
|
||||
@@ -51,7 +51,7 @@ doSomethingElseForAWhile()
|
||||
<-ch // Wait for goroutine to finish; discard sent value.
|
||||
```
|
||||
|
||||
如果希望程序一直阻塞,在匿名函数中省略 `ch <- 1`即可。
|
||||
如果希望程序一直阻塞,在匿名函数中省略 `ch <- 1` 即可。
|
||||
|
||||
(4)通道的工厂模板:以下函数是一个通道工厂,启动一个匿名函数作为协程以生产通道:
|
||||
|
||||
@@ -113,4 +113,4 @@ func Worker(in, out chan *Task) {
|
||||
|
||||
- [目录](directory.md)
|
||||
- 上一节:[文件](18.7.md)
|
||||
- 下一节:[网络和网页应用](18.9.md)
|
||||
- 下一节:[网络和网页应用](18.9.md)
|
||||
|
@@ -8,7 +8,7 @@
|
||||
var strTempl = template.Must(template.New("TName").Parse(strTemplateHTML))
|
||||
```
|
||||
|
||||
在网页应用中使用HTML过滤器过滤HTML特殊字符:
|
||||
在网页应用中使用 HTML 过滤器过滤 HTML 特殊字符:
|
||||
|
||||
`{{html .}}` 或者通过一个字段 `FieldName {{ .FieldName |html }}`
|
||||
|
||||
@@ -18,4 +18,4 @@ var strTempl = template.Must(template.New("TName").Parse(strTemplateHTML))
|
||||
|
||||
- [目录](directory.md)
|
||||
- 上一节:[协程(goroutine)与通道(channel)](18.8.md)
|
||||
- 下一节:[其他](18.10.md)
|
||||
- 下一节:[其他](18.10.md)
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
你肯定知道有些浏览器中的地址(称为 URL)非常长且/或复杂,在网上有一些将他们转换成简短 URL 来使用的服务。我们的项目与此类似:它是具有 2 个功能的 *web 服务*(web service):
|
||||
|
||||
## 添加 (Add)
|
||||
## 添加(Add)
|
||||
|
||||
给定一个较长的 URL,会将其转换成较短的版本,例如:
|
||||
```
|
||||
@@ -11,7 +11,7 @@ http://maps.google.com/maps?f=q&source=s_q&hl=en&geocode=&q=tokyo&sll=37.0625,-9
|
||||
- (A) 转变为:`http://goto/UrcGq`
|
||||
- (B) 并保存这对数据
|
||||
|
||||
## 重定向 (Redirect)
|
||||
## 重定向(Redirect)
|
||||
|
||||
短网址被请求时,会把用户重定向到原始的长 URL。因此如果你在浏览器输入网址 (B),会被重定向到页面 (A)。
|
||||
|
||||
|
@@ -19,7 +19,7 @@ type URLStore struct {
|
||||
}
|
||||
```
|
||||
|
||||
通道和 map 一样,必须用 `make` 创建。我们会以此修改 `NewURLStore` 工厂函数,并给定缓冲区大小为1000,例如:`save := make(chan record, saveQueueLength)`。为解决性能问题,`Put` 可以发送记录 record 到带缓冲的 `save` 通道:
|
||||
通道和 map 一样,必须用 `make` 创建。我们会以此修改 `NewURLStore` 工厂函数,并给定缓冲区大小为 1000,例如:`save := make(chan record, saveQueueLength)`。为解决性能问题,`Put` 可以发送记录 record 到带缓冲的 `save` 通道:
|
||||
```go
|
||||
func (s *URLStore) Put(url string) string {
|
||||
for {
|
||||
|
Reference in New Issue
Block a user