mirror of
https://github.com/unknwon/the-way-to-go_ZH_CN.git
synced 2025-08-12 02:35:53 +08:00
修改部分描述,添加必要的标点符号,补充代码超链接 (#804)
This commit is contained in:
@@ -97,7 +97,7 @@ import ("fmt"; "os")
|
|||||||
|
|
||||||
**可见性规则**
|
**可见性规则**
|
||||||
|
|
||||||
当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 private )。
|
当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是它们在整个包的内部是可见并且可用的(像面向对象语言中的 private )。
|
||||||
|
|
||||||
(大写字母可以使用任何 Unicode 编码的字符,比如希腊文,不仅仅是 ASCII 码中的大写字母)。
|
(大写字母可以使用任何 Unicode 编码的字符,比如希腊文,不仅仅是 ASCII 码中的大写字母)。
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ import ("fmt"; "os")
|
|||||||
|
|
||||||
假设在包 pack1 中我们有一个变量或函数叫做 Thing(以 T 开头,所以它能够被导出),那么在当前包中导入 pack1 包,Thing 就可以像面向对象语言那样使用点标记来调用:`pack1.Thing`(pack1 在这里是不可以省略的)。
|
假设在包 pack1 中我们有一个变量或函数叫做 Thing(以 T 开头,所以它能够被导出),那么在当前包中导入 pack1 包,Thing 就可以像面向对象语言那样使用点标记来调用:`pack1.Thing`(pack1 在这里是不可以省略的)。
|
||||||
|
|
||||||
因此包也可以作为命名空间使用,帮助避免命名冲突(名称冲突):两个包中的同名变量的区别在于他们的包名,例如 `pack1.Thing` 和 `pack2.Thing`。
|
因此包也可以作为命名空间使用,帮助避免命名冲突(名称冲突):两个包中的同名变量的区别在于它们的包名,例如 `pack1.Thing` 和 `pack2.Thing`。
|
||||||
|
|
||||||
你可以通过使用包的别名来解决包名之间的名称冲突,或者说根据你的个人喜好对包名进行重新设置,如:`import fm "fmt"`。下面的代码展示了如何使用包的别名:
|
你可以通过使用包的别名来解决包名之间的名称冲突,或者说根据你的个人喜好对包名进行重新设置,如:`import fm "fmt"`。下面的代码展示了如何使用包的别名:
|
||||||
|
|
||||||
|
@@ -40,9 +40,9 @@ aVar != 10 -> false
|
|||||||
|
|
||||||
Go 对于值之间的比较有非常严格的限制,只有两个类型相同的值才可以进行比较,如果值的类型是接口(interface,第 11 章),它们也必须都实现了相同的接口。如果其中一个值是常量,那么另外一个值的类型必须和该常量类型相兼容的。如果以上条件都不满足,则其中一个值的类型必须在被转换为和另外一个值的类型相同之后才可以进行比较。
|
Go 对于值之间的比较有非常严格的限制,只有两个类型相同的值才可以进行比较,如果值的类型是接口(interface,第 11 章),它们也必须都实现了相同的接口。如果其中一个值是常量,那么另外一个值的类型必须和该常量类型相兼容的。如果以上条件都不满足,则其中一个值的类型必须在被转换为和另外一个值的类型相同之后才可以进行比较。
|
||||||
|
|
||||||
布尔型的常量和变量也可以通过和逻辑运算符(非 `!`、和 `&&`、或 `||`)结合来产生另外一个布尔值,这样的逻辑语句就其本身而言,并不是一个完整的 Go 语句。
|
布尔型的常量和变量也可以通过和逻辑运算符(非 `!`、与 `&&`、或 `||`)结合来产生另外一个布尔值,这样的逻辑语句就其本身而言,并不是一个完整的 Go 语句。
|
||||||
|
|
||||||
逻辑值可以被用于条件结构中的条件语句(第 5 章),以便测试某个条件是否满足。另外,和 `&&`、或 `||` 与相等 `==` 或不等 `!=` 属于二元运算符,而非 `!` 属于一元运算符。在接下来的内容中,我们会使用 T 来代表条件符合的语句,用 F 来代表条件不符合的语句。
|
逻辑值可以被用于条件结构中的条件语句(第 5 章),以便测试某个条件是否满足。另外,与 `&&`、或 `||` 与相等 `==` 或不等 `!=` 属于二元运算符,而非 `!` 属于一元运算符。在接下来的内容中,我们会使用 T 来代表条件符合的语句,用 F 来代表条件不符合的语句。
|
||||||
|
|
||||||
Go 语言中包含以下逻辑运算符:
|
Go 语言中包含以下逻辑运算符:
|
||||||
|
|
||||||
@@ -228,7 +228,7 @@ Go 拥有以下复数类型:
|
|||||||
complex64 (32 位实数和虚数)
|
complex64 (32 位实数和虚数)
|
||||||
complex128 (64 位实数和虚数)
|
complex128 (64 位实数和虚数)
|
||||||
|
|
||||||
复数使用 `re+imI` 来表示,其中 `re` 代表实数部分,`im` 代表虚数部分,I 代表根号负 1。
|
复数使用 `re+imI` 来表示,其中 `re` 代表实数部分,`im` 代表虚数部分,`I` 代表根号负 1。
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
# 6.11 计算函数执行时间
|
# 6.11 计算函数执行时间
|
||||||
|
|
||||||
有时候,能够知道一个计算执行消耗的时间是非常有意义的,尤其是在对比和基准测试中。最简单的一个办法就是在计算开始之前设置一个起始时候,再由计算结束时的结束时间,最后取出它们的差值,就是这个计算所消耗的时间。想要实现这样的做法,可以使用 `time` 包中的 `Now()` 和 `Sub` 函数:
|
有时候,能够知道一个计算执行消耗的时间是非常有意义的,尤其是在对比和基准测试中。最简单的一个办法就是在计算开始之前设置一个起始时间,再记录计算结束时的结束时间,最后计算它们的差值,就是这个计算所消耗的时间。想要实现这样的做法,可以使用 `time` 包中的 `Now()` 和 `Sub` 函数:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
而我们要做就是将第 n 个数的值存在数组中索引为 n 的位置(详见第 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 秒
|
- 普通写法:4.730270 秒
|
||||||
- 内存缓存:0.001000 秒
|
- 内存缓存:0.001000 秒
|
||||||
|
@@ -84,7 +84,7 @@ func MultiPly3Nums(a int, b int, c int) int {
|
|||||||
|
|
||||||
`getX2AndX3` 与 `getX2AndX3_2` 两个函数演示了如何使用非命名返回值与命名返回值的特性。当需要返回多个非命名返回值时,需要使用 `()` 把它们括起来,比如 `(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)
|
示例 6.3 [multiple_return.go](examples/chapter_6/multiple_return.go)
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
# 6.7 将函数作为参数
|
# 6.7 将函数作为参数
|
||||||
|
|
||||||
函数可以作为其它函数的参数进行传递,然后在其它函数内调用执行,一般称之为回调。下面是一个将函数作为参数的简单例子(function_parameter.go):
|
函数可以作为其它函数的参数进行传递,然后在其它函数内调用执行,一般称之为回调。下面是一个将函数作为参数的简单例子([function_parameter.go](examples/chapter_6/function_parameter.go)):
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
|
@@ -20,7 +20,7 @@ func() {
|
|||||||
|
|
||||||
表示参数列表的第一对括号必须紧挨着关键字 `func`,因为匿名函数没有名称。花括号 `{}` 涵盖着函数体,最后的一对括号表示对该匿名函数的调用。
|
表示参数列表的第一对括号必须紧挨着关键字 `func`,因为匿名函数没有名称。花括号 `{}` 涵盖着函数体,最后的一对括号表示对该匿名函数的调用。
|
||||||
|
|
||||||
下面的例子展示了如何将匿名函数赋值给变量并对其进行调用(function_literal.go):
|
下面的例子展示了如何将匿名函数赋值给变量并对其进行调用([function_literal.go](examples/chapter_6/function_literal.go)):
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
@@ -63,7 +63,7 @@ func (u string) {
|
|||||||
}(v)
|
}(v)
|
||||||
```
|
```
|
||||||
|
|
||||||
请学习以下示例并思考(return_defer.go):函数 `f` 返回时,变量 `ret` 的值是什么?
|
请学习以下示例并思考([return_defer.go](examples/chapter_6/return_defer.go)):函数 `f` 返回时,变量 `ret` 的值是什么?
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
# 6.9 应用闭包:将函数作为返回值
|
# 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
|
```go
|
||||||
func Add2() (func(b int) int)
|
func Add2() (func(b int) int)
|
||||||
@@ -9,7 +9,7 @@ func Adder(a int) (func(b int) int)
|
|||||||
|
|
||||||
函数 Add2 不接受任何参数,但函数 Adder 接受一个 int 类型的整数作为参数。
|
函数 Add2 不接受任何参数,但函数 Adder 接受一个 int 类型的整数作为参数。
|
||||||
|
|
||||||
我们也可以将 Adder 返回的函数存到变量中(function_return.go)。
|
我们也可以将 Adder 返回的函数存到变量中([function_return.go](examples/chapter_6/function_return.go))。
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
@@ -45,7 +45,7 @@ Call Add2 for 3 gives: 5
|
|||||||
The result is: 5
|
The result is: 5
|
||||||
```
|
```
|
||||||
|
|
||||||
下例为一个略微不同的实现(function_closure.go):
|
下例为一个略微不同的实现([function_closure.go](examples/chapter_6/function_closure.go)):
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
# 7.0 数组与切片
|
# 7.0 数组与切片
|
||||||
|
|
||||||
这章我们开始剖析 **容器**, 它是可以包含大量条目(item)的数据结构, 例如数组、切片和 map。从这看到 Go 明显受到 Python 的影响。
|
这章我们开始剖析 **容器**,它是可以包含大量条目(item)的数据结构,例如数组、切片和 map。从这看到 Go 明显受到 Python 的影响。
|
||||||
|
|
||||||
以 `[]` 符号标识的数组类型几乎在所有的编程语言中都是一个基本主力。Go 语言中的数组也是类似的,只是有一些特点。Go 没有 C 那么灵活,但是拥有切片(slice)类型。这是一种建立在 Go 语言数组类型之上的抽象,要想理解切片我们必须先理解数组。数组有特定的用处,但是却有一些呆板,所以在 Go 语言的代码里并不是特别常见。相对的,切片确实随处可见的。它们构建在数组之上并且提供更强大的能力和便捷。
|
以 `[]` 符号标识的数组类型几乎在所有的编程语言中都是一个基本主力。Go 语言中的数组也是类似的,只是有一些特点。Go 没有 C 那么灵活,但是拥有切片(slice)类型。这是一种建立在 Go 语言数组类型之上的抽象,要想理解切片我们必须先理解数组。数组有特定的用处,但是却有一些呆板,所以在 Go 语言的代码里并不是特别常见。相对的,切片确实随处可见的。它们构建在数组之上并且提供更强大的能力和便捷。
|
||||||
|
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
# 7.1 声明和初始化
|
# 7.1 声明和初始化
|
||||||
|
|
||||||
## 7.1.1 概念
|
## 7.1.1 概念
|
||||||
数组是具有相同 **唯一类型** 的一组已编号且长度固定的数据项序列(这是一种同构的数据结构);这种类型可以是任意的原始类型例如整型、字符串或者自定义类型。数组长度必须是一个常量表达式,并且必须是一个非负整数。数组长度也是数组类型的一部分,所以[5]int和[10]int是属于不同类型的。数组的编译时值初始化是按照数组顺序完成的(如下)。
|
数组是具有相同 **唯一类型** 的一组已编号且长度固定的数据项序列(这是一种同构的数据结构);这种类型可以是任意的原始类型例如整型、字符串或者自定义类型。数组长度必须是一个常量表达式,并且必须是一个非负整数。数组长度也是数组类型的一部分,所以 `[5]int` 和 `[10]int` 是属于不同类型的。数组的编译时值初始化是按照数组顺序完成的(如下)。
|
||||||
|
|
||||||
**注意事项** 如果我们想让数组元素类型为任意类型的话可以使用空接口作为类型(参考 [第 11 章](11.9.md))。当使用值时我们必须先做一个类型判断(参考 [第 11 章](11.3.md))。
|
**注意事项** 如果我们想让数组元素类型为任意类型的话可以使用空接口作为类型(参考 [第 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
|
runtime error: index out of range
|
||||||
|
|
||||||
由于索引的存在,遍历数组的方法自然就是使用 for 结构:
|
由于索引的存在,遍历数组的方法自然就是使用 for 结构:
|
||||||
|
|
||||||
- 通过 for 初始化数组项
|
- 通过 for 初始化数组项
|
||||||
- 通过 for 打印数组元素
|
- 通过 for 打印数组元素
|
||||||
@@ -181,7 +181,7 @@ var arrAge = [5]int{18, 20, 15, 22, 16}
|
|||||||
var arrLazy = [...]int{5, 6, 7, 8, 22}
|
var arrLazy = [...]int{5, 6, 7, 8, 22}
|
||||||
```
|
```
|
||||||
|
|
||||||
`...` 可同样可以忽略,从技术上说它们其实变化成了切片。
|
`...` 同样可以忽略,从技术上说它们其实变成了切片。
|
||||||
|
|
||||||
第三种变化:`key: value 语法`
|
第三种变化:`key: value 语法`
|
||||||
|
|
||||||
@@ -258,7 +258,7 @@ func main() {
|
|||||||
|
|
||||||
## 7.1.4 将数组传递给函数
|
## 7.1.4 将数组传递给函数
|
||||||
|
|
||||||
把一个大数组传递给函数会消耗很多内存。有两种方法可以避免这种现象:
|
把一个大数组传递给函数会消耗很多内存。有两种方法可以避免这种情况:
|
||||||
|
|
||||||
- 传递数组的指针
|
- 传递数组的指针
|
||||||
- 使用数组的切片
|
- 使用数组的切片
|
||||||
|
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
切片的初始化格式是:`var slice1 []type = arr1[start:end]`。
|
切片的初始化格式是:`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`。
|
如果某个人写:`var slice1 []type = arr1[:]` 那么 slice1 就等于完整的 arr1 数组(所以这种表示方式是 `arr1[0:len(arr1)]` 的一种缩写)。另外一种表述方式是:`slice1 = &arr1`。
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
如果你想去掉 slice1 的最后一个元素,只要 `slice1 = slice1[:len(slice1)-1]`。
|
如果你想去掉 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[:]` 是用切片组成的切片,拥有相同的元素,但是仍然指向相同的相关数组。
|
`s2 := s[:]` 是用切片组成的切片,拥有相同的元素,但是仍然指向相同的相关数组。
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ func main() {
|
|||||||
|
|
||||||
如果 s2 是一个 slice,你可以将 s2 向后移动一位 `s2 = s2[1:]`,但是末尾没有移动。切片只能向后移动,`s2 = s2[-1:]` 会导致编译错误。切片不能被重新分片以获取数组的前一个元素。
|
如果 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: 给定切片 `b:= []byte{'g', 'o', 'l', 'a', 'n', 'g'}`,那么 `b[1:4]`、`b[:2]`、`b[2:]` 和 `b[:]` 分别是什么?
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ func main() {
|
|||||||
|
|
||||||
## 7.2.3 用 make() 创建一个切片
|
## 7.2.3 用 make() 创建一个切片
|
||||||
|
|
||||||
当相关数组还没有定义时,我们可以使用 make() 函数来创建一个切片 同时创建好相关数组:`var slice1 []type = make([]type, len)`。
|
当相关数组还没有定义时,我们可以使用 make() 函数来创建一个切片,同时创建好相关数组:`var slice1 []type = make([]type, len)`。
|
||||||
|
|
||||||
也可以简写为 `slice1 := make([]type, len)`,这里 `len` 是数组的长度并且也是 `slice` 的初始长度。
|
也可以简写为 `slice1 := make([]type, len)`,这里 `len` 是数组的长度并且也是 `slice` 的初始长度。
|
||||||
|
|
||||||
@@ -243,7 +243,7 @@ v := make([]int, 10, 50)
|
|||||||
|
|
||||||
## 7.2.6 bytes 包
|
## 7.2.6 bytes 包
|
||||||
|
|
||||||
类型 `[]byte` 的切片十分常见,Go 语言有一个 bytes 包专门用来解决这种类型的操作方法。
|
类型 `[]byte` 的切片十分常见,Go 语言有一个 bytes 包专门用来提供这种类型的操作方法。
|
||||||
|
|
||||||
bytes 包和字符串包十分类似(参见第 4.7 节)。而且它还包含一个十分有用的类型 Buffer:
|
bytes 包和字符串包十分类似(参见第 4.7 节)。而且它还包含一个十分有用的类型 Buffer:
|
||||||
|
|
||||||
|
@@ -89,7 +89,7 @@ b) 如果 a) 无法正常工作,写一个 for 循环让值可以 double。
|
|||||||
|
|
||||||
**问题 7.6** 通过使用省略号操作符 `...` 来实现累加方法。
|
**问题 7.6** 通过使用省略号操作符 `...` 来实现累加方法。
|
||||||
|
|
||||||
**练习 7.7** sum_array.go
|
**练习 7.7** [sum_array.go](exercises/chapter_7/sum_array.go)
|
||||||
|
|
||||||
a) 写一个 Sum 函数,传入参数为一个 32 位 float 数组成的数组 arrF,返回该数组的所有数字和。
|
a) 写一个 Sum 函数,传入参数为一个 32 位 float 数组成的数组 arrF,返回该数组的所有数字和。
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ a) 写一个 Sum 函数,传入参数为一个 32 位 float 数组成的数组
|
|||||||
|
|
||||||
b) 写一个 SumAndAverage 方法,返回两个 int 和 float32 类型的未命名变量的和与平均值。
|
b) 写一个 SumAndAverage 方法,返回两个 int 和 float32 类型的未命名变量的和与平均值。
|
||||||
|
|
||||||
**练习 7.8** min_max.go
|
**练习 7.8** [min_max.go](exercises/chapter_7/min_max.go)
|
||||||
|
|
||||||
写一个 minSlice 方法,传入一个 int 的切片并且返回最小值,再写一个 maxSlice 方法返回最大值。
|
写一个 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...)`。
|
如果你想将切片 y 追加到切片 x 后面,只要将第二个参数扩展成一个列表即可:`x = append(x, y...)`。
|
||||||
|
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
假设 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
|
```go
|
||||||
package main
|
package main
|
||||||
@@ -37,7 +37,7 @@ b = append(b, s...)
|
|||||||
|
|
||||||
## 7.6.2 获取字符串的某一部分
|
## 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 字符串和切片的内存结构
|
## 7.6.3 字符串和切片的内存结构
|
||||||
|
|
||||||
|
@@ -80,7 +80,7 @@ mapAssigned 也是 mapLit 的引用,对 mapAssigned 的修改也会影响到 m
|
|||||||
|
|
||||||
**不要使用 new,永远用 make 来构造 map**
|
**不要使用 new,永远用 make 来构造 map**
|
||||||
|
|
||||||
**注意** 如果你错误的使用 new() 分配了一个引用对象,你会获得一个空引用的指针,相当于声明了一个未初始化的变量并且取了它的地址:
|
**注意** 如果你错误地使用 new() 分配了一个引用对象,你会获得一个空引用的指针,相当于声明了一个未初始化的变量并且取了它的地址:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
mapCreated := new(map[string]float32)
|
mapCreated := new(map[string]float32)
|
||||||
@@ -130,7 +130,7 @@ noteFrequency := map[string]float32 {
|
|||||||
|
|
||||||
## 8.1.3 用切片作为 map 的值
|
## 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 的例子:
|
这里有一些定义这种 map 的例子:
|
||||||
|
|
||||||
|
@@ -50,7 +50,7 @@ func main() {
|
|||||||
sorted:
|
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 /
|
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
|
```go
|
||||||
type name struct {
|
type name struct {
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
- `unsafe`: 包含了一些打破 Go 语言“类型安全”的命令,一般的程序中不会被使用,可用在 C/C++ 程序的调用中。
|
- `unsafe`: 包含了一些打破 Go 语言“类型安全”的命令,一般的程序中不会被使用,可用在 C/C++ 程序的调用中。
|
||||||
- `syscall`-`os`-`os/exec`:
|
- `syscall`-`os`-`os/exec`:
|
||||||
- `os`: 提供给我们一个平台无关性的操作系统功能接口,采用类Unix设计,隐藏了不同操作系统间差异,让不同的文件系统和操作系统对象表现一致。
|
- `os`: 提供给我们一个平台无关性的操作系统功能接口,采用类 Unix 设计,隐藏了不同操作系统间的差异,让不同的文件系统和操作系统对象表现一致。
|
||||||
- `os/exec`: 提供我们运行外部操作系统命令和程序的方式。
|
- `os/exec`: 提供我们运行外部操作系统命令和程序的方式。
|
||||||
- `syscall`: 底层的外部包,提供了操作系统底层调用的基本接口。
|
- `syscall`: 底层的外部包,提供了操作系统底层调用的基本接口。
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `archive/tar` 和 `/zip-compress`:压缩(解压缩)文件功能。
|
- `archive/tar` 和 `/zip-compress`:压缩(解压缩)文件功能。
|
||||||
- `fmt`-`io`-`bufio`-`path/filepath`-`flag`:
|
- `fmt`-`io`-`bufio`-`path/filepath`-`flag`:
|
||||||
- `fmt`: 提供了格式化输入输出功能。
|
- `fmt`: 提供了格式化输入输出功能。
|
||||||
- `io`: 提供了基本输入输出功能,大多数是围绕系统功能的封装。
|
- `io`: 提供了基本输入输出功能,大多数是围绕系统功能的封装。
|
||||||
@@ -66,10 +66,10 @@ for e := l.Front(); e != nil; e = e.Next() {
|
|||||||
|
|
||||||
- `time`-`log`:
|
- `time`-`log`:
|
||||||
- `time`: 日期和时间的基本操作。
|
- `time`: 日期和时间的基本操作。
|
||||||
- `log`: 记录程序运行时产生的日志,我们将在后面的章节使用它。
|
- `log`: 记录程序运行时产生的日志,我们将在后面的章节使用它。
|
||||||
- `encoding/json`-`encoding/xml`-`text/template`:
|
- `encoding/json`-`encoding/xml`-`text/template`:
|
||||||
- `encoding/json`: 读取并解码和写入并编码 JSON 数据。
|
- `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 节)。
|
- `text/template`:生成像 HTML 一样的数据与文本混合的数据驱动模板(参见第 15.7 节)。
|
||||||
- `net`-`net/http`-`html`:(参见第 15 章)
|
- `net`-`net/http`-`html`:(参见第 15 章)
|
||||||
- `net`: 网络数据的基本操作。
|
- `net`: 网络数据的基本操作。
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
# 9.11 在 Go 程序中使用外部库
|
# 9.11 在 Go 程序中使用外部库
|
||||||
|
|
||||||
(本节我们将创建一个 Web 应用和它的 Google App Engine 版本,在第 19 和 21 章分别说明,当你阅读到这些章节时可以再回到这个例子。)
|
(本节我们将创建一个 Web 应用和它的 Google App Engine 版本,在第 19 和 21 章分别说明,当你阅读到这些章节时可以再回到这个例子。)
|
||||||
|
|
||||||
当开始一个新项目或增加新的功能到现有的项目,你可以通过在应用程序中使用已经存在的库来节省开发时间。为了做到这一点,你必须理解库的 API(应用编程接口),那就是:库中有哪些方法可以调用,如何调用。你可能没有这个库的源代码,但作者肯定有记载的 API 以及详细介绍了如何使用它。
|
当开始一个新项目或增加新的功能到现有的项目,你可以通过在应用程序中使用已经存在的库来节省开发时间。为了做到这一点,你必须理解库的 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/) 查看。
|
备注:谷歌让通过使用 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"
|
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:
|
要调用 urlshortener 接口必须先通过 http 包中的默认客户端创建一个服务实例 urlshortenerSvc:
|
||||||
```go
|
```go
|
||||||
@@ -138,7 +138,7 @@ handlers:
|
|||||||
script: _go_app
|
script: _go_app
|
||||||
```
|
```
|
||||||
|
|
||||||
现在你可以去到你的项目目录并在终端运行:`dev_appserver.py urlshort`
|
现在你可以到你的项目目录并在终端运行:`dev_appserver.py urlshort`
|
||||||
|
|
||||||
在浏览器打开你的 Web应用:http://localhost:8080。
|
在浏览器打开你的 Web应用:http://localhost:8080。
|
||||||
|
|
||||||
|
@@ -10,7 +10,7 @@
|
|||||||
ok, _ := regexp.Match(pat, []byte(searchIn))
|
ok, _ := regexp.Match(pat, []byte(searchIn))
|
||||||
```
|
```
|
||||||
|
|
||||||
变量 ok 将返回 true 或者 false,我们也可以使用 `MatchString`:
|
变量 ok 将返回 true 或者 false,我们也可以使用 `MatchString`:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
ok, _ := regexp.MatchString(pat, searchIn)
|
ok, _ := regexp.MatchString(pat, searchIn)
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
# 9.3 锁和 sync 包
|
# 9.3 锁和 sync 包
|
||||||
|
|
||||||
在一些复杂的程序中,通常通过不同线程执行不同应用来实现程序的并发。当不同线程要使用同一个变量时,经常会出现一个问题:无法预知变量被不同线程修改的顺序!(这通常被称为资源竞争,指不同线程对同一变量使用的竞争)显然这无法让人容忍,那我们该如何解决这个问题呢?
|
在一些复杂的程序中,通常通过不同线程执行不同应用来实现程序的并发。当不同线程要使用同一个变量时,经常会出现一个问题:无法预知变量被不同线程修改的顺序!(这通常被称为资源竞争,指不同线程对同一变量使用的竞争)显然这无法让人容忍,那我们该如何解决这个问题呢?
|
||||||
|
|
||||||
经典的做法是一次只能让一个线程对共享变量进行操作。当变量被一个线程改变时(临界区),我们为它上锁,直到这个线程执行完成并解锁后,其他线程才能访问它。
|
经典的做法是一次只能让一个线程对共享变量进行操作。当变量被一个线程改变时(临界区),我们为它上锁,直到这个线程执行完成并解锁后,其他线程才能访问它。
|
||||||
|
|
||||||
特别是我们之前章节学习的 map 类型是不存在锁的机制来实现这种效果(出于对性能的考虑),所以 map 类型是非线程安全的。当并行访问一个共享的 map 类型的数据,map 数据将会出错。
|
特别是我们之前章节学习的 map 类型是不存在锁的机制来实现这种效果(出于对性能的考虑),所以 map 类型是非线程安全的。当并行访问一个共享的 map 类型的数据,map 数据将会出错。
|
||||||
|
|
||||||
在 Go 语言中这种锁的机制是通过 sync 包中 Mutex 来实现的。sync 来源于 "synchronized" 一词,这意味着线程将有序的对同一变量进行访问。
|
在 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 节中对这两种方式进行比较。
|
相对简单的情况下,通过使用 sync 包可以解决同一时间只能一个线程访问变量或 map 类型数据的问题。如果这种方式导致程序明显变慢或者引起其他问题,我们要重新思考来通过 goroutines 和 channels 来解决问题,这是在 Go 语言中所提倡用来实现并发的技术。我们将在第 14 章对其深入了解,并在第 14.7 节中对这两种方式进行比较。
|
||||||
|
|
||||||
|
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
包是 Go 语言中代码组织和代码编译的主要方式。关于它们的很多基本信息已经在 4.2 章节中给出,最引人注目的便是可见性。现在我们来看看具体如何来使用自己写的包。在下一节,我们将回顾一些标准库中的包,自定义的包和标准库以外的包。
|
包是 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):
|
示例 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。
|
主程序利用的包必须在主程序编写之前被编译。主程序中每个 pack1 项目都要通过包名来使用:`pack1.Item`。具体使用方法请参见示例 4.6 和 4.7。
|
||||||
|
|
||||||
因此,按照惯例,子目录和包之间有着密切的联系:为了区分,不同包存放在不同的目录下,每个包(所有属于这个包中的 go 文件)都存放在和包名相同的子目录下:
|
因此,按照惯例,子目录和包之间有着密切的联系:为了区分,不同包存放在不同的目录下,每个包(所有属于这个包中的 go 文件)都存放在和包名相同的子目录下:
|
||||||
|
|
||||||
Import with `.` :
|
Import with `.` :
|
||||||
|
|
||||||
import . "./pack1"
|
import . "./pack1"
|
||||||
|
|
||||||
当使用`.`来做为包的别名时,你可以不通过包名来使用其中的项目。例如:`test := ReturnStr()`。
|
当使用 `.` 作为包的别名时,你可以不通过包名来使用其中的项目。例如:`test := ReturnStr()`。
|
||||||
|
|
||||||
在当前的命名空间导入 pack1 包,一般是为了具有更好的测试效果。
|
在当前的命名空间导入 pack1 包,一般是为了具有更好的测试效果。
|
||||||
|
|
||||||
@@ -133,11 +133,11 @@ init 函数是不能被调用的。
|
|||||||
|
|
||||||
通过 `chmod 777 ./Makefile`确保它的可执行性。
|
通过 `chmod 777 ./Makefile`确保它的可执行性。
|
||||||
|
|
||||||
上面脚本内的include语引入了相应的功能,将自动检测机器的架构并调用正确的编译器和链接器。
|
上面脚本内的 include 语句引入了相应的功能,将自动检测机器的架构并调用正确的编译器和链接器。
|
||||||
|
|
||||||
然后终端执行 make 或 `gomake` 工具:他们都会生成一个包含静态库 pack1.a 的 _obj 目录。
|
然后终端执行 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 章我们遇到使用测试工具进行测试的时候我们将重新回到自己的包的制作和编译这个话题。
|
当第 13 章我们遇到使用测试工具进行测试的时候我们将重新回到自己的包的制作和编译这个话题。
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@ b)一个源文件是否能包含多个包?
|
|||||||
|
|
||||||
在同一个包中创建一个 `IsAM` 函数返回一个布尔值用来判断当前时间是 AM 还是 PM,同样创建 `IsAfternoon` 和 `IsEvening` 函数。
|
在同一个包中创建一个 `IsAM` 函数返回一个布尔值用来判断当前时间是 AM 还是 PM,同样创建 `IsAfternoon` 和 `IsEvening` 函数。
|
||||||
|
|
||||||
使用 main_greetings 作出合适的问候(提示:使用 time 包)。
|
使用 main_greetings 作出合适的问候(提示:使用 time 包)。
|
||||||
|
|
||||||
**练习 9.4** 创建一个程序 main_oddven.go 判断前 100 个整数是不是偶数,将判断所用的函数编写在 even 包里。
|
**练习 9.4** 创建一个程序 main_oddven.go 判断前 100 个整数是不是偶数,将判断所用的函数编写在 even 包里。
|
||||||
|
|
||||||
|
@@ -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))。
|
假设我们要安装一个有趣的包 tideland(它包含了许多帮助示例,参见 [项目主页](http://code.google.com/p/tideland-cgl))。
|
||||||
|
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
## 9.8.1 自定义包的目录结构
|
## 9.8.1 自定义包的目录结构
|
||||||
|
|
||||||
下面的结构给了你一个好的示范(uc 代表通用包名, 名字为粗体的代表目录,斜体代表可执行文件):
|
下面的结构给了你一个好的示范(uc 代表通用包名, 名字为粗体的代表目录,斜体代表可执行文件):
|
||||||
|
|
||||||
/home/user/goprograms
|
/home/user/goprograms
|
||||||
ucmain.go (uc 包主程序)
|
ucmain.go (uc 包主程序)
|
||||||
@@ -90,7 +90,7 @@ include $(GOROOT)/src/Make.pkg
|
|||||||
|
|
||||||
在第 13.8 节我们将给出另外一个测试例子并进行深入研究。
|
在第 13.8 节我们将给出另外一个测试例子并进行深入研究。
|
||||||
|
|
||||||
备注:有可能你当前的用户不具有足够的资格使用 go install(没有权限)。这种情况下,选择 root 用户 su。确保 Go 环境变量和 Go 源码路径也设置给 su,同样也适用你的普通用户(详见第 2.3 节)。
|
备注:有可能你当前的用户不具有足够的资格使用 go install(没有权限)。这种情况下,选择 root 用户 su。确保 Go 环境变量和 Go 源码路径也设置给 su,同样也适用你的普通用户(详见第 2.3 节)。
|
||||||
|
|
||||||
接下来我们创建主程序 ucmain.go:
|
接下来我们创建主程序 ucmain.go:
|
||||||
|
|
||||||
|
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
如果您还没有账号,可以去注册一个开源项目的免费帐号。输入正确的帐号密码和有效的邮箱地址并进一步创建用户。然后你将获得一个 Git 命令的列表。本地仓库的操作命令已经完成。一个优秀的系统在你遇到任何问题的时候将 [引导你](http://help.github.com/)。
|
如果您还没有账号,可以去注册一个开源项目的免费帐号。输入正确的帐号密码和有效的邮箱地址并进一步创建用户。然后你将获得一个 Git 命令的列表。本地仓库的操作命令已经完成。一个优秀的系统在你遇到任何问题的时候将 [引导你](http://help.github.com/)。
|
||||||
|
|
||||||
在云端创建一个新的 uc 仓库;发布的指令为(`NNNN` 替代用户名):
|
在云端创建一个新的 uc 仓库;发布的指令为(`NNNN` 替代用户名):
|
||||||
|
|
||||||
```
|
```
|
||||||
git remote add origin git@github.com:NNNN/uc.git
|
git remote add origin git@github.com:NNNN/uc.git
|
||||||
@@ -49,7 +49,7 @@ Gomake(和 go install)将通过 `$GOPATH` 下的本地版本进行工作。
|
|||||||
- Google Code(hg/Git/svn)
|
- Google Code(hg/Git/svn)
|
||||||
- Launchpad(bzr)
|
- Launchpad(bzr)
|
||||||
|
|
||||||
版本控制系统可以选择你熟悉的或者本地使用的代码版本控制。Go 核心代码的仓库是使用 Mercurial(hg) 来控制的,所以它是一个最可能保证你可以得到开发者项目中最好的软件。Git 也很出名,同样也适用。如果你从未使用过版本控制,这些网站有一些很好的帮助并且你可以通过在谷歌搜索 "{name} tutorial",(name为你想要使用的版本控制系统),得到许多很好的教程。
|
版本控制系统可以选择你熟悉的或者本地使用的代码版本控制。Go 核心代码的仓库是使用 Mercurial(hg) 来控制的,所以它是一个最可能保证你可以得到开发者项目中最好的软件。Git 也很出名,同样也适用。如果你从未使用过版本控制,这些网站有一些很好的帮助并且你可以通过在谷歌搜索 "{name} tutorial"(name为你想要使用的版本控制系统)得到许多很好的教程。
|
||||||
|
|
||||||
## 链接
|
## 链接
|
||||||
|
|
||||||
|
@@ -74,7 +74,7 @@ new 和 make 这两个内置函数已经在第 [7.2.4](07.2.md) 节通过切片
|
|||||||
|
|
||||||
下面的例子说明了在映射上使用 new 和 make 的区别以及可能发生的错误:
|
下面的例子说明了在映射上使用 new 和 make 的区别以及可能发生的错误:
|
||||||
|
|
||||||
示例 10.4 new_make.go(不能编译)
|
示例 10.4 [new_make.go](examples/chapter_10/new_make.go)(不能编译)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
|
@@ -41,7 +41,7 @@ func (_ receiver_type) methodName(parameter_list) (return_value_list) { ... }
|
|||||||
|
|
||||||
`recv` 就像是面向对象语言中的 `this` 或 `self`,但是 Go 中并没有这两个关键字。随个人喜好,你可以使用 `this` 或 `self` 作为 receiver 的名字。下面是一个结构体上的简单方法的例子:
|
`recv` 就像是面向对象语言中的 `this` 或 `self`,但是 Go 中并没有这两个关键字。随个人喜好,你可以使用 `this` 或 `self` 作为 receiver 的名字。下面是一个结构体上的简单方法的例子:
|
||||||
|
|
||||||
示例 10.10 method .go:
|
示例 10.10 [method1 .go](examples/chapter_10/method1.go):
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
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
|
```go
|
||||||
package main
|
package main
|
||||||
@@ -143,7 +143,7 @@ func (t time.Time) first3Chars() string {
|
|||||||
|
|
||||||
但是有一个间接的方式:可以先定义该类型(比如:int 或 float)的别名类型,然后再为别名类型定义方法。或者像下面这样将它作为匿名类型嵌入在一个新的结构体中。当然方法只在这个别名类型上有效。
|
但是有一个间接的方式:可以先定义该类型(比如:int 或 float)的别名类型,然后再为别名类型定义方法。或者像下面这样将它作为匿名类型嵌入在一个新的结构体中。当然方法只在这个别名类型上有效。
|
||||||
|
|
||||||
示例 10.12 method_on_time.go:
|
示例 10.12 [method_on_time.go](examples/chapter_10/method_on_time.go):
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
@@ -200,7 +200,7 @@ 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
|
```go
|
||||||
package main
|
package main
|
||||||
@@ -251,7 +251,7 @@ func (p Point3) Abs() float64 {
|
|||||||
|
|
||||||
可以使用 `p3.Abs()` 来替代 `(*p3).Abs()`。
|
可以使用 `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()`。
|
因此 `two2.AddThem` 可以替代 `(&two2).AddThem()`。
|
||||||
|
|
||||||
@@ -263,7 +263,7 @@ func (p Point3) Abs() float64 {
|
|||||||
|
|
||||||
**指针方法和值方法都可以在指针或非指针上被调用**,如下面程序所示,类型 `List` 在值上有一个方法 `Len()`,在指针上有一个方法 `Append()`,但是可以看到两个方法都可以在两种类型的变量上被调用。
|
**指针方法和值方法都可以在指针或非指针上被调用**,如下面程序所示,类型 `List` 在值上有一个方法 `Len()`,在指针上有一个方法 `Append()`,但是可以看到两个方法都可以在两种类型的变量上被调用。
|
||||||
|
|
||||||
示例 10.14 methodset1.go:
|
示例 10.14 [methodset1.go](examples/chapter_10/methodset1.go):
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
@@ -296,7 +296,7 @@ func main() {
|
|||||||
|
|
||||||
这可以通过面向对象语言一个众所周知的技术来完成:提供 getter 和 setter 方法。对于 setter 方法使用 Set 前缀,对于 getter 方法只使用成员名。
|
这可以通过面向对象语言一个众所周知的技术来完成:提供 getter 和 setter 方法。对于 setter 方法使用 Set 前缀,对于 getter 方法只使用成员名。
|
||||||
|
|
||||||
示例 10.15 person2.go:
|
示例 10.15 [person2.go](examples/chapter_10/person2.go):
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package person
|
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
|
```go
|
||||||
package main
|
package main
|
||||||
@@ -368,7 +368,7 @@ func (c *Car) GoToWorkIn() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
下面是 `method3.go` 的完整例子,它展示了内嵌结构体上的方法可以直接在外层类型的实例上调用:
|
下面是 [method3.go](examples/chapter_10/method3.go) 的完整例子,它展示了内嵌结构体上的方法可以直接在外层类型的实例上调用:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
@@ -397,11 +397,11 @@ func main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
内嵌将一个已存在类型的字段和方法注入到了另一个类型里:匿名字段上的方法“晋升”成为了外层类型的方法。当然类型可以有只作用于本身实例而不作用于内嵌“父”类型上的方法,
|
内嵌将一个已存在类型的字段和方法注入到了另一个类型里:匿名字段上的方法“晋升”成为了外层类型的方法。当然类型可以有只作用于本身实例而不作用于内嵌“父”类型上的方法。
|
||||||
|
|
||||||
可以覆写方法(像字段一样):和内嵌类型方法具有同样名字的外层类型的方法会覆写内嵌类型对应的方法。
|
可以覆写方法(像字段一样):和内嵌类型方法具有同样名字的外层类型的方法会覆写内嵌类型对应的方法。
|
||||||
|
|
||||||
在示例 10.18 method4.go 中添加:
|
在示例 10.18 [method4.go](examples/chapter_10/method4.go) 中添加:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func (n *NamedPoint) Abs() float64 {
|
func (n *NamedPoint) Abs() float64 {
|
||||||
@@ -435,7 +435,7 @@ B:内嵌:内嵌(匿名地)所需功能类型,像前一节 10.6.5 所
|
|||||||
|
|
||||||
方式 A 可以通过如下方法实现(使用了第 10.7 节中的 `String()` 功能):
|
方式 A 可以通过如下方法实现(使用了第 10.7 节中的 `String()` 功能):
|
||||||
|
|
||||||
示例 10.19 embed_func1.go:
|
示例 10.19 [embed_func1.go](examples/chapter_10/embed_func1.go):
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
@@ -485,7 +485,7 @@ func (c *Customer) Log() *Log {
|
|||||||
1 - Yes we can!
|
1 - Yes we can!
|
||||||
2 - After me the world will be a better place!
|
2 - After me the world will be a better place!
|
||||||
|
|
||||||
相对的方式 B 可能会像这样:
|
相对的方式 B 可能会像这样([embed_func2.go](examples/chapter_10/embed_func2.go)):
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
@@ -541,7 +541,7 @@ func (c *Customer) String() string {
|
|||||||
|
|
||||||
作为一个例子,假设有一个类型 `CameraPhone`,通过它可以 `Call()`,也可以 `TakeAPicture()`,但是第一个方法属于类型 `Phone`,第二个方法属于类型 `Camera`。
|
作为一个例子,假设有一个类型 `CameraPhone`,通过它可以 `Call()`,也可以 `TakeAPicture()`,但是第一个方法属于类型 `Phone`,第二个方法属于类型 `Camera`。
|
||||||
|
|
||||||
只要嵌入这两个类型就可以解决这个问题,如下所示:
|
只要嵌入这两个类型就可以解决这个问题,如下所示([mult_inheritance.go](examples/chapter_10/mult_inheritance.go)):
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
我们使用第 10.4 节中程序的类型来进行测试:
|
我们使用第 10.4 节中程序的类型来进行测试:
|
||||||
|
|
||||||
示例 10.22 method_string.go:
|
示例 10.22 [method_string.go](examples/chapter_10/method_string.go):
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
|
@@ -14,7 +14,7 @@ Go 中的接口跟 Java/C# 类似:都是必须提供一个指定方法集的
|
|||||||
|
|
||||||
类似于 Python 和 Ruby 这类动态语言中的 `动态类型(duck typing)`;这意味着对象可以根据提供的方法被处理(例如,作为参数传递给函数),而忽略它们的实际类型:它们能做什么比它们是什么更重要。
|
类似于 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):
|
示例 11.16 [duck_dance.go](examples/chapter_11/duck_dance.go):
|
||||||
|
|
||||||
@@ -119,7 +119,8 @@ Go 的接口提高了代码的分离度,改善了代码的复用性,使得
|
|||||||
`提取接口` 是非常有用的设计模式,可以减少需要的类型和方法数量,而且不需要像传统的基于类的面向对象语言那样维护整个的类层次结构。
|
`提取接口` 是非常有用的设计模式,可以减少需要的类型和方法数量,而且不需要像传统的基于类的面向对象语言那样维护整个的类层次结构。
|
||||||
|
|
||||||
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):
|
示例 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)
|
fmt.Printf(format string, a ...interface{}) (n int, errno error)
|
||||||
```
|
```
|
||||||
|
|
||||||
这个函数通过枚举 `slice` 类型的实参动态确定所有参数的类型。并查看每个类型是否实现了 `String()` 方法,如果是就用于产生输出信息。我们可以回到 11.10 节查看这些细节。
|
这个函数通过枚举 `slice` 类型的实参动态确定所有参数的类型,并查看每个类型是否实现了 `String()` 方法,如果是就用于产生输出信息。我们可以回到 11.10 节查看这些细节。
|
||||||
|
|
||||||
## 11.12.6 接口的继承
|
## 11.12.6 接口的继承
|
||||||
|
|
||||||
|
@@ -64,7 +64,7 @@ func classifier(items ...interface{}) {
|
|||||||
|
|
||||||
在处理来自于外部的、类型未知的数据时,比如解析诸如 JSON 或 XML 编码的数据,类型测试和转换会非常有用。
|
在处理来自于外部的、类型未知的数据时,比如解析诸如 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:
|
**练习 11.4** simple_interface2.go:
|
||||||
|
|
||||||
|
@@ -68,7 +68,7 @@ func main() {
|
|||||||
|
|
||||||
**总结**
|
**总结**
|
||||||
|
|
||||||
在接口上调用方法时,必须有和方法定义时相同的接收者类型或者是可以从具体类型 `P` 直接可以辨识的:
|
在接口上调用方法时,必须有和方法定义时相同的接收者类型或者是可以根据具体类型 `P` 直接辨识的:
|
||||||
|
|
||||||
- 指针方法可以通过指针调用
|
- 指针方法可以通过指针调用
|
||||||
- 值方法可以通过值调用
|
- 值方法可以通过值调用
|
||||||
|
@@ -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章 我们会看到很多在实战中使用它们的例子。
|
`io` 包里的 `Readers` 和 `Writers` 都是不带缓冲的,`bufio` 包里提供了对应的带缓冲的操作,在读写 `UTF-8` 编码的文本文件时它们尤其有用。在 第12章 我们会看到很多在实战中使用它们的例子。
|
||||||
|
|
||||||
|
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
如同 json 包一样,也有 `Marshal()` 和 `UnMarshal()` 从 XML 中编码和解码数据;但这个更通用,可以从文件中读取和写入(或者任何实现了 io.Reader 和 io.Writer 接口的类型)
|
如同 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 数据内容。下面的例子说明如何使用解析器:
|
encoding/xml 包实现了一个简单的 XML 解析器(SAX),用来解析 XML 数据内容。下面的例子说明如何使用解析器:
|
||||||
|
|
||||||
|
@@ -160,7 +160,7 @@ filename := filepath.Base(path)
|
|||||||
```
|
```
|
||||||
每行的第一个字段为 title,第二个字段为 price,第三个字段为 quantity。内容的格式基本与 示例 12.3c 的相同,除了分隔符改成了分号。请读取出文件的内容,创建一个结构用于存取一行的数据,然后使用结构的切片,并把数据打印出来。
|
每行的第一个字段为 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`包:读取压缩文件
|
## 12.2.2 `compress`包:读取压缩文件
|
||||||
|
|
||||||
|
@@ -35,7 +35,7 @@ func main() {
|
|||||||
|
|
||||||
**练习 12.5**:[hello_who.go](exercises/chapter_12/hello_who.go)
|
**练习 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 包
|
## 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
|
```go
|
||||||
package main
|
package main
|
||||||
|
@@ -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):
|
示例 12.14 [cat2.go](examples/chapter_12/cat2.go):
|
||||||
|
|
||||||
|
@@ -37,7 +37,8 @@ hello world! - buffered
|
|||||||
```go
|
```go
|
||||||
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
|
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
|
||||||
```
|
```
|
||||||
其不是写入一个文件,而是写入一个 `io.Writer` 接口类型的变量,下面是 `Writer` 接口在 io 包中的定义:
|
|
||||||
|
不是写入一个文件,而是写入一个 `io.Writer` 接口类型的变量,下面是 `Writer` 接口在 io 包中的定义:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type Writer interface {
|
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 方法:
|
bufio.Writer 实现了 Write 方法:
|
||||||
|
|
||||||
@@ -53,13 +54,13 @@ bufio.Writer 实现了 Write 方法:
|
|||||||
func (b *Writer) Write(p []byte) (nn int, err error)
|
func (b *Writer) Write(p []byte) (nn int, err error)
|
||||||
```
|
```
|
||||||
|
|
||||||
它还有一个工厂函数:传给它一个 `io.Writer` 类型的参数,它会返回一个带缓冲的 `bufio.Writer` 类型的 `io.Writer`:
|
它还有一个工厂函数:传给它一个 `io.Writer` 类型的参数,它会返回一个带缓冲的 `bufio.Writer` 类型的 `io.Writer` :
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func NewWriter(wr io.Writer) (b *Writer)
|
func NewWriter(wr io.Writer) (b *Writer)
|
||||||
```
|
```
|
||||||
|
|
||||||
其适合任何形式的缓冲写入。
|
适合任何形式的缓冲写入。
|
||||||
|
|
||||||
在缓冲写入的最后千万不要忘了使用 `Flush()`,否则最后的输出不会被写入。
|
在缓冲写入的最后千万不要忘了使用 `Flush()`,否则最后的输出不会被写入。
|
||||||
|
|
||||||
@@ -107,4 +108,4 @@ func main() {
|
|||||||
|
|
||||||
- [目录](directory.md)
|
- [目录](directory.md)
|
||||||
- 上一节:[用 defer 关闭文件](12.7.md)
|
- 上一节:[用 defer 关闭文件](12.7.md)
|
||||||
- 下一节:[格式化 Json 数据](12.9.md)
|
- 下一节:[格式化 JSON 数据](12.9.md)
|
||||||
|
@@ -109,7 +109,7 @@ JSON 与 Go 类型对应如下:
|
|||||||
- string 对应 JSON 的 string
|
- string 对应 JSON 的 string
|
||||||
- nil 对应 JSON 的 null
|
- nil 对应 JSON 的 null
|
||||||
|
|
||||||
不是所有的数据都可以编码为 JSON 类型:只有验证通过的数据结构才能被编码:
|
不是所有的数据都可以编码为 JSON 类型,只有验证通过的数据结构才能被编码:
|
||||||
|
|
||||||
- JSON 对象只支持字符串类型的 key;要编码一个 Go map 类型,map 必须是 map[string]T(T是 `json` 包中支持的任何类型)
|
- JSON 对象只支持字符串类型的 key;要编码一个 Go map 类型,map 必须是 map[string]T(T是 `json` 包中支持的任何类型)
|
||||||
- Channel,复杂类型和函数类型不能被编码
|
- Channel,复杂类型和函数类型不能被编码
|
||||||
@@ -124,7 +124,7 @@ JSON 与 Go 类型对应如下:
|
|||||||
|
|
||||||
虽然反射能够让 JSON 字段去尝试匹配目标结构字段;但是只有真正匹配上的字段才会填充数据。字段没有匹配不会报错,而是直接忽略掉。
|
虽然反射能够让 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
|
||||||
|
|
||||||
### 解码任意的数据:
|
### 解码任意的数据:
|
||||||
|
|
||||||
|
@@ -12,7 +12,7 @@ type error interface {
|
|||||||
|
|
||||||
## 13.1.1 定义错误
|
## 13.1.1 定义错误
|
||||||
|
|
||||||
任何时候当你需要一个新的错误类型,都可以用 `errors`(必须先 import)包的 `errors.New` 函数接收合适的错误信息来创建,像下面这样:
|
任何时候当你需要一个新的错误类型,都可以用 `errors` 包(必须先 import)的 `errors.New` 函数接收合适的错误信息来创建,像下面这样:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
err := errors.New("math - square root of negative number")
|
err := errors.New("math - square root of negative number")
|
||||||
|
@@ -39,7 +39,7 @@ 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```
|
现在用这个 flag 运行程序:```progexec -cpuprofile=progexec.prof```
|
||||||
|
|
||||||
|
@@ -26,7 +26,7 @@ func protect(g func()) {
|
|||||||
|
|
||||||
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 怎么结合使用的完整例子:
|
这是一个展示 panic,defer 和 recover 怎么结合使用的完整例子:
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ func test() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
badCall()
|
badCall()
|
||||||
fmt.Printf("After bad call\r\n") // <-- wordt niet bereikt
|
fmt.Printf("After bad call\r\n") // <-- would not reach
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -71,7 +71,7 @@ Test completed
|
|||||||
|
|
||||||
`defer-panic-recover` 在某种意义上也是一种像 `if`,`for` 这样的控制流机制。
|
`defer-panic-recover` 在某种意义上也是一种像 `if`,`for` 这样的控制流机制。
|
||||||
|
|
||||||
Go 标准库中许多地方都用了这个机制,例如,json 包中的解码和 regexp 包中的 Complie 函数。Go 库的原则是即使在包的内部使用了 panic,在它的对外接口(API)中也必须用 recover 处理成返回显式的错误。
|
Go 标准库中许多地方都用了这个机制,例如,json 包中的解码和 regexp 包中的 Complie 函数。Go 库的原则是即使在包的内部使用了 panic,在它的对外接口(API)中也必须用 recover 处理成显式返回的错误。
|
||||||
|
|
||||||
## 链接
|
## 链接
|
||||||
|
|
||||||
|
@@ -62,8 +62,6 @@ func f1(a type1, b type2) {
|
|||||||
|
|
||||||
我们会在 [15.5 节](15.5.md) 的 web 应用中使用这种模式。
|
我们会在 [15.5 节](15.5.md) 的 web 应用中使用这种模式。
|
||||||
|
|
||||||
<u>练习</u>
|
|
||||||
|
|
||||||
**练习 13.1**:[recover_dividebyzero.go](exercises/chapter_13/recover_divbyzero.go)
|
**练习 13.1**:[recover_dividebyzero.go](exercises/chapter_13/recover_divbyzero.go)
|
||||||
|
|
||||||
用示例 13.3 中的编码模式通过整数除以 0 触发一个运行时 panic。
|
用示例 13.3 中的编码模式通过整数除以 0 触发一个运行时 panic。
|
||||||
|
@@ -20,7 +20,7 @@
|
|||||||
func TestAbcde(t *testing.T)
|
func TestAbcde(t *testing.T)
|
||||||
```
|
```
|
||||||
|
|
||||||
T 是传给测试函数的结构类型,用来管理测试状态,支持格式化测试日志,如 t.Log,t.Error,t.ErrorF 等。在函数的结尾把输出跟想要的结果对比,如果不等就打印一个错误。成功的测试则直接返回。
|
T 是传给测试函数的结构类型,用来管理测试状态,支持格式化测试日志,如 t.Log,t.Error,t.ErrorF 等。在函数的结尾把输出跟想要的结果对比,如果不等就打印一个错误,成功的测试则直接返回。
|
||||||
|
|
||||||
用下面这些函数来通知测试失败:
|
用下面这些函数来通知测试失败:
|
||||||
|
|
||||||
|
@@ -119,7 +119,7 @@ FAIL
|
|||||||
|
|
||||||
为练习 7.14 [string_reverse.go](exercises/chapter_7/string_reverse.go) 写一个单元测试。
|
为练习 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) {
|
func TestFunction(t *testing.T) {
|
||||||
for i, tt := range tests {
|
for i, tt := range tests {
|
||||||
s := FuncToBeTested(tt.in)
|
s := FuncToBeTested(tt.in)
|
||||||
verify(t, i, “FuncToBeTested: “, tt.in, s, tt.out)
|
verify(t, i, "FuncToBeTested: ", tt.in, s, tt.out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
客户端-服务器应用正是 goroutines 和 channels 的亮点所在。
|
客户端-服务器应用正是 goroutines 和 channels 的亮点所在。
|
||||||
|
|
||||||
客户端(Client)可以是运行在任意设备上的任意程序,它会按需发送请求(request)至服务器。服务器(Server)接收到这个请求后开始相应的工作,然后再将响应(response)返回给客户端。典型情况下一般是多个客户端(即多个请求)对应一个(或少量)服务器。例如我们日常使用的浏览器客户端,其功能就是向服务器请求网页。而Web服务器则会向浏览器响应网页数据。
|
客户端(Client)可以是运行在任意设备上的任意程序,它会按需发送请求(request)至服务器。服务器(Server)接收到这个请求后开始相应的工作,然后再将响应(response)返回给客户端。典型情况下一般是多个客户端(即多个请求)对应一个(或少量)服务器。例如我们日常使用的浏览器客户端,其功能就是向服务器请求网页。而 Web 服务器则会向浏览器响应网页数据。
|
||||||
|
|
||||||
使用 Go 的服务器通常会在协程中执行向客户端的响应,故而会对每一个客户端请求启动一个协程。一个常用的操作方法是客户端请求自身中包含一个通道,而服务器则向这个通道发送响应。
|
使用 Go 的服务器通常会在协程中执行向客户端的响应,故而会对每一个客户端请求启动一个协程。一个常用的操作方法是客户端请求自身中包含一个通道,而服务器则向这个通道发送响应。
|
||||||
|
|
||||||
@@ -95,7 +95,8 @@ func main() {
|
|||||||
|
|
||||||
这个程序仅启动了 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
|
```go
|
||||||
package main
|
package main
|
||||||
|
|
||||||
@@ -180,13 +181,15 @@ func server(op binOp, service chan *request, quit chan bool) {
|
|||||||
|
|
||||||
在 `main` 函数中我们做出如下更改:
|
在 `main` 函数中我们做出如下更改:
|
||||||
|
|
||||||
|
```go
|
||||||
adder, quit := startServer(func(a, b int) int { return a + b })
|
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
|
```go
|
||||||
package main
|
package main
|
||||||
|
|
||||||
@@ -244,7 +247,9 @@ func main() {
|
|||||||
fmt.Println("done")
|
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
|
```go
|
||||||
req1 := &Request{3, 4, make(chan int)}
|
req1 := &Request{3, 4, make(chan int)}
|
||||||
req2 := &Request{150, 250, make(chan int)}
|
req2 := &Request{150, 250, make(chan int)}
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
使用带缓冲区的通道很容易实现这一点(参见 [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
|
```go
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
这个程序同时也展示了如何通过 `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
|
```go
|
||||||
package main
|
package main
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
right <- 0 // bang!
|
right <- 0 // bang!
|
||||||
x := <-leftmost // wait for completion
|
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(无缓存信道具有同步阻塞的特性)*
|
||||||
|
@@ -26,8 +26,8 @@ func main() {
|
|||||||
runtime.GOMAXPROCS(NCPU) // runtime.GOMAXPROCS = NCPU
|
runtime.GOMAXPROCS(NCPU) // runtime.GOMAXPROCS = NCPU
|
||||||
DoAll()
|
DoAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- `DoAll()` 函数创建了一个 `sem` 通道,每个并行计算都将在对其发送完成信号;在一个 for 循环中 `NCPU` 个协程被启动了,每个协程会承担 `1/NCPU` 的工作量。每一个 `DoPart()` 协程都会向 `sem` 通道发送完成信号。
|
- `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()` 会在 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))
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
假设我们需要处理一些数量巨大且互不相关的数据项,它们从一个 `in` 通道被传递进来,当我们处理完以后又要将它们放入另一个 `out` 通道,就像一个工厂流水线一样。处理每个数据项也可能包含许多步骤:Preprocess(预处理) / StepA(步骤A) / StepB(步骤B) / ... / PostProcess(后处理)
|
假设我们需要处理一些数量巨大且互不相关的数据项,它们从一个 `in` 通道被传递进来,当我们处理完以后又要将它们放入另一个 `out` 通道,就像一个工厂流水线一样。处理每个数据项也可能包含许多步骤:Preprocess(预处理) / StepA(步骤A) / StepB(步骤B) / ... / PostProcess(后处理)
|
||||||
|
|
||||||
一个典型的用于解决按顺序执行每个步骤的顺序流水线算法可以写成下面这样:
|
一个典型的用于解决按顺序执行每个步骤的顺序流水线算法可以写成下面这样:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func SerialProcessData(in <-chan *Data, out chan<- *Data) {
|
func SerialProcessData(in <-chan *Data, out chan<- *Data) {
|
||||||
for data := range in {
|
for data := range in {
|
||||||
@@ -19,6 +20,7 @@ func SerialProcessData(in <-chan *Data, out chan<- *Data) {
|
|||||||
如果你仔细想想,你很快就会发现这将会造成巨大的时间浪费。
|
如果你仔细想想,你很快就会发现这将会造成巨大的时间浪费。
|
||||||
|
|
||||||
一个更高效的计算方式是让每一个处理步骤作为一个协程独立工作。每一个步骤从上一步的输出通道中获得输入数据。这种方式仅有极少数时间会被浪费,而大部分时间所有的步骤都在一直执行中:
|
一个更高效的计算方式是让每一个处理步骤作为一个协程独立工作。每一个步骤从上一步的输出通道中获得输入数据。这种方式仅有极少数时间会被浪费,而大部分时间所有的步骤都在一直执行中:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func ParallelProcessData (in <-chan *Data, out chan<- *Data) {
|
func ParallelProcessData (in <-chan *Data, out chan<- *Data) {
|
||||||
// make channels:
|
// make channels:
|
||||||
@@ -34,6 +36,7 @@ func ParallelProcessData (in <-chan *Data, out chan<- *Data) {
|
|||||||
go PostProcessData(StepCOut,out)
|
go PostProcessData(StepCOut,out)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
通道的缓冲区大小可以用来进一步优化整个过程。
|
通道的缓冲区大小可以用来进一步优化整个过程。
|
||||||
|
|
||||||
|
|
||||||
|
@@ -4,9 +4,11 @@
|
|||||||
|
|
||||||
考虑以下的客户端-服务器结构:客户端协程执行一个无限循环从某个源头(也许是网络)接收数据;数据读取到 `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)
|
var serverChan = make(chan *Buffer)
|
||||||
|
```
|
||||||
|
|
||||||
以下是客户端的算法代码:
|
以下是客户端的算法代码:
|
||||||
|
|
||||||
@@ -27,9 +29,10 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
服务器的循环则接收每一条来自客户端的消息并处理它,之后尝试将缓冲返回给共享的空闲缓冲区:
|
服务器的循环则接收每一条来自客户端的消息并处理它,之后尝试将缓冲返回给共享的空闲缓冲区:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func server() {
|
func server() {
|
||||||
for {
|
for {
|
||||||
@@ -45,6 +48,7 @@ func server() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
但是这种方法在 `freeList` 通道已满的时候是行不通的,因为无法放入空闲 `freeList` 通道的缓冲区会被“丢到地上”由垃圾收集器回收(故名:漏桶算法)
|
但是这种方法在 `freeList` 通道已满的时候是行不通的,因为无法放入空闲 `freeList` 通道的缓冲区会被“丢到地上”由垃圾收集器回收(故名:漏桶算法)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
如果你想排除指定部分的代码或者更具体的指定要测试的部分,可以使用 `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
|
```go
|
||||||
package main
|
package main
|
||||||
@@ -43,7 +43,9 @@ func BenchmarkChannelBuffered(b *testing.B) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
输出:
|
输出:
|
||||||
|
|
||||||
```
|
```
|
||||||
Output:Windows: N Time 1 op Operations per sec
|
Output:Windows: N Time 1 op Operations per sec
|
||||||
sync 1000000 2443 ns/op --> 409 332 / s
|
sync 1000000 2443 ns/op --> 409 332 / s
|
||||||
@@ -57,5 +59,3 @@ func BenchmarkChannelBuffered(b *testing.B) {
|
|||||||
- [目录](directory.md)
|
- [目录](directory.md)
|
||||||
- 上一节:[漏桶算法](14.15.md)
|
- 上一节:[漏桶算法](14.15.md)
|
||||||
- 下一节:[使用通道并发访问对象](14.17.md)
|
- 下一节:[使用通道并发访问对象](14.17.md)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
# 14.17 使用通道并发访问对象
|
# 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
|
```go
|
||||||
package main
|
package main
|
||||||
|
|
||||||
@@ -58,7 +58,9 @@ func main() {
|
|||||||
fmt.Println(bs)
|
fmt.Println(bs)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
输出:
|
输出:
|
||||||
|
|
||||||
```
|
```
|
||||||
Person - name is: Smith Bill - salary is: 2500.50
|
Person - name is: Smith Bill - salary is: 2500.50
|
||||||
Salary changed:
|
Salary changed:
|
||||||
|
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
未初始化的通道的值是 nil 。
|
未初始化的通道的值是 nil 。
|
||||||
|
|
||||||
所以通道只能传输一种类型的数据,比如 `chan int` 或者 `chan string`,所有的类型都可以用于通道,空接口 `interface{}` 也可以。甚至可以(有时非常有用)创建通道的通道。
|
所以通道只能传输一种类型的数据,比如 `chan int` 或者 `chan string`,所有的类型都可以用于通道,空接口 `interface{}` 也可以,甚至可以(有时非常有用)创建通道的通道。
|
||||||
|
|
||||||
通道实际上是类型化消息的队列:使数据得以传输。它是先进先出(FIFO)的结构所以可以保证发送给他们的元素的顺序(有些人知道,通道可以比作 Unix shells 中的双向管道(two-way pipe))。通道也是引用类型,所以我们使用 `make()` 函数来给它分配内存。这里先声明了一个字符串通道 ch1,然后创建了它(实例化):
|
通道实际上是类型化消息的队列:使数据得以传输。它是先进先出(FIFO)的结构所以可以保证发送给他们的元素的顺序(有些人知道,通道可以比作 Unix shells 中的双向管道(two-way pipe))。通道也是引用类型,所以我们使用 `make()` 函数来给它分配内存。这里先声明了一个字符串通道 ch1,然后创建了它(实例化):
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ if <- ch != 1000{
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
同一个操作符 `<-` 既用于**发送**也用于**接收**,但Go会根据操作对象弄明白该干什么 。虽非强制要求,但为了可读性通道的命名通常以 `ch` 开头或者包含 `chan`。通道的发送和接收都是原子操作:它们总是互不干扰的完成的。下面的示例展示了通信操作符的使用。
|
同一个操作符 `<-` 既用于**发送**也用于**接收**,但 Go 会根据操作对象弄明白该干什么 。虽非强制要求,但为了可读性通道的命名通常以 `ch` 开头或者包含 `chan` 。通道的发送和接收都是原子操作:它们总是互不干扰地完成。下面的示例展示了通信操作符的使用。
|
||||||
|
|
||||||
示例 14.2-[goroutine2.go](examples/chapter_14/goroutine2.go)
|
示例 14.2-[goroutine2.go](examples/chapter_14/goroutine2.go)
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ Washington Tripoli London Beijing tokyo
|
|||||||
|
|
||||||
尽管这看上去是非常严格的约束,实际在大部分情况下工作的很不错。
|
尽管这看上去是非常严格的约束,实际在大部分情况下工作的很不错。
|
||||||
|
|
||||||
程序 `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)
|
示例 14.3-[channel_block.go](examples/chapter_14/channel_block.go)
|
||||||
|
|
||||||
|
@@ -52,7 +52,7 @@ default:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
在示例程序 14.2 中使用这些可以改进为版本 goroutine3.go,输出相同。
|
在示例程序 14.2 中使用这些可以改进为版本 [goroutine3.go](examples/chapter_14/goroutine3.go),输出相同。
|
||||||
|
|
||||||
实现非阻塞通道的读取,需要使用 select(参见第 [14.4](14.4.md) 节)。
|
实现非阻塞通道的读取,需要使用 select(参见第 [14.4](14.4.md) 节)。
|
||||||
|
|
||||||
|
@@ -91,13 +91,13 @@ Received on channel 1: 94346
|
|||||||
Received on channel 1: 94348
|
Received on channel 1: 94348
|
||||||
```
|
```
|
||||||
|
|
||||||
一秒内的输出非常惊人,如果我们给它计数(goroutine_select2.go),得到了 90000 个左右的数字。
|
一秒内的输出非常惊人,如果我们给它计数([goroutine_select2.go](examples/chapter_14/goroutine_select2.go)),得到了 90000 个左右的数字。
|
||||||
|
|
||||||
## 练习:
|
## 练习:
|
||||||
|
|
||||||
练习 14.7:
|
练习 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)
|
- 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)
|
- 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
|
```go
|
||||||
ticker := time.NewTicker(updateInterval)
|
ticker := time.NewTicker(updateInterval)
|
||||||
|
@@ -18,7 +18,7 @@ type Task struct {
|
|||||||
Tasks []*Task
|
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
|
```go
|
||||||
func Worker(pool *Pool) {
|
func Worker(pool *Pool) {
|
||||||
@@ -34,7 +34,7 @@ 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 得到。加锁实现同步的方式在工作协程比较少时可以工作得很好,但是当工作协程数量很大,任务量也很多时,处理效率将会因为频繁的加锁/解锁开销而降低。当工作协程数增加到一个阈值时,程序效率会急剧下降,这就成为了瓶颈。
|
||||||
|
|
||||||
新模式:使用通道
|
新模式:使用通道
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ 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 数量的增多也会增加通信的开销,这会对性能有轻微的影响。
|
||||||
|
|
||||||
从这个简单的例子中可能很难看出第二种模式的优势,但含有复杂锁运用的程序不仅在编写上显得困难,也不容易编写正确,使用第二种模式的话,就无需考虑这么复杂的东西了。
|
从这个简单的例子中可能很难看出第二种模式的优势,但含有复杂锁运用的程序不仅在编写上显得困难,也不容易编写正确,使用第二种模式的话,就无需考虑这么复杂的东西了。
|
||||||
|
|
||||||
|
@@ -9,7 +9,7 @@
|
|||||||
....
|
....
|
||||||
```
|
```
|
||||||
|
|
||||||
生成器每次返回的是序列中下一个值而非整个序列;这种特性也称之为惰性求值:只在你需要时进行求值,同时保留相关变量资源(内存和cpu):这是一项在需要时对表达式进行求值的技术。例如,生成一个无限数量的偶数序列:要产生这样一个序列并且在一个一个的使用可能会很困难,而且内存会溢出!但是一个含有通道和go协程的函数能轻易实现这个需求。
|
生成器每次返回的是序列中下一个值而非整个序列;这种特性也称之为惰性求值:只在你需要时进行求值,同时保留相关变量资源(内存和 CPU):这是一项在需要时对表达式进行求值的技术。例如,生成一个无限数量的偶数序列:要产生这样一个序列并且在一个一个的使用可能会很困难,而且内存会溢出!但是一个含有通道和 go 协程的函数能轻易实现这个需求。
|
||||||
|
|
||||||
在 14.12 的例子中,我们实现了一个使用 int 型通道来实现的生成器。通道被命名为 `yield` 和 `resume` ,这些词经常在协程代码中使用。
|
在 14.12 的例子中,我们实现了一个使用 int 型通道来实现的生成器。通道被命名为 `yield` 和 `resume` ,这些词经常在协程代码中使用。
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
# 15.0 网络、模板与网页应用
|
# 15.0 网络、模板与网页应用
|
||||||
|
|
||||||
Go 在编写 web 应用方面非常得力。因为目前它还没有GUI(Graphic User Interface 即图形化用户界面)的框架,通过文本或者模板展现的 html 页面是目前 Go 编写界面应用程序的唯一方式。(**译者注:实际上在翻译的时候,已经有了一些不太成熟的GUI库例如:go ui。**)
|
Go 在编写 web 应用方面非常得力。因为目前它还没有GUI(Graphic User Interface 即图形化用户界面)的框架,通过文本或者模板展现的 html 页面是目前 Go 编写界面应用程序的唯一方式。(**译者注:实际上在翻译的时候,已经有了一些不太成熟的 GUI 库,例如:go ui 。**)
|
||||||
|
|
||||||
## 链接
|
## 链接
|
||||||
|
|
||||||
|
@@ -99,7 +99,7 @@ func main() {
|
|||||||
|
|
||||||
如果在服务器没有开始监听的情况下运行客户端程序,客户端会停止并打印出以下错误信息:`对tcp 127.0.0.1:50000发起连接时产生错误:由于目标计算机的积极拒绝而无法创建连接`。
|
如果在服务器没有开始监听的情况下运行客户端程序,客户端会停止并打印出以下错误信息:`对tcp 127.0.0.1:50000发起连接时产生错误:由于目标计算机的积极拒绝而无法创建连接`。
|
||||||
|
|
||||||
打开命令提示符并转到服务器和客户端可执行程序所在的目录,Windows 系统下输入server.exe(或者只输入server),Linux系统下输入./server。
|
打开命令提示符并转到服务器和客户端可执行程序所在的目录,Windows 系统下输入 `server.exe`(或者只输入 `server` ),Linux 系统下输入 `./server` 。
|
||||||
|
|
||||||
接下来控制台出现以下信息:`Starting the server ...`
|
接下来控制台出现以下信息:`Starting the server ...`
|
||||||
|
|
||||||
|
@@ -116,7 +116,7 @@ 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"/>
|
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` 包中的其他重要的函数:
|
||||||
|
|
||||||
|
@@ -137,7 +137,7 @@ func load(title string) (*Page, error) {
|
|||||||
```go
|
```go
|
||||||
templates = make(map[string]*template.Template)
|
templates = make(map[string]*template.Template)
|
||||||
```
|
```
|
||||||
此种技术被称为*模板缓存*,是推荐的最佳实践。
|
这种技术被称为*模板缓存*,是推荐的最佳实践。
|
||||||
|
|
||||||
3. 为了真正从模板和结构体构建出页面,必须使用:
|
3. 为了真正从模板和结构体构建出页面,必须使用:
|
||||||
```go
|
```go
|
||||||
|
@@ -158,7 +158,8 @@ Logger | http://localhost:12345/ (根) | oops
|
|||||||
|
|
||||||
`Logger` 处理函数用 `w.WriteHeader(404)` 来输出 “404 Not Found”头部。
|
`Logger` 处理函数用 `w.WriteHeader(404)` 来输出 “404 Not Found”头部。
|
||||||
|
|
||||||
此项技术通常很有用,无论何时服务器执行代码产生错误,都可以应用类似这样的代码:
|
这项技术通常很有用,无论何时服务器执行代码产生错误,都可以应用类似这样的代码:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(400)
|
w.WriteHeader(400)
|
||||||
@@ -209,6 +210,7 @@ Channel | http://localhost:12345/chan | channel send #1
|
|||||||
Channel | 刷新 | channel send #2
|
Channel | 刷新 | channel send #2
|
||||||
|
|
||||||
每当有新请求到达,通道的 `ServeHTTP` 方法从通道获取下一个整数并显示。由此可见,网页服务器可以从通道中获取要发送的响应,它可以由另一个函数产生(甚至是客户端)。下面的代码片段正是一个这样的处理函数,但会在 30 秒后超时:
|
每当有新请求到达,通道的 `ServeHTTP` 方法从通道获取下一个整数并显示。由此可见,网页服务器可以从通道中获取要发送的响应,它可以由另一个函数产生(甚至是客户端)。下面的代码片段正是一个这样的处理函数,但会在 30 秒后超时:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func ChanResponse(w http.ResponseWriter, req *http.Request) {
|
func ChanResponse(w http.ResponseWriter, req *http.Request) {
|
||||||
timeout := make (chan bool)
|
timeout := make (chan bool)
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
# 16.4 何时使用 new() 和 make()
|
# 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
|
- 切片、映射和通道,使用 make
|
||||||
- 数组、结构体和所有的值类型,使用 new
|
- 数组、结构体和所有的值类型,使用 new
|
||||||
|
@@ -1,14 +1,18 @@
|
|||||||
# 16.5 不需要将一个指向切片的指针传递给函数
|
# 16.5 不需要将一个指向切片的指针传递给函数
|
||||||
|
|
||||||
在[第4.9小节](04.9.md),我们已经知道,切片实际是一个指向潜在数组的指针。我们常常需要把切片作为一个参数传递给函数是因为:实际就是传递一个指向变量的指针,在函数内可以改变这个变量,而不是传递数据的拷贝。
|
在第[4.9小节](04.9.md),我们已经知道,切片实际是一个指向潜在数组的指针。我们常常需要把切片作为一个参数传递给函数是因为:实际就是传递一个指向变量的指针,在函数内可以改变这个变量,而不是传递数据的拷贝。
|
||||||
|
|
||||||
因此应该这样做:
|
因此应该这样做:
|
||||||
|
|
||||||
|
```go
|
||||||
func findBiggest( listOfNumbers []int ) int {}
|
func findBiggest( listOfNumbers []int ) int {}
|
||||||
|
```
|
||||||
|
|
||||||
而不是:
|
而不是:
|
||||||
|
|
||||||
|
```go
|
||||||
func findBiggest( listOfNumbers *[]int ) int {}
|
func findBiggest( listOfNumbers *[]int ) int {}
|
||||||
|
```
|
||||||
|
|
||||||
**当切片作为参数传递时,切记不要解引用切片。**
|
**当切片作为参数传递时,切记不要解引用切片。**
|
||||||
|
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
查看如下程序:`nexter` 是一个接口类型,并且定义了一个 `next()` 方法读取下一字节。函数 `nextFew1` 将 `nexter` 接口作为参数并读取接下来的 `num` 个字节,并返回一个切片:这是正确做法。但是 `nextFew2` 使用一个指向 `nexter` 接口类型的指针作为参数传递给函数:当使用 `next()` 函数时,系统会给出一个编译错误:**n.next undefined (type *nexter has no
|
查看如下程序:`nexter` 是一个接口类型,并且定义了一个 `next()` 方法读取下一字节。函数 `nextFew1` 将 `nexter` 接口作为参数并读取接下来的 `num` 个字节,并返回一个切片:这是正确做法。但是 `nextFew2` 使用一个指向 `nexter` 接口类型的指针作为参数传递给函数:当使用 `next()` 函数时,系统会给出一个编译错误:**n.next undefined (type *nexter has no
|
||||||
field or method next)** (译者注:n.next 未定义(*nexter 类型没有 next 成员或 next 方法))
|
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
|
```go
|
||||||
package main
|
package main
|
||||||
@@ -23,7 +23,7 @@ func nextFew1(n nexter, num int) []byte {
|
|||||||
func nextFew2(n *nexter, num int) []byte {
|
func nextFew2(n *nexter, num int) []byte {
|
||||||
var b []byte
|
var b []byte
|
||||||
for i:=0; i < num; i++ {
|
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
|
return b
|
||||||
}
|
}
|
||||||
|
@@ -13,14 +13,14 @@ import (
|
|||||||
var values = [5]int{10, 11, 12, 13, 14}
|
var values = [5]int{10, 11, 12, 13, 14}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// 版本A:
|
// 版本 A:
|
||||||
for ix := range values { // ix 是索引值
|
for ix := range values { // ix 是索引值
|
||||||
func() {
|
func() {
|
||||||
fmt.Print(ix, " ")
|
fmt.Print(ix, " ")
|
||||||
}() // 调用闭包打印每个索引值
|
}() // 调用闭包打印每个索引值
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
// 版本B: 和A版本类似,但是通过调用闭包作为一个协程
|
// 版本 B:和 A 版本类似,但是通过调用闭包作为一个协程
|
||||||
for ix := range values {
|
for ix := range values {
|
||||||
go func() {
|
go func() {
|
||||||
fmt.Print(ix, " ")
|
fmt.Print(ix, " ")
|
||||||
@@ -28,7 +28,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
time.Sleep(5e9)
|
time.Sleep(5e9)
|
||||||
// 版本C: 正确的处理方式
|
// 版本 C:正确的处理方式
|
||||||
for ix := range values {
|
for ix := range values {
|
||||||
go func(ix interface{}) {
|
go func(ix interface{}) {
|
||||||
fmt.Print(ix, " ")
|
fmt.Print(ix, " ")
|
||||||
@@ -36,7 +36,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
time.Sleep(5e9)
|
time.Sleep(5e9)
|
||||||
// 版本D: 输出值:
|
// 版本 D:输出值:
|
||||||
for ix := range values {
|
for ix := range values {
|
||||||
val := values[ix]
|
val := values[ix]
|
||||||
go func() {
|
go func() {
|
||||||
@@ -45,7 +45,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
time.Sleep(1e9)
|
time.Sleep(1e9)
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
输出:
|
输出:
|
||||||
|
@@ -32,7 +32,7 @@ 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)
|
要实现简洁的错误检测代码,更好的方式是使用闭包,参考[第16.10.2小节](16.10.md)
|
||||||
|
|
||||||
|
@@ -16,7 +16,7 @@ for key, value := range map1 {
|
|||||||
|
|
||||||
`val1, isPresent = map1[key1]`
|
`val1, isPresent = map1[key1]`
|
||||||
|
|
||||||
返回值:键`key1`对应的值或者`0`, `true`或者`false`
|
返回值:键 `key1` 对应的值或者 `0`,`true` 或者 `false`
|
||||||
|
|
||||||
(3)如何在映射中删除一个键:
|
(3)如何在映射中删除一个键:
|
||||||
|
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
你肯定知道有些浏览器中的地址(称为 URL)非常长且/或复杂,在网上有一些将他们转换成简短 URL 来使用的服务。我们的项目与此类似:它是具有 2 个功能的 *web 服务*(web service):
|
你肯定知道有些浏览器中的地址(称为 URL)非常长且/或复杂,在网上有一些将他们转换成简短 URL 来使用的服务。我们的项目与此类似:它是具有 2 个功能的 *web 服务*(web service):
|
||||||
|
|
||||||
## 添加 (Add)
|
## 添加(Add)
|
||||||
|
|
||||||
给定一个较长的 URL,会将其转换成较短的版本,例如:
|
给定一个较长的 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`
|
- (A) 转变为:`http://goto/UrcGq`
|
||||||
- (B) 并保存这对数据
|
- (B) 并保存这对数据
|
||||||
|
|
||||||
## 重定向 (Redirect)
|
## 重定向(Redirect)
|
||||||
|
|
||||||
短网址被请求时,会把用户重定向到原始的长 URL。因此如果你在浏览器输入网址 (B),会被重定向到页面 (A)。
|
短网址被请求时,会把用户重定向到原始的长 URL。因此如果你在浏览器输入网址 (B),会被重定向到页面 (A)。
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user