Go实战
在Go语言中,所有变量都被初始化为其零值。对于数值类型,零值是0;对于字符串类型,零值是空字符串;对于布尔类型,零值是false;对于指针,零值是nil。对于引用类型来说,所引用的底层数据结构会被初始化为对应的零值。但是被声明为其零值的引用类型的变量,会返回nil作为其值。
feeds, err := RetrieveFeeds()
这里可以看到简化变量声明运算符(:=)。这个运算符用于声明一个变量,同时给这个变量赋予初始值。编译器使用函数返回值的类型来确定每个变量的类型。简化变量声明运算符只是一种简化记法,让代码可读性更高。这个运算符声明的变量和其他使用关键字var声明的变量没有任何区别。
results := make(chan *Result)
我们使用内置的make函数创建了一个无缓冲的通道。我们使用简化变量声明运算符,在调用make的同时声明并初始化该通道变量。根据经验,如果需要声明初始值为零值的变量,应该使用var关键字声明变量;如果提供确切的非零值初始化变量或者使用函数返回值创建变量,应该使用简化变量声明运算符。
在Go语言中,通道(channel)和映射(map)与切片(slice)一样,也是引用类型,不过通道本身实现的是一组带类型的值,这组值用于在goroutine之间传递数据。通道内置同步机制,从而保证通信安全。
var waitGroup sync.WaitGroup
waitGroup.Add(len(feeds))
在Go语言中,如果main函数返回,整个程序也就终止了。Go程序终止时,还会关闭所有之前启动且还在运行的goroutine。写并发程序的时候,最佳做法是,在main函数返回前,清理并终止所有之前启动的goroutine。编写启动和终止时的状态都很清晰的程序,有助减少bug,防止资源异常。
这个程序使用sync包的WaitGroup跟踪所有启动的goroutine。非常推荐使用WaitGroup来跟踪goroutine的工作是否完成。WaitGroup是一个计数信号量,我们可以利用它来统计所有的goroutine是不是都完成了工作。
在第23行我们声明了一个sync包里的WaitGroup类型的变量。之后在第27行,我们将WaitGroup变量的值设置为将要启动的goroutine的数量。马上就能看到,我们为每个数据源都启动了一个goroutine来处理数据。每个goroutine完成其工作后,就会递减WaitGroup变量的计数值,当这个值递减到0时,我们就知道所有的工作都做完了。
使用for range迭代切片时,每次迭代会返回两个值。第一个值是迭代的元素在切片里的索引位置,第二个值是元素值的一个副本。
这是第二次看到使用了下划线标识符。第一次是在main.go里导入matchers包的时候。这次,下划线标识符的作用是占位符,占据了保存range调用返回的索引值的变量的位置。如果要调用的函数返回多个值,而又不需要其中的某个值,就可以使用下划线标识符将其忽略。在我们的例子里,我们不需要使用返回的索引值,所以就使用下划线标识符把它忽略掉。
matcher, exists := matchers[feed.Type]
if !exists {
matcher = matchers["default"]
}
我们检查map是否含有符合数据源类型的值。查找map里的键时,有两个选择:要么赋值给一个变量,要么为了精确查找,赋值给两个变量。赋值给两个变量时第一个值和赋值给一个变量时的值一样,是map查找的结果值。如果指定了第二个值,就会返回一个布尔标志,来表示查找的键是否存在于map里。如果这个键不存在,map会返回其值类型的零值作为返回值,如果这个键存在,map会返回键所对应值的副本。
指针变量可以方便地在函数之间共享数据。使用指针变量可以让函数访问并修改一个变量的状态,而这个变量可以在其他函数甚至是其他goroutine的作用域里声明。
在Go语言中,所有的变量都以值的方式传递。因为指针变量的值是所指向的内存地址,在函数间传递指针变量,是在传递这个地址值,所以依旧被看作以值的方式在传递。
Go语言支持闭包,这里就应用了闭包。实际上,在匿名函数内访问searchTerm和results变量,也是通过闭包的形式访问的。因为有了闭包,函数可以直接访问到那些没有作为参数传入的变量。匿名函数并没有拿到这些变量的副本,而是直接访问外层函数作用域中声明的这些变量本身。因为matcher和feed变量每次调用时值不相同,所以并没有使用闭包的方式访问这两个变量
type Feed struct {
Name string json:"site"
URI string json:"link"
Type string json:"type"
}我们声明了一个名叫Feed的结构类型。这个类型会对外暴露。这个类型里面声明了3个字段,每个字段的类型都是字符串,对应于数据文件中各个文档的不同字段。每个字段的声明最后 ` 引号里的部分被称作标记(tag)。这个标记里描述了JSON解码的元数据,用于创建Feed类型值的切片。每个标记将结构类型里字段对应到JSON文档里指定名字的字段。
关键字defer会安排随后的函数调用在函数返回时才执行。在使用完文件后,需要主动关闭文件。使用关键字defer来安排调用Close方法,可以保证这个函数一定会被调用。哪怕函数意外崩溃终止,也能保证关键字defer安排调用的函数会被执行。关键字defer可以缩短打开文件和关闭文件之间间隔的代码行数,有助提高代码可读性,减少错误。
命名接口的时候,也需要遵守Go语言的命名惯例。如果接口类型只包含一个方法,那么这个类型的名字以er结尾。我们的例子里就是这么做的,所以这个接口的名字叫作Matcher。如果接口类型内部声明了多个方法,其名字需要与其行为关联。
我们使用一个空结构声明了一个名叫defaultMatcher的结构类型。空结构在创建实例时,不会分配任何内存。这种结构很适合创建没有任何状态的类型。对于默认匹配器来说,不需要维护任何状态,所以我们只要实现对应的接口就行。
func (m defaultMatcher) Search(feed *Feed, searchTerm string) ([]*Result, error) {
return nil, nil
}
如果声明函数的时候带有接收者,则意味着声明了一个方法。这个方法会和指定的接收者的类型绑在一起。在我们的例子里,Search方法与defaultMatcher类型的值绑在一起。这意味着我们可以使用defaultMatcher类型的值或者指向这个类型值的指针来调用Search方法。无论我们是使用接收者类型的值来调用这个方,还是使用接收者类型值的指针来调用这个方法,编译器都会正确地引用或者解引用对应的值,作为接收者传递给Search方法
// 方法声明为使用defaultMatcher类型的值作为接收者
func (m defaultMatcher) Search(feed *Feed, searchTerm string)
// 声明一个指向defaultMatcher类型值的指针
dm := new(defaultMatch)
// 编译器会解开dm指针的引用,使用对应的值调用方法
dm.Search(feed, "test")
// 方法声明为使用指向defaultMatcher类型值的指针作为接收者
func (m *defaultMatcher) Search(feed *Feed, searchTerm string)
// 声明一个defaultMatcher类型的值
var dm defaultMatch
// 编译器会自动生成指针引用dm值,使用指针调用方法
dm.Search(feed, "test")
因为大部分方法在被调用后都需要维护接收者的值的状态,所以,一个最佳实践是,将方法的接收者声明为指针。对于defaultMatcher类型来说,使用值作为接收者是因为创建一个defaultMatcher类型的值不需要分配内存。由于defaultMatcher不需要维护状态,所以不需要指针形式的接收者。
与直接通过值或者指针调用方法不同,如果通过接口类型的值调用方法,规则有很大不同,如代码清单2-38所示。使用指针作为接收者声明的方法,只能在接口类型的值是一个指针的时候被调用。使用值作为接收者声明的方法,在接口类型的值为值或者指针时,都可以被调用。
// 方法声明为使用指向defaultMatcher类型值的指针作为接收者
func (m *defaultMatcher) Search(feed *Feed, searchTerm string)
// 通过interface类型的值来调用方法
var dm defaultMatcher
var matcher Matcher = dm // 将值赋值给接口类型
matcher.Search(feed, "test") // 使用值来调用接口方法
go build
cannot use dm (type defaultMatcher) as type Matcher in assignment
// 方法声明为使用defaultMatcher类型的值作为接收者
func (m defaultMatcher) Search(feed *Feed, searchTerm string)
// 通过interface类型的值来调用方法
var dm defaultMatcher
var matcher Matcher = &dm // 将指针赋值给接口类型
matcher.Search(feed, "test") // 使用指针来调用接口方法
go build
Build Successful
接口是用来定义行为的类型。这些被定义的行为不由接口直接实现,而是通过方法由用户定义的类型实现。如果用户定义的类型实现了某个接口类型声明的一组方法,那么这个用户定义的类型的值就可以赋给这个接口类型的值。这个赋值会把用户定义的类型的值存入接口类型的值。
接口
- 接口
01 // 这个示例程序展示Go语言里如何使用接口
02 package main
03
04 import (
05 "fmt"
06 )
07
08 // notifier是一个定义了
09 // 通知类行为的接口
10 type notifier interface {
11 notify()
12 }
13
14 // user在程序里定义一个用户类型
15 type user struct {
16 name string
17 email string
18 }
19
20 // notify是使用指针接收者实现的方法
21 func (u *user) notify() {
22 fmt.Printf("Sending user email to %s<%s>\n",
23 u.name,
24 u.email)
25 }
26
27 // main是应用程序的入口
28 func main() {
29 // 创建一个user类型的值,并发送通知
30 u := user{"Bill", "bill@email.com"}
31
32 sendNotification(&u)
33
34 // ./listing36.go:32: 不能将u(类型是user)作为
35 // sendNotification的参数类型notifier:
36 // user类型并没有实现notifier
37 // (notify方法使用指针接收者声明)
38 }
39
40 // sendNotification接受一个实现了notifier接口的值
41 // 并发送通知
42 func sendNotification(n notifier) {
43 n.notify()
44 }
- 接口多态
01 // 这个示例程序使用接口展示多态行为
02 package main
03
04 import (
05 "fmt"
06 )
07
08 // notifier是一个定义了
09 // 通知类行为的接口
10 type notifier interface {
11 notify()
12 }
13
14 // user在程序里定义一个用户类型
15 type user struct {
16 name string
17 email string
18 }
19
20 // notify使用指针接收者实现了notifier接口
21 func (u *user) notify() {
22 fmt.Printf("Sending user email to %s<%s>\n",
23 u.name,
24 u.email)
25 }
26
27 // admin定义了程序里的管理员
28 type admin struct {
29 name string
30 email string
31 }
32
33 // notify使用指针接收者实现了notifier接口
34 func (a *admin) notify() {
35 fmt.Printf("Sending admin email to %s<%s>\n",
36 a.name,
37 a.email)
38 }
39
40 // main是应用程序的入口
41 func main() {
42 // 创建一个user值并传给sendNotification
43 bill := user{"Bill", "bill@email.com"}
44 sendNotification(&bill)
45
46 // 创建一个admin值并传给sendNotification
47 lisa := admin{"Lisa", "lisa@email.com"}
48 sendNotification(&lisa)
49 }
50
51 // sendNotification接受一个实现了notifier接口的值
52 // 并发送通知
53 func sendNotification(n notifier) {
54 n.notify()
55 }
- 嵌入类型
Go语言允许用户扩展或者修改已有类型的行为。这个功能对代码复用很重要,在修改已有类型以符合新类型的时候也很重要。这个功能是通过嵌入类型(type embedding)完成的。嵌入类型是将已有的类型直接声明在新的结构类型里。被嵌入的类型被称为新的外部类型的内部类型。
通过嵌入类型,与内部类型相关的标识符会提升到外部类型上。这些被提升的标识符就像直接声明在外部类型里的标识符一样,也是外部类型的一部分。这样外部类型就组合了内部类型包含的所有属性,并且可以添加新的字段和方法。外部类型也可以通过声明与内部类型标识符同名的标识符来覆盖内部标识符的字段或者方法。这就是扩展或者修改已有类型的方法。
在代码清单5-58的第37行,我们创建了一个名为ad的变量,其类型是外部类型admin。这个类型内部嵌入了user类型。之后第48行,我们将这个外部类型变量的地址传给sendNotification函数。编译器认为这个指针实现了notifier接口,并接受了这个值的传递。不过如果看一下整个示例程序,就会发现admin类型并没有实现这个接口。
由于内部类型的提升,内部类型实现的接口会自动提升到外部类型。这意味着由于内部类型的实现,外部类型也同样实现了这个接口。
01 // 这个示例程序展示如何将一个类型嵌入另一个类型,以及
02 // 内部类型和外部类型之间的关系
03 package main
04
05 import (
06 "fmt"
07 )
08
09 // user在程序里定义一个用户类型
10 type user struct {
11 name string
12 email string
13 }
14
15 // notify实现了一个可以通过user类型值的指针
16 // 调用的方法
17 func (u *user) notify() {
18 fmt.Printf("Sending user email to %s<%s>\n",
19 u.name,
20 u.email)
21 }
22
23 // admin代表一个拥有权限的管理员用户
24 type admin struct {
25 user // 嵌入类型
26 level string
27 }
28
29 // main是应用程序的入口
30 func main() {
31 // 创建一个admin用户
32 ad := admin{
33 user: user{
34 name: "john smith",
35 email: "john@yahoo.com",
36 },
37 level: "super",
38 }
39
40 // 我们可以直接访问内部类型的方法
41 ad.user.notify()
42
43 // 内部类型的方法也被提升到外部类型
44 ad.notify()
45 }
如果外部类型并不需要使用内部类型的实现,而想使用自己的一套实现,该怎么办?
01 // 这个示例程序展示当内部类型和外部类型要
02 // 实现同一个接口时的做法
03 package main
04
05 import (
06 "fmt"
07 )
08
08 // notifier是一个定义了
09 // 通知类行为的接口
11 type notifier interface {
12 notify()
13 }
14
15 // user在程序里定义一个用户类型
16 type user struct {
17 name string
18 email string
19 }
20
21 // 通过user类型值的指针
22 // 调用的方法
23 func (u *user) notify() {
24 fmt.Printf("Sending user email to %s<%s>\n",
25 u.name,
26 u.email)
27 }
28
29 // admin代表一个拥有权限的管理员用户
30 type admin struct {
31 user
32 level string
33 }
34
35 // 通过admin类型值的指针
36 // 调用的方法
37 func (a *admin) notify() {
38 fmt.Printf("Sending admin email to %s<%s>\n",
39 a.name,
40 a.email)
41 }
42
43 // main是应用程序的入口
44 func main() {
45 // 创建一个admin用户
46 ad := admin{
47 user: user{
48 name: "john smith",
49 email: "john@yahoo.com",
50 },
51 level: "super",
52 }
53
54 // 给admin用户发送一个通知
55 // 接口的嵌入的内部类型实现并没有提升到
56 // 外部类型
57 sendNotification(&ad)
58
59 // 我们可以直接访问内部类型的方法
60 ad.user.notify()
61
62 // 内部类型的方法没有被提升
63 ad.notify()
64 }
65
66 // sendNotification接受一个实现了notifier接口的值
67 // 并发送通知
68 func sendNotification(n notifier) {
69 n.notify()
70 }
这表明,如果外部类型实现了notify方法,内部类型的实现就不会被提升。不过内部类型的值一直存在,因此还可以通过直接访问内部类型的值,来调用没有被提升的内部类型实现的方法。
- 公开或未公开的标识符
可以看到对counters包里New函数的调用。这个New函数返回的值被赋给一个名为counter的变量。这个程序可以编译并且运行,但为什么呢?New函数返回的是一个未公开的alertCounter类型的值,而main函数能够接受这个值并创建一个未公开的类型的变量。
要让这个行为可行,需要两个理由。第一,公开或者未公开的标识符,不是一个值。第二,短变量声明操作符,有能力捕获引用的类型,并创建一个未公开的类型的变量。永远不能显式创建一个未公开的类型的变量,不过短变量声明操作符可以这么做。
entities/entities.go
-----------------------------------------------------------------------
01 // entities包包含系统中
02 // 与人有关的类型
03 package entities
04
05 // user在程序里定义一个用户类型
06 type user struct {
07 Name string
08 Email string
09 }
10
11 // Admin在程序里定义了管理员
12 type Admin struct {
13 user // 嵌入的类型是未公开的
14 Rights int
15 }
listing74.go
-----------------------------------------------------------------------
01 // 这个示例程序展示公开的结构类型中如何访问
02 // 未公开的内嵌类型的例子
03 package main
04
05 import (
06 "fmt"
07
08 "github.com/goinaction/code/chapter5/listing74/entities"
09 )
10
11 // main是应用程序的入口
12 func main() {
13 // 创建entities包中的Admin类型的值
14 a := entities.Admin{
15 Rights: 10,
16 }
17
18 // 设置未公开的内部类型的
19 // 公开字段的值
20 a.Name = "Bill"
21 a.Email = "bill@email.com"
22
23 fmt.Printf("User: %v\n", a)
24 }
让我们从代码清单5-76的第14行的main函数开始。这个函数创建了entities包中的Admin类型的值。由于内部类型user是未公开的,这段代码无法直接通过结构字面量的方式初始化该内部类型。不过,即便内部类型是未公开的,内部类型里声明的字段依旧是公开的。既然内部类型的标识符提升到了外部类型,这些公开的字段也可以通过外部类型的字段的值来访问。
因此,在第20行和第21行,来自未公开的内部类型的字段Name和Email可以通过外部类型的变量a被访问并被初始化。因为user类型是未公开的,所以这里没有直接访问内部类型。
并发
goroutine
Go语言里的并发指的是能让某个函数独立于其他函数运行的能力。当一个函数创建为goroutine时,Go会将其视为一个独立的工作单元。这个单元会被调度到可用的逻辑处理器上执行。Go语言运行时的调度器是一个复杂的软件,能管理被创建的所有goroutine并为其分配执行时间。这个调度器在操作系统之上,将操作系统的线程与语言运行时的逻辑处理器绑定,并在逻辑处理器上运行goroutine。调度器在任何给定的时间,都会全面控制哪个goroutine要在哪个逻辑处理器上运行。
Go语言的并发同步模型来自一个叫作通信顺序进程(Communicating Sequential Processes,CSP)的范型(paradigm)。CSP是一种消息传递模型,通过在goroutine之间传递数据来传递消息,而不是对数据进行加锁来实现同步访问。用于在goroutine之间同步和传递数据的关键数据类型叫作通道(channel)。
让我们先来学习一下抽象程度较高的概念:什么是操作系统的线程(thread)和进程(process)。这会有助于后面理解Go语言运行时调度器如何利用操作系统来并发运行goroutine。当运行一个应用程序(如一个IDE或者编辑器)的时候,操作系统会为这个应用程序启动一个进程。可以将这个进程看作一个包含了应用程序在运行中需要用到和维护的各种资源的容器。
图6-1展示了一个包含所有可能分配的常用资源的进程。这些资源包括但不限于内存地址空间、文件和设备的句柄以及线程。一个线程是一个执行空间,这个空间会被操作系统调度来运行函数中所写的代码。每个进程至少包含一个线程,每个进程的初始线程被称作主线程。因为执行这个线程的空间是应用程序的本身的空间,所以当主线程终止时,应用程序也会终止。操作系统将线程调度到某个处理器上运行,这个处理器并不一定是进程所在的处理器。不同操作系统使用的线程调度算法一般都不一样,但是这种不同会被操作系统屏蔽,并不会展示给程序员。
- 操作系统会在物理处理器上调度线程来运行,而Go语言的运行时会在逻辑处理器上调度goroutine来运行。每个逻辑处理器都分别绑定到单个操作系统线程。在1.5版本上,Go语言的运行时默认会为每个可用的物理处理器分配一个逻辑处理器。在1.5版本之前的版本中,默认给整个应用程序只分配一个逻辑处理器。这些逻辑处理器会用于执行所有被创建的goroutine。即便只有一个逻辑处理器,Go也可以以神奇的效率和性能,并发调度无数个goroutine。
在图6-2中,可以看到操作系统线程、逻辑处理器和本地运行队列之间的关系。如果创建一个goroutine并准备运行,这个goroutine就会被放到调度器的全局运行队列中。之后,调度器就将这些队列中的goroutine分配给一个逻辑处理器,并放到这个逻辑处理器对应的本地运行队列中。本地运行队列中的goroutine会一直等待直到自己被分配的逻辑处理器执行。
图6-2 Go调度器如何管理goroutine
有时,正在运行的goroutine需要执行一个阻塞的系统调用,如打开一个文件。当这类调用发生时,线程和goroutine会从逻辑处理器上分离,该线程会继续阻塞,等待系统调用的返回。与此同时,这个逻辑处理器就失去了用来运行的线程。所以,调度器会创建一个新线程,并将其绑定到该逻辑处理器上。之后,调度器会从本地运行队列里选择另一个goroutine来运行。一旦被阻塞的系统调用执行完成并返回,对应的goroutine会放回到本地运行队列,而之前的线程会保存好,以便之后可以继续使用。
如果一个goroutine需要做一个网络I/O调用,流程上会有些不一样。在这种情况下,goroutine会和逻辑处理器分离,并移到集成了网络轮询器的运行时。一旦该轮询器指示某个网络读或者写操作已经就绪,对应的goroutine就会重新分配到逻辑处理器上来完成操作。调度器对可以创建的逻辑处理器的数量没有限制,但语言运行时默认限制每个程序最多创建10 000个线程。这个限制值可以通过调用runtime/debug包的SetMaxThreads方法来更改。如果程序试图使用更多的线程,就会崩溃。
并发(concurrency)不是并行(parallelism)。并行是让不同的代码片段同时在不同的物理处理器上执行。并行的关键是同时做很多事情,而并发是指同时管理很多事情,这些事情可能只做了一半就被暂停去做别的事情了。在很多情况下,并发的效果比并行好,因为操作系统和硬件的总资源一般很少,但能支持系统同时做很多事情。这种“使用较少的资源做更多的事情”的哲学,也是指导Go语言设计的哲学。
如果希望让goroutine并行,必须使用多于一个逻辑处理器。当有多个逻辑处理器时,调度器会将goroutine平等分配到每个逻辑处理器上。这会让goroutine在不同的线程上运行。不过要想真的实现并行的效果,用户需要让自己的程序运行在有多个物理处理器的机器上。否则,哪怕Go语言运行时使用多个线程,goroutine依然会在同一个物理处理器上并发运行,达不到并行的效果。
01 // 这个示例程序展示如何创建goroutine
02 // 以及调度器的行为
03 package main
04
05 import (
06 "fmt"
07 "runtime"
08 "sync"
09 )
10
11 // main是所有Go程序的入口
12 func main() {
13 // 分配一个逻辑处理器给调度器使用
14 runtime.GOMAXPROCS(1)
15
16 // wg用来等待程序完成
17 // 计数加2,表示要等待两个goroutine
18 var wg sync.WaitGroup
19 wg.Add(2)
20
21 fmt.Println("StartGoroutines")
22
23 // 声明一个匿名函数,并创建一个goroutine
24 go func() {
25 // 在函数退出时调用Done来通知main函数工作已经完成
26 defer wg.Done()
27
28 // 显示字母表3次
29 for count := 0; count < 3; count++ {
30 for char := 'a'; char < 'a'+26; char++ {
31 fmt.Printf("%c ", char)
32 }
33 }
34 }()
35
36 // 声明一个匿名函数,并创建一个goroutine
37 go func() {
38 // 在函数退出时调用Done来通知main函数工作已经完成
39 defer wg.Done()
40
41 // 显示字母表3次
42 for count := 0; count < 3; count++ {
43 for char := 'A'; char < 'A'+26; char++ {
44 fmt.Printf("%c ", char)
45 }
46 }
47 }()
48
49 // 等待goroutine结束
50 fmt.Println("Waiting To Finish")
51 wg.Wait()
52
53 fmt.Println("\nTerminating Program")
54 }
01 // 这个示例程序展示goroutine调度器是如何在单个线程上
02 // 切分时间片的
03 package main
04
05 import (
06 "fmt"
07 "runtime"
08 "sync"
09 )
10
11 // wg用来等待程序完成
12 var wg sync.WaitGroup
13
14 // main是所有Go程序的入口
15 func main() {
16 // 分配一个逻辑处理器给调度器使用
17 runtime.GOMAXPROCS(1)
18
19 // 计数加2,表示要等待两个goroutine
20 wg.Add(2)
21
22 // 创建两个goroutine
23 fmt.Println("CreateGoroutines")
24 go printPrime("A")
25 go printPrime("B")
26
27 // 等待goroutine结束
28 fmt.Println("Waiting To Finish")
29 wg.Wait()
30
31 fmt.Println("Terminating Program")
32 }
33
34 // printPrime 显示5000以内的素数值
35 func printPrime(prefix string) {
36 // 在函数退出时调用Done来通知main函数工作已经完成
37 defer wg.Done()
38
39 next:
40 for outer := 2; outer < 5000; outer++ {
41 for inner := 2; inner < outer; inner++ {
42 if outer%inner == 0 {
43 continue next
44 }
45 }
46 fmt.Printf("%s:%d\n", prefix, outer)
47 }
48 fmt.Println("Completed", prefix)
49 }
import "runtime"
// 给每个可用的核心分配一个逻辑处理器
runtime.GOMAXPROCS(runtime.NumCPU())
竞争状态
如果两个或者多个goroutine在没有互相同步的情况下,访问某个共享的资源,并试图同时读和写这个资源,就处于相互竞争的状态,这种情况被称作竞争状态(race candition)。竞争状态的存在是让并发程序变得复杂的地方,十分容易引起潜在问题。对一个共享资源的读和写操作必须是原子化的,换句话说,同一时刻只能有一个goroutine对共享资源进行读和写操作。
01 // 这个示例程序展示如何在程序里造成竞争状态
02 // 实际上不希望出现这种情况
03 package main
04
05 import (
06 "fmt"
07 "runtime"
08 "sync"
09 )
10
11 var (
12 // counter是所有goroutine都要增加其值的变量
13 counter int
14
15 // wg用来等待程序结束
16 wg sync.WaitGroup
17 )
18
19 // main是所有Go程序的入口
20 func main() {
21 // 计数加2,表示要等待两个goroutine
22 wg.Add(2)
23
24 // 创建两个goroutine
25 go incCounter(1)
26 go incCounter(2)
27
28 // 等待goroutine结束
29 wg.Wait()
30 fmt.Println("Final Counter:", counter)
31 }
32
33 // incCounter增加包里counter变量的值
34 func incCounter(id int) {
35 // 在函数退出时调用Done来通知main函数工作已经完成
36 defer wg.Done()
37
38 for count := 0; count < 2; count++ {
39 // 捕获counter的值
40 value := counter
41
42 // 当前goroutine从线程退出,并放回到队列
43 runtime.Gosched()
44
45 // 增加本地value变量的值
46 value++
47
48 // 将该值保存回counter
49 counter = value
50 }
51 }
让我们顺着程序理解一下发生了什么。在第25行和第26行,使用incCounter函数创建了两个goroutine。在第34行,incCounter函数对包内变量counter进行了读和写操作,而这个变量是这个示例程序里的共享资源。每个goroutine都会先读出这个counter变量的值,并在第40行将counter变量的副本存入一个叫作value的本地变量。之后在第46行,incCounter函数对value的副本的值加1,最终在第49行将这个新值存回到counter变量。这个函数在第43行调用了runtime包的Gosched函数,用于将goroutine从当前线程退出,给其他goroutine运行的机会。在两次操作中间这样做的目的是强制调度器切换两个goroutine,以便让竞争状态的效果变得更明显。
Go语言有一个特别的工具,可以在代码里检测竞争状态。在查找这类错误的时候,这个工具非常好用,尤其是在竞争状态并不像这个例子里这么明显的时候。让我们用这个竞争检测器来检测一下我们的例子代码
go build -race // 用竞争检测器标志来编译程序
./example // 运行程序
锁住共享资源
Go语言提供了传统的同步goroutine的机制,就是对共享资源加锁。如果需要顺序访问一个整型变量或者一段代码,atomic和sync包里的函数提供了很好的解决方案。下面我们了解一下atomic包里的几个函数以及sync包里的mutex类型。
原子函数
原子函数能够以很底层的加锁机制来同步访问整型变量和指针。
01 // 这个示例程序展示如何使用atomic包来提供
02 // 对数值类型的安全访问
03 package main
04
05 import (
06 "fmt"
07 "runtime"
08 "sync"
09 "sync/atomic"
10 )
11
12 var (
13 // counter是所有goroutine都要增加其值的变量
14 counter int64
15
16 // wg用来等待程序结束
17 wg sync.WaitGroup
18 )
19
20 // main是所有Go程序的入口
21 func main() {
22 // 计数加2,表示要等待两个goroutine
23 wg.Add(2)
24
25 // 创建两个goroutine
26 go incCounter(1)
27 go incCounter(2)
28
29 // 等待goroutine结束
30 wg.Wait()
31
32 // 显示最终的值
33 fmt.Println("Final Counter:", counter)
34 }
35
36 // incCounter增加包里counter变量的值
37 func incCounter(id int) {
38 // 在函数退出时调用Done来通知main函数工作已经完成
39 defer wg.Done()
40
41 for count := 0; count < 2; count++ {
42 // 安全地对counter加1
43 atomic.AddInt64(&counter, 1)
44
45 // 当前goroutine从线程退出,并放回到队列
46 runtime.Gosched()
47 }
48 }
现在,程序的第43行使用了atmoic包的AddInt64函数。这个函数会同步整型值的加法,方法是强制同一时刻只能有一个goroutine运行并完成这个加法操作。当goroutine试图去调用任何原子函数时,这些goroutine都会自动根据所引用的变量做同步处理。现在我们得到了正确的值4。
另外两个有用的原子函数是LoadInt64和StoreInt64。这两个函数提供了一种安全地读和写一个整型值的方式。代码清单6-15中的示例程序使用LoadInt64和StoreInt64来创建一个同步标志,这个标志可以向程序里多个goroutine通知某个特殊状态。
互斥锁
另一种同步访问共享资源的方式是使用互斥锁(mutex)。互斥锁这个名字来自互斥(mutual exclusion)的概念。互斥锁用于在代码上创建一个临界区,保证同一时间只有一个goroutine可以执行这个临界区代码。
01 // 这个示例程序展示如何使用互斥锁来
02 // 定义一段需要同步访问的代码临界区
03 // 资源的同步访问
04 package main
05
06 import (
07 "fmt"
08 "runtime"
09 "sync"
10 )
11
12 var (
13 // counter是所有goroutine都要增加其值的变量
14 counter int
15
16 // wg用来等待程序结束
17 wg sync.WaitGroup
18
19 // mutex 用来定义一段代码临界区
20 mutex sync.Mutex
21 )
22
23 // main是所有Go程序的入口
24 func main() {
25 // 计数加2,表示要等待两个goroutine
26 wg.Add(2)
27
28 // 创建两个goroutine
29 go incCounter(1)
30 go incCounter(2)
31
32 // 等待goroutine结束
33 wg.Wait()
34 fmt.Printf("Final Counter: %d\\n", counter)
35 }
36
37 // incCounter使用互斥锁来同步并保证安全访问,
38 // 增加包里counter变量的值
39 func incCounter(id int) {
40 // 在函数退出时调用Done来通知main函数工作已经完成
41 defer wg.Done()
42
43 for count := 0; count < 2; count++ {
44 // 同一时刻只允许一个goroutine进入
45 // 这个临界区
46 mutex.Lock()
47 {
48 // 捕获counter的值
49 value := counter
50
51 // 当前goroutine从线程退出,并放回到队列
52 runtime.Gosched()
53
54 // 增加本地value变量的值
55 value++
56
57 // 将该值保存回counter
58 counter = value
59 }
60 mutex.Unlock()
61 // 释放锁,允许其他正在等待的goroutine
62 // 进入临界区
63 }
64 }
对counter变量的操作在第46行和第60行的Lock()和Unlock()函数调用定义的临界区里被保护起来。使用大括号只是为了让临界区看起来更清晰,并不是必需的。同一时刻只有一个goroutine可以进入临界区。之后,直到调用Unlock()函数之后,其他goroutine才能进入临界区。
通道
原子函数和互斥锁都能工作,但是依靠它们都不会让编写并发程序变得更简单,更不容易出错,或者更有趣。在Go语言里,你不仅可以使用原子函数和互斥锁来保证对共享资源的安全访问以及消除竞争状态,还可以使用通道,通过发送和接收需要共享的资源,在goroutine之间做同步。
当一个资源需要在goroutine之间共享时,通道在goroutine之间架起了一个管道,并提供了确保同步交换数据的机制。声明通道时,需要指定将要被共享的数据的类型。可以通过通道共享内置类型、命名类型、结构类型和引用类型的值或者指针。
// 无缓冲的整型通道
unbuffered := make(chan int)
// 有缓冲的字符串通道
buffered := make(chan string, 10)
可以看到使用内置函数make创建了两个通道,一个无缓冲的通道,一个有缓冲的通道。make的第一个参数需要是关键字chan,之后跟着允许通道交换的数据的类型。如果创建的是一个有缓冲的通道,之后还需要在第二个参数指定这个通道的缓冲区的大小。
// 有缓冲的字符串通道
buffered := make(chan string, 10)
// 通过通道发送一个字符串
buffered <- "Gopher"
// 从通道接收一个字符串
value := <-buffered
当从通道里接收一个值或者指针时,<-运算符在要操作的通道变量的左侧,如代码清单6-19所示。
通道是否带有缓冲,其行为会有一些不同。理解这个差异对决定到底应该使用还是不使用缓冲很有帮助。下面我们分别介绍一下这两种类型。
无缓冲的通道
无缓冲的通道(unbuffered channel)是指在接收前没有能力保存任何值的通道。这种类型的通道要求发送goroutine和接收goroutine同时准备好,才能完成发送和接收操作。如果两个goroutine没有同时准备好,通道会导致先执行发送或接收操作的goroutine阻塞等待。这种对通道进行发送和接收的交互行为本身就是同步的。其中任意一个操作都无法离开另一个操作单独存在。
01 // 这个示例程序展示如何用无缓冲的通道来模拟
02 // 2个goroutine间的网球比赛
03 package main
04
05 import (
06 "fmt"
07 "math/rand"
08 "sync"
09 "time"
10 )
11
12 // wg用来等待程序结束
13 var wg sync.WaitGroup
14
15 func init() {
16 rand.Seed(time.Now().UnixNano())
17 }
18
19 // main是所有Go程序的入口
20 func main() {
21 // 创建一个无缓冲的通道
22 court := make(chan int)
23
24 // 计数加2,表示要等待两个goroutine
25 wg.Add(2)
26
27 // 启动两个选手
28 go player("Nadal", court)
29 go player("Djokovic", court)
30
31 // 发球
32 court <- 1
33
34 // 等待游戏结束
35 wg.Wait()
36 }
37
38 // player 模拟一个选手在打网球
39 func player(name string, court chan int) {
40 // 在函数退出时调用Done来通知main函数工作已经完成
41 defer wg.Done()
42
43 for {
44 // 等待球被击打过来
45 ball, ok := <-court
46 if !ok {
47 // 如果通道被关闭,我们就赢了
48 fmt.Printf("Player %s Won\n", name)
49 return
50 }
51
52 // 选随机数,然后用这个数来判断我们是否丢球
53 n := rand.Intn(100)
54 if n%13 == 0 {
55 fmt.Printf("Player %s Missed\n", name)
56
57 // 关闭通道,表示我们输了
58 close(court)
59 return
60 }
61
62 // 显示击球数,并将击球数加1
63 fmt.Printf("Player %s Hit %d\n", name, ball)
64 ball++
65
66 // 将球打向对手
67 court <- ball
68 }
69 }
01 // 这个示例程序展示如何用无缓冲的通道来模拟
02 // 4个goroutine间的接力比赛
03 package main
04
05 import (
06 "fmt"
07 "sync"
08 "time"
09 )
10
11 // wg用来等待程序结束
12 var wg sync.WaitGroup
13
14 // main是所有Go程序的入口
15 func main() {
16 // 创建一个无缓冲的通道
17 baton := make(chan int)
18
19 // 为最后一位跑步者将计数加1
20 wg.Add(1)
21
22 // 第一位跑步者持有接力棒
23 go Runner(baton)
24
25 // 开始比赛
26 baton <- 1
27
28 // 等待比赛结束
29 wg.Wait()
30 }
31
32 // Runner模拟接力比赛中的一位跑步者
33 func Runner(baton chan int) {
34 var newRunner int
35
36 // 等待接力棒
37 runner := <-baton
38
39 // 开始绕着跑道跑步
40 fmt.Printf("Runner %d Running With Baton\n", runner)
41
42 // 创建下一位跑步者
43 if runner != 4 {
44 newRunner = runner + 1
45 fmt.Printf("Runner %d To The Line\n", newRunner)
46 go Runner(baton)
47 }
48
49 // 围绕跑道跑
50 time.Sleep(100 * time.Millisecond)
51
52 // 比赛结束了吗?
53 if runner == 4 {
54 fmt.Printf("Runner %d Finished, Race Over\n", runner)
55 wg.Done()
56 return
57 }
58
59 // 将接力棒交给下一位跑步者
60 fmt.Printf("Runner %d Exchange With Runner %d\n",
61 runner,
62 newRunner)
63
64 baton <- newRunner
65 }
有缓冲的通道
有缓冲的通道(buffered channel)是一种在被接收前能存储一个或者多个值的通道。这种类型的通道并不强制要求goroutine之间必须同时完成发送和接收。通道会阻塞发送和接收动作的条件也会不同。只有在通道中没有要接收的值时,接收动作才会阻塞。只有在通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。这导致有缓冲的通道和无缓冲的通道之间的一个很大的不同:无缓冲的通道保证进行发送和接收的goroutine会在同一时间进行数据交换;有缓冲的通道没有这种保证。
01 // 这个示例程序展示如何使用
02 // 有缓冲的通道和固定数目的
03 // goroutine来处理一堆工作
04 package main
05
06 import (
07 "fmt"
08 "math/rand"
09 "sync"
10 "time"
11 )
12
13 const (
14 numberGoroutines = 4 // 要使用的goroutine的数量
15 taskLoad = 10 // 要处理的工作的数量
16 )
17
18 // wg用来等待程序完成
19 var wg sync.WaitGroup
20
21 // init初始化包,Go语言运行时会在其他代码执行之前
22 // 优先执行这个函数
23 func init() {
24 // 初始化随机数种子
25 rand.Seed(time.Now().Unix())
26 }
27
28 // main是所有Go程序的入口
29 func main() {
30 // 创建一个有缓冲的通道来管理工作
31 tasks := make(chan string, taskLoad)
32
33 // 启动goroutine来处理工作
34 wg.Add(numberGoroutines)
35 for gr := 1; gr <= numberGoroutines; gr++ {
36 go worker(tasks, gr)
37 }
38
39 // 增加一组要完成的工作
40 for post := 1; post <= taskLoad; post++ {
41 tasks <- fmt.Sprintf("Task : %d", post)
42 }
43
44 // 当所有工作都处理完时关闭通道
45 // 以便所有goroutine退出
46 close(tasks)
47
48 // 等待所有工作完成
49 wg.Wait()
50 }
51
52 // worker作为goroutine启动来处理
53 // 从有缓冲的通道传入的工作
54 func worker(tasks chan string, worker int) {
55 // 通知函数已经返回
56 defer wg.Done()
57
58 for {
59 // 等待分配工作
60 task, ok := <-tasks
61 if !ok {
62 // 这意味着通道已经空了,并且已被关闭
63 fmt.Printf("Worker: %d : Shutting Down\n", worker)
64 return
65 }
66
67 // 显示我们开始工作了
68 fmt.Printf("Worker: %d : Started %s\n", worker, task)
69
70 // 随机等一段时间来模拟工作
71 sleep := rand.Int63n(100)
72 time.Sleep(time.Duration(sleep) * time.Millisecond)
73
74 // 显示我们完成了工作
75 fmt.Printf("Worker: %d : Completed %s\n", worker, task)
76 }
77 }
第7章 并发模式
runner包
runner包用于展示如何使用通道来监视程序的执行时间,如果程序运行时间太长,也可以用runner包来终止程序。当开发需要调度后台处理任务的程序的时候,这种模式会很有用。这个程序可能会作为cron作业执行,或者在基于定时任务的云环境(如iron.io)里执行。
01 // Gabriel Aszalos协助完成了这个示例
02 // runner包管理处理任务的运行和生命周期
03 package runner
04
05 import (
06 "errors"
07 "os"
08 "os/signal"
09 "time"
10 )
11
12 // Runner在给定的超时时间内执行一组任务,
13 // 并且在操作系统发送中断信号时结束这些任务
14 type Runner struct {
15 // interrupt通道报告从操作系统
16 // 发送的信号
17 interrupt chan os.Signal
18
19 // complete通道报告处理任务已经完成
20 complete chan error
21
22 // timeout报告处理任务已经超时
23 timeout <-chan time.Time
24
25 // tasks持有一组以索引顺序依次执行的
26 // 函数
27 tasks []func(int)
28 }
29
30 // ErrTimeout会在任务执行超时时返回
31 var ErrTimeout = errors.New("received timeout")
32
33 // ErrInterrupt会在接收到操作系统的事件时返回
34 var ErrInterrupt = errors.New("received interrupt")
35
36 // New返回一个新的准备使用的Runner
37 func New(d time.Duration) *Runner {
38 return &Runner{
39 interrupt: make(chan os.Signal, 1),
40 complete: make(chan error),
41 timeout: time.After(d),
42 }
43 }
44
45 // Add将一个任务附加到Runner上。这个任务是一个
46 // 接收一个int类型的ID作为参数的函数
47 func (r *Runner) Add(tasks ...func(int)) {
48 r.tasks = append(r.tasks, tasks...)
49 }
50
51 // Start执行所有任务,并监视通道事件
52 func (r *Runner) Start() error {
53 // 我们希望接收所有中断信号
54 signal.Notify(r.interrupt, os.Interrupt)
55
56 // 用不同的goroutine执行不同的任务
57 go func() {
58 r.complete <- r.run()
59 }()
60
61 select {
62 // 当任务处理完成时发出的信号
63 case err := <-r.complete:
64 return err
65
66 // 当任务处理程序运行超时时发出的信号
67 case <-r.timeout:
68 return ErrTimeout
69 }
70 }
71
72 // run执行每一个已注册的任务
73 func (r *Runner) run() error {
74 for id, task := range r.tasks {
75 // 检测操作系统的中断信号
76 if r.gotInterrupt() {
77 return ErrInterrupt
78 }
79
80 // 执行已注册的任务
81 task(id)
82 }
83
84 return nil
85 }
86
87 // gotInterrupt验证是否接收到了中断信号
88 func (r *Runner) gotInterrupt() bool {
89 select {
90 // 当中断事件被触发时发出的信号
91 case <-r.interrupt:
92 // 停止接收后续的任何信号
93 signal.Stop(r.interrupt)
95 return true
96
97 // 继续正常运行
98 default:
99 return false
100 }
101 }
pool包
这个包用于展示如何使用有缓冲的通道实现资源池,来管理可以在任意数量的goroutine之间共享及独立使用的资源。这种模式在需要共享一组静态资源的情况(如共享数据库连接或者内存缓冲区)下非常有用。如果goroutine需要从池里得到这些资源中的一个,它可以从池里申请,使用完后归还到资源池里。
01 // Fatih Arslan和Gabriel Aszalos协助完成了这个示例
02 // 包pool管理用户定义的一组资源
03 package pool
04
05 import (
06 "errors"
07 "log"
08 "io"
09 "sync"
10 )
11
12 // Pool管理一组可以安全地在多个goroutine间
13 // 共享的资源。被管理的资源必须
14 // 实现io.Closer接口
15 type Pool struct {
16 m sync.Mutex
17 resources chan io.Closer
18 factory func() (io.Closer, error)
19 closed bool
20 }
21
22 // ErrPoolClosed表示请求(Acquire)了一个
23 // 已经关闭的池
24 var ErrPoolClosed = errors.New("Pool has been closed.")
25
26 // New创建一个用来管理资源的池。
27 // 这个池需要一个可以分配新资源的函数,
28 // 并规定池的大小
29 func New(fn func() (io.Closer, error), size uint) (*Pool, error) {
30 if size <= 0 {
31 return nil, errors.New("Size value too small.")
32 }
33
34 return &Pool{
35 factory: fn,
36 resources: make(chan io.Closer, size),
37 }, nil
38 }
39
40 // Acquire从池中获取一个资源
41 func (p *Pool) Acquire() (io.Closer, error) {
42 select {
43 // 检查是否有空闲的资源
44 case r, ok := <-p.resources:
45 log.Println("Acquire:", "Shared Resource")
46 if !ok {
47 return nil, ErrPoolClosed
48 }
49 return r, nil
50
51 // 因为没有空闲资源可用,所以提供一个新资源
52 default:
53 log.Println("Acquire:", "New Resource")
54 return p.factory()
55 }
56 }
57
58 // Release将一个使用后的资源放回池里
59 func (p *Pool) Release(r io.Closer) {
60 // 保证本操作和Close操作的安全
61 p.m.Lock()
62 defer p.m.Unlock()
63
64 // 如果池已经被关闭,销毁这个资源
65 if p.closed {
66 r.Close()
67 return
68 }
69
70 select {
71 // 试图将这个资源放入队列
72 case p.resources <- r:
73 log.Println("Release:", "In Queue")
74
75 // 如果队列已满,则关闭这个资源
76 default:
77 log.Println("Release:", "Closing")
78 r.Close()
79 }
80 }
81
82 // Close会让资源池停止工作,并关闭所有现有的资源
83 func (p *Pool) Close() {
84 // 保证本操作与Release操作的安全
85 p.m.Lock()
86 defer p.m.Unlock()
87
88 // 如果pool已经被关闭,什么也不做
89 if p.closed {
90 return
91 }
92
93 // 将池关闭
94 p.closed = true
95
96 // 在清空通道里的资源之前,将通道关闭
97 // 如果不这样做,会发生死锁
98 close(p.resources)
99
100 // 关闭资源
101 for r := range p.resources {
102 r.Close()
103 }
104 }
work包
work包的目的是展示如何使用无缓冲的通道来创建一个goroutine池,这些goroutine执行并控制一组工作,让其并发执行。在这种情况下,使用无缓冲的通道要比随意指定一个缓冲区大小的有缓冲的通道好,因为这个情况下既不需要一个工作队列,也不需要一组goroutine配合执行。无缓冲的通道保证两个goroutine之间的数据交换。这种使用无缓冲的通道的方法允许使用者知道什么时候goroutine池正在执行工作,而且如果池里的所有goroutine都忙,无法接受新的工作的时候,也能及时通过通道来通知调用者。使用无缓冲的通道不会有工作在队列里丢失或者卡住,所有工作都会被处理。
01 // Jason Waldrip协助完成了这个示例
02 // work包管理一个goroutine池来完成工作
03 package work
04
05 import "sync"
06
07 // Worker必须满足接口类型,
08 // 才能使用工作池
09 type Worker interface {
10 Task()
11 }
12
13 // Pool提供一个goroutine池,这个池可以完成
14 // 任何已提交的Worker任务
15 type Pool struct {
16 work chan Worker
17 wg sync.WaitGroup
18 }
19
20 // New创建一个新工作池
21 func New(maxGoroutines int) *Pool {
22 p := Pool{
23 work: make(chan Worker),
24 }
25
26 p.wg.Add(maxGoroutines)
27 for i := 0; i < maxGoroutines; i++ {
28 go func() {
29 for w := range p.work {
30 w.Task()
31 }
32 p.wg.Done()
33 }()
34 }
35
36 return &p
37 }
38
39 // Run提交工作到工作池
40 func (p *Pool) Run(w Worker) {
41 p.work <- w
42 }
43
44 // Shutdown等待所有goroutine停止工作
45 func (p *Pool) Shutdown() {
46 close(p.work)
47 p.wg.Wait()
48 }
第8章 标准库
记录日志
01 // 这个示例程序展示如何使用最基本的log包
02 package main
03
04 import (
05 "log"
06 )
07
08 func init() {
09 log.SetPrefix("TRACE: ")
10 log.SetFlags(log.Ldate | log.Lmicroseconds | log.Llongfile)
11 }
12
13 func main() {
14 // Println写到标准日志记录器
15 log.Println("message")
16
17 // Fatalln在调用Println()之后会接着调用os.Exit(1)
18 log.Fatalln("fatal message")
19
20 // Panicln在调用Println()之后会接着调用panic()
21 log.Panicln("panic message")
22 }
const (
// 将下面的位使用或运算符连接在一起,可以控制要输出的信息。没有
// 办法控制这些信息出现的顺序(下面会给出顺序)或者打印的格式
// (格式在注释里描述)。这些项后面会有一个冒号:
// 2009/01/23 01:23:23.123123 /a/b/c/d.go:23: message
// 日期: 2009/01/23
Ldate = 1 << iota
// 时间: 01:23:23
Ltime
// 毫秒级时间: 01:23:23.123123。该设置会覆盖Ltime标志
Lmicroseconds
// 完整路径的文件名和行号: /a/b/c/d.go:23
Llongfile
// 最终的文件名元素和行号: d.go:23
// 覆盖 Llongfile
Lshortfile
// 标准日志记录器的初始值
LstdFlags = Ldate | Ltime
)
如何使用3个函数Println、Fatalln和Panicln来写日志消息。这些函数也有可以格式化消息的版本,只需要用f替换结尾的ln。Fatal系列函数用来写日志消息,然后使用os.Exit(1)终止程序。Panic系列函数用来写日志消息,然后触发一个panic。除非程序执行recover函数,否则会导致程序打印调用栈后终止。Print系列函数是写日志消息的标准方法。log包有一个很方便的地方就是,这些日志记录器是多goroutine安全的。这意味着在多个goroutine可以同时调用来自同一个日志记录器的这些函数,而不会有彼此间的写冲突。标准日志记录器具有这一性质,用户定制的日志记录器也应该满足这一性质。
编码/解码
01 // 这个示例程序展示如何使用json包和NewDecoder函数
02 // 来解码JSON响应
03 package main
04
05 import (
06 "encoding/json"
07 "fmt"
08 "log"
09 "net/http"
10 )
11
12 type (
13 // gResult映射到从搜索拿到的结果文档
14 gResult struct {
15 GsearchResultClass string `json:"GsearchResultClass"`
16 UnescapedURL string `json:"unescapedUrl"`
17 URL string `json:"url"`
18 VisibleURL string `json:"visibleUrl"`
19 CacheURL string `json:"cacheUrl"`
20 Title string `json:"title"`
21 TitleNoFormatting string `json:"titleNoFormatting"`
22 Content string `json:"content"`
23 }
24
25 // gResponse包含顶级的文档
26 gResponse struct {
27 ResponseData struct {
28 Results []gResult `json:"results"`
29 } `json:"responseData"`
30 }
31 )
32
33 func main() {
34 uri := "http://ajax.googleapis.com/ajax/services/search/web?v=1.0&rsz=8&q=golang"
35
36 // 向Google发起搜索
37 resp, err := http.Get(uri)
38 if err != nil {
39 log.Println("ERROR:", err)
40 return
41 }
42 defer resp.Body.Close()
43
44 // 将JSON响应解码到结构类型
45 var gr gResponse
46 err = json.NewDecoder(resp.Body).Decode(&gr)
47 if err != nil {
48 log.Println("ERROR:", err)
49 return
50 }
51
52 fmt.Println(gr)
53 }
你会注意到每个字段最后使用单引号声明了一个字符串。这些字符串被称作标签(tag),是提供每个字段的元信息的一种机制,将JSON文档和结构类型里的字段一一映射起来。如果不存在标签,编码和解码过程会试图以大小写无关的方式,直接使用字段的名字进行匹配。如果无法匹配,对应的结构类型里的字段就包含其零值。
// NewDecoder返回从r读取的解码器
//
// 解码器自己会进行缓冲,而且可能会从r读比解码JSON值
// 所需的更多的数据
func NewDecoder(r io.Reader) *Decoder
// Decode从自己的输入里读取下一个编码好的JSON值,
// 并存入v所指向的值里
//
// 要知道从JSON转换为Go的值的细节,
// 请查看Unmarshal的文档
func (dec *Decoder) Decode(v interface{}) error
在代码清单8-25中可以看到NewDecoder函数接受一个实现了io.Reader接口类型的值作为参数。在下一节,我们会更详细地介绍io.Reader和io.Writer接口,现在只需要知道标准库里的许多不同类型,包括http包里的一些类型,都实现了这些接口就行。只要类型实现了这些接口,就可以自动获得许多功能的支持。
函数NewDecoder返回一个指向Decoder类型的指针值。由于Go语言支持复合语句调用,可以直接调用从NewDecoder函数返回的值的Decode方法,而不用把这个返回值存入变量。在代码清单8-25里,可以看到Decode方法接受一个interface{}类型的值做参数,并返回一个error值。
在第5章中曾讨论过,任何类型都实现了一个空接口interface{}。这意味着Decode方法可以接受任意类型的值。使用反射,Decode方法会拿到传入值的类型信息。然后,在读取JSON响应的过程中,Decode方法会将对应的响应解码为这个类型的值。这意味着用户不需要创建对应的值,Decode会为用户做这件事情,如代码清单8-26所示。
在代码清单8-26中,我们向Decode方法传入了指向gResponse类型的指针变量的地址,而这个地址的实际值为nil。该方法调用后,这个指针变量会被赋给一个gResponse类型的值,并根据解码后的JSON文档做初始化。
有时,需要处理的JSON文档会以string的形式存在。在这种情况下,需要将string转换为byte切片([]byte),并使用json包的Unmarshal函数进行反序列化的处理
01 // 这个示例程序展示如何解码JSON字符串
02 package main
03
04 import (
05 "encoding/json"
06 "fmt"
07 "log"
08 )
09
10 // Contact结构代表我们的JSON字符串
11 type Contact struct {
12 Name string `json:"name"`
13 Title string `json:"title"`
14 Contact struct {
15 Home string `json:"home"`
16 Cell string `json:"cell"`
17 } `json:"contact"`
18 }
19
20 // JSON包含用于反序列化的演示字符串
21 var JSON = `{
22 "name": "Gopher",
23 "title": "programmer",
24 "contact": {
25 "home": "415.333.3333",
26 "cell": "415.555.5555"
27 }
28 }`
29
30 func main() {
31 // 将JSON字符串反序列化到变量
32 var c Contact
33 err := json.Unmarshal([]byte(JSON), &c)
34 if err != nil {
35 log.Println("ERROR:", err)
36 return
37 }
38
39 fmt.Println(c)
40 }
有时,无法为JSON的格式声明一个结构类型,而是需要更加灵活的方式来处理JSON文档。在这种情况下,可以将JSON文档解码到一个map变量中
01 // 这个示例程序展示如何解码JSON字符串
02 package main
03
04 import (
05 "encoding/json"
06 "fmt"
07 "log"
08 )
09
10 // JSON包含要反序列化的样例字符串
11 var JSON = `{
12 "name": "Gopher",
13 "title": "programmer",
14 "contact": {
15 "home": "415.333.3333",
16 "cell": "415.555.5555"
17 }
18 }`
19
20 func main() {
21 // 将JSON字符串反序列化到map变量
22 var c map[string]interface{}
23 err := json.Unmarshal([]byte(JSON), &c)
24 if err != nil {
25 log.Println("ERROR:", err)
26 return
27 }
28
29 fmt.Println("Name:", c["name"])
30 fmt.Println("Title:", c["title"])
31 fmt.Println("Contact")
32 fmt.Println("H:", c["contact"].(map[string]interface{})["home"])
33 fmt.Println("C:", c["contact"].(map[string]interface{})["cell"])
34 }
将其中的结构类型变量替换为map类型的变量。变量c声明为一个map类型,其键是string类型,其值是interface{}类型。这意味着这个map类型可以使用任意类型的值作为给定键的值。虽然这种方法为处理JSON文档带来了很大的灵活性,但是却有一个小缺点。让我们看一下访问contact子文档的home字段的代码
因为每个键的值的类型都是interface{},所以必须将值转换为合适的类型,才能处理这个值。代码清单8-30展示了如何将contact键的值转换为另一个键是string类型,值是interface{}类型的map类型。这有时会使映射里包含另一个文档的JSON文档处理起来不那么友好。但是,如果不需要深入正在处理的JSON文档,或者只打算做很少的处理,因为不需要声明新的类型,使用map类型会很快。
编码JSON
我们要学习的处理JSON的第二个方面是,使用json包的MarshalIndent函数进行编码。这个函数可以很方便地将Go语言的map类型的值或者结构类型的值转换为易读格式的JSON文档。序列化(marshal)是指将数据转换为JSON字符串的过程。
01 // 这个示例程序展示如何序列化JSON字符串
02 package main
03
04 import (
05 "encoding/json"
06 "fmt"
07 "log"
08 )
09
10 func main() {
11 // 创建一个保存键值对的映射
12 c := make(map[string]interface{})
13 c["name"] = "Gopher"
14 c["title"] = "programmer"
15 c["contact"] = map[string]interface{}{
16 "home": "415.333.3333",
17 "cell": "415.555.5555",
18 }
19
20 // 将这个映射序列化到JSON字符串
21 data, err := json.MarshalIndent(c, "", " ")
22 if err != nil {
23 log.Println("ERROR:", err)
24 return
25 }
26
27 fmt.Println(string(data))
28 }
代码清单8-31展示了如何使用json包的MarshalIndent函数将一个map值转换为JSON字符串。函数MarshalIndent返回一个byte切片,用来保存JSON字符串和一个error值。下面来看一下json包中MarshalIndent函数的声明,如代码清单8-32所示。
代码清单8-32 golang.org/src/encoding/json/encode.go// MarshalIndent很像Marshal,只是用缩进对输出进行格式化
func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) {
}
在MarshalIndent函数里再一次看到使用了空接口类型interface{}。函数MarshalIndent会使用反射来确定如何将map类型转换为JSON字符串。
如果不需要输出带有缩进格式的JSON字符串,json包还提供了名为Marshal的函数来进行解码。这个函数产生的JSON字符串很适合作为在网络响应(如Web API)的数据。函数Marshal的工作原理和函数MarshalIndent一样,只不过没有用于前缀prefix和缩进indent的参数。
输入和输出
01 // 这个示例程序展示来自不同标准库的不同函数是如何
02 // 使用io.Writer接口的
03 package main
04
05 import (
06 "bytes"
07 "fmt"
08 "os"
09 )
10
11 // main是应用程序的入口
12 func main() {
13 // 创建一个Buffer值,并将一个字符串写入Buffer
14 // 使用实现io.Writer的Write方法
15 var b bytes.Buffer
16 b.Write([]byte("Hello "))
17
18 // 使用Fprintf来将一个字符串拼接到Buffer里
19 // 将bytes.Buffer的地址作为io.Writer类型值传入
20 fmt.Fprintf(&b, "World!")
21
22 // 将Buffer的内容输出到标准输出设备
23 // 将os.File值的地址作为io.Writer类型值传入
24 b.WriteTo(os.Stdout)
25 }
Write从p里向底层的数据流写入len(p)字节的数据。这个方法返回从p里写出的字节
数(0 <= n <= len(p)),以及任何可能导致写入提前结束的错误。Write在返回n
< len(p)的时候,必须返回某个非nil值的error。Write绝不能改写切片里的数据,
哪怕是临时修改也不行。
(1) Read最多读入len(p)字节,保存到p。这个方法返回读入的字节数(0 <= n
<= len(p))和任何读取时发生的错误。即便Read返回的n < len(p),方法也可
能使用所有p的空间存储临时数据。如果数据可以读取,但是字节长度不足len(p),
习惯上Read会立刻返回可用的数据,而不等待更多的数据。
(2) 当成功读取 n > 0字节后,如果遇到错误或者文件读取完成,Read方法会返回
读入的字节数。方法可能会在本次调用返回一个非nil的错误,或者在下一次调用时返
回错误(同时n == 0)。这种情况的的一个例子是,在输入的流结束时,Read会返回
非零的读取字节数,可能会返回err == EOF,也可能会返回err == nil。无论如何,
下一次调用Read应该返回0, EOF。
(3) 调用者在返回的n > 0时,总应该先处理读入的数据,再处理错误err。这样才
能正确操作读取一部分字节后发生的I/O错误。EOF也要这样处理。
(4) Read的实现不鼓励返回0个读取字节的同时,返回nil值的错误。调用者需要将
这种返回状态视为没有做任何操作,而不是遇到读取结束。
简单的curl
01 // 这个示例程序展示如何使用io.Reader和io.Writer接口
02 // 写一个简单版本的curl
03 package main
04
05 import (
06 "io"
07 "log"
08 "net/http"
09 "os"
10 )
11
12 // main是应用程序的入口
13 func main() {
14 // 这里的r是一个响应,r.Body是io.Reader
15 r, err := http.Get(os.Args[1])
16 if err != nil {
17 log.Fatalln(err)
18 }
19
20 // 创建文件来保存响应内容
21 file, err := os.Create(os.Args[2])
22 if err != nil {
23 log.Fatalln(err)
24 }
25 defer file.Close()
26
27 // 使用MultiWriter,这样就可以同时向文件和标准输出设备
28 // 进行写操作
29 dest := io.MultiWriter(os.Stdout, file)
30
31 // 读出响应的内容,并写到两个目的地
32 io.Copy(dest, r.Body)
33 if err := r.Body.Close(); err != nil {
34 log.Println(err)
35 }
36 }
可以在io包里找到大量的支持不同功能的函数,这些函数都能通过实现了io.Writer和io.Reader接口类型的值进行调用。其他包,如http包,也使用类似的模式,将接口声明为包的API的一部分,并提供对io包的支持。应该花时间看一下标准库中提供了些什么,以及它是如何实现的——不仅要防止重新造轮子,还要理解Go语言的设计者的习惯,并将这些习惯应用到自己的包和API的设计上。
测试和性能
作为一名合格的开发者,不应该在程序开发完之后才开始写测试代码。使用Go语言的测试框架,可以在开发的过程中就进行单元测试和基准测试。和go build命令类似,go test命令可以用来执行写好的测试代码,需要做的就是遵守一些规则来写测试。而且,可以将测试无缝地集成到代码工程和持续集成系统里。
在Go语言里有几种方法写单元测试。基础测试(basic test)只使用一组参数和结果来测试一段代码。表组测试(table test)也会测试一段代码,但是会使用多组参数和结果进行测试。也可以使用一些方法来模仿(mock)测试代码需要使用到的外部资源,如数据库或者网络服务器。这有助于让测试在没有所需的外部资源可用的时候,模拟这些资源的行为使测试正常进行。最后,在构建自己的网络服务时,有几种方法可以在不运行服务的情况下,调用服务的功能进行测试。
01 // 这个示例程序展示如何写基础单元测试
02 package listing01
03
04 import (
05 "net/http"
06 "testing"
07 )
08
09 const checkMark = "\u2713"
10 const ballotX = "\u2717"
11
12 // TestDownload确认http包的Get函数可以下载内容
13 func TestDownload(t *testing.T) {
14 url := "http://www.goinggo.net/feeds/posts/default?alt=rss"
15 statusCode := 200
16
17 t.Log("Given the need to test downloading content.")
18 {
19 t.Logf("\tWhen checking \"%s\" for status code \"%d\"",
20 url, statusCode)
21 {
22 resp, err := http.Get(url)
23 if err != nil {
24 t.Fatal("\t\tShould be able to make the Get call.",
25 ballotX, err)
26 }
27 t.Log("\t\tShould be able to make the Get call.",
28 checkMark)
29
30 defer resp.Body.Close()
31
32 if resp.StatusCode == statusCode {
33 t.Logf("\t\tShould receive a \"%d\" status. %v",
34 statusCode, checkMark)
35 } else {
36 t.Errorf("\t\tShould receive a \"%d\" status. %v %v",
37 statusCode, ballotX, resp.StatusCode)
38 }
39 }
40 }
41 }
这个例子背后发生了很多事情,来确保测试能正确工作,并显示结果。让我们从测试文件的文件名开始。如果查看代码清单9-1一开始的部分,会看到测试文件的文件名是listing01_test.go。Go语言的测试工具只会认为以_test.go结尾的文件是测试文件。如果没有遵从这个约定,在包里运行go test的时候就可能会报告没有测试文件。一旦测试工具找到了测试文件,就会查找里面的测试函数并执行。
在代码清单9-2里,可以看到第06行引入了testing包。这个testing包提供了从测试框架到报告测试的输出和状态的各种测试功能的支持。第09行和第10行声明了两个常量,这两个常量包含写测试输出时会用到的对号(√)和叉号(×)。
在代码清单9-3的第13行中,可以看到测试函数的名字是TestDownload。一个测试函数必须是公开的函数,并且以Test单词开头。不但函数名字要以Test开头,而且函数的签名必须接收一个指向testing.T类型的指针,并且不返回任何值。如果没有遵守这些约定,测试框架就不会认为这个函数是一个测试函数,也不会让测试工具去执行它。
指向testing.T类型的指针很重要。这个指针提供的机制可以报告每个测试的输出和状态。测试的输出格式没有标准要求。我更喜欢使用Go写文档的方式,输出容易读的测试结果。对我来说,测试的输出是代码文档的一部分。测试的输出需使用完整易读的语句,来记录为什么需要这个测试,具体测试了什么,以及测试的结果是什么。
可以看到,在代码清单9-4的第14行和第15行,声明并初始化了两个变量。这两个变量包含了要测试的URL,以及期望从响应中返回的状态。在第17行,使用方法t.Log来输出测试的消息。这个方法还有一个名为t.Logf的版本,可以格式化消息。如果执行go test的时候没有加入冗余选项(-v),除非测试失败,否则我们是看不到任何测试输出的。
每个测试函数都应该通过解释这个测试的给定要求(given need),来说明为什么应该存在这个测试。对这个例子来说,给定要求是测试能否成功下载数据。在声明了测试的给定要求后,测试应该说明被测试的代码应该在什么情况下被执行,以及如何执行。
代码清单9-6中的代码使用http包的Get函数来向goinggo.net网络服务器发起请求,请求下载该博客的RSS列表。在Get调用返回之后,会检查错误值,来判断调用是否成功。在每种情况下,我们都会说明测试应有的结果。如果调用失败,除了结果,还会输出叉号以及得到的错误值。如果测试成功,会输出对号。
如果Get调用失败,使用第24行的t.Fatal方法,让测试框架知道这个测试失败了。t.Fatal方法不但报告这个单元测试已经失败,而且会向测试输出写一些消息,而后立刻停止这个测试函数的执行。如果除了这个函数外还有其他没有执行的测试函数,会继续执行其他测试函数。这个方法对应的格式化版本名为t.Fatalf。
如果测试函数执行时没有调用过t.Fatal或者t.Error方法,就会认为测试通过了。
01 // 这个示例程序展示如何写一个基本的表组测试
02 package listing08
03
04 import (
05 "net/http"
06 "testing"
07 )
08
09 const checkMark = "\u2713"
10 const ballotX = "\u2717"
11
12 // TestDownload确认http包的Get函数可以下载内容
13 // 并正确处理不同的状态
14 func TestDownload(t *testing.T) {
15 var urls = []struct {
16 url string
17 statusCode int
18 }{
19 {
20 "http://www.goinggo.net/feeds/posts/default?alt=rss",
21 http.StatusOK,
22 },
23 {
24 "http://rss.cnn.com/rss/cnn_topstbadurl.rss",
25 http.StatusNotFound,
26 },
27 }
28
29 t.Log("Given the need to test downloading different content.")
30 {
31 for _, u := range urls {
32 t.Logf("\tWhen checking \"%s\" for status code \"%d\"",
33 u.url, u.statusCode)
34 {
35 resp, err := http.Get(u.url)
36 if err != nil {
37 t.Fatal("\t\tShould be able to Get the url.",
38 ballotX, err)
39 }
40 t.Log("\t\tShould be able to Get the url",
41 checkMark)
42
43 defer resp.Body.Close()
44
45 if resp.StatusCode == u.statusCode {
46 t.Logf("\t\tShould have a \"%d\" status. %v",
47 u.statusCode, checkMark)
48 } else {
49 t.Errorf("\t\tShould have a \"%d\" status %v %v",
50 u.statusCode, ballotX, resp.StatusCode)
51 }
52 }
53 }
54 }
55 }
模仿调用
01 // 这个示例程序展示如何内部模仿HTTP GET调用
02 // 与本书之前的例子有些差别
03 package listing12
04
05 import (
06 "encoding/xml"
07 "fmt"
08 "net/http"
09 "net/http/httptest"
10 "testing"
11 )
12
13 const checkMark = "\u2713"
14 const ballotX = "\u2717"
15
16 // feed模仿了我们期望接收的XML文档
17 var feed = `<?xml version="1.0" encoding="UTF-8"?>
18 <rss>
19 <channel>
20 <title>GoingGoProgramming</title>
21 <description>Golang : https://github.com/goinggo</description>
22 <link>http://www.goinggo.net/</link>
23 <item>
24 <pubDate>Sun, 15 Mar 2015 15:04:00 +0000</pubDate>
25 <title>Object Oriented Programming Mechanics</title>
26 <description>Gois an object oriented language.</description>
27 <link>http://www.goinggo.net/2015/03/object-oriented</link>
28 </item>
29 </channel>
30 </rss>`
31
32 // mockServer返回用来处理请求的服务器的指针
33 func mockServer() *httptest.Server {
34 f := func(w http.ResponseWriter, r *http.Request) {
35 w.WriteHeader(200)
36 w.Header().Set("Content-Type", "application/xml")
37 fmt.Fprintln(w, feed)
38 }
39
40 return httptest.NewServer(http.HandlerFunc(f))
41 }
42
43 // TestDownload确认http包的Get函数可以下载内容
44 // 并且内容可以被正确地反序列化并关闭
45 func TestDownload(t *testing.T) {
46 statusCode := http.StatusOK
47
48 server := mockServer()
49 defer server.Close()
50
51 t.Log("Given the need to test downloading content.")
52 {
53 t.Logf("\tWhen checking \"%s\" for status code \"%d\"",
54 server.URL, statusCode)
55 {
56 resp, err := http.Get(server.URL)
57 if err != nil {
58 t.Fatal("\t\tShould be able to make the Get call.",
59 ballotX, err)
60 }
61 t.Log("\t\tShould be able to make the Get call.",
62 checkMark)
63
64 defer resp.Body.Close()
65
66 if resp.StatusCode != statusCode {
67 t.Fatalf("\t\tShould receive a \"%d\" status. %v %v",
68 statusCode, ballotX, resp.StatusCode)
69 }
70 t.Logf("\t\tShould receive a \"%d\" status. %v",
71 statusCode, checkMark)
72 }
73 }
74 }
type HandlerFunc func(ResponseWriter, *Request)
HandlerFunc类型是一个适配器,允许常规函数作为HTTP的处理函数使用。如果函数f具有合适的签名,
HandlerFunc(f)就是一个处理HTTP请求的Handler对象,内部通过调用f处理请求
测试服务端点
服务端点(endpoint)是指与服务宿主信息无关,用来分辨某个服务的地址,一般是不包含宿主的一个路径。如果在构造网络API,你会希望直接测试自己的服务的所有服务端点,而不用启动整个网络服务。
01 // 这个示例程序实现了简单的网络服务
02 package main
03
04 import (
05 "log"
06 "net/http"
07
08 "github.com/goinaction/code/chapter9/listing17/handlers"
09 )
10
11 // main是应用程序的入口
12 func main() {
13 handlers.Routes()
14
15 log.Println("listener : Started : Listening on :4000")
16 http.ListenAndServe(":4000", nil)
17 }
01 // handlers包提供了用于网络服务的服务端点
02 package handlers
03
04 import (
05 "encoding/json"
06 "net/http"
07 )
08
09 // Routes为网络服务设置路由
10 func Routes() {
11 http.HandleFunc("/sendjson", SendJSON)
12 }
13
14 // SendJSON返回一个简单的JSON文档
15 func SendJSON(rw http.ResponseWriter, r *http.Request) {
16 u := struct {
17 Name string
18 Email string
19 }{
20 Name: "Bill",
21 Email: "bill@ardanstudios.com",
22 }
23
24 rw.Header().Set("Content-Type", "application/json")
25 rw.WriteHeader(200)
26 json.NewEncoder(rw).Encode(&u)
27 }
01 // 这个示例程序展示如何测试内部服务端点
02 // 的执行效果
03 package handlers_test
04
05 import (
06 "encoding/json"
07 "net/http"
08 "net/http/httptest"
09 "testing"
10
11 "github.com/goinaction/code/chapter9/listing17/handlers"
12 )
13
14 const checkMark = "\u2713"
15 const ballotX = "\u2717"
16
17 func init() {
18 handlers.Routes()
19 }
20
21 // TestSendJSON测试/sendjson内部服务端点
22 func TestSendJSON(t *testing.T) {
23 t.Log("Given the need to test the SendJSON endpoint.")
24 {
25 req, err := http.NewRequest("GET", "/sendjson", nil)
26 if err != nil {
27 t.Fatal("\tShould be able to create a request.",
28 ballotX, err)
29 }
30 t.Log("\tShould be able to create a request.",
31 checkMark)
32
33 rw := httptest.NewRecorder()
34 http.DefaultServeMux.ServeHTTP(rw, req)
35
36 if rw.Code != 200 {
37 t.Fatal("\tShould receive \"200\"", ballotX, rw.Code)
38 }
39 t.Log("\tShould receive \"200\"", checkMark)
40
41 u := struct {
42 Name string
43 Email string
44 }{}
45
46 if err := json.NewDecoder(rw.Body).Decode(&u); err != nil {
47 t.Fatal("\tShould decode the response.", ballotX)
48 }
49 t.Log("\tShould decode the response.", checkMark)
50
51 if u.Name == "Bill" {
52 t.Log("\tShould have a Name.", checkMark)
53 } else {
54 t.Error("\tShould have a Name.", ballotX, u.Name)
55 }
56
57 if u.Email == "bill@ardanstudios.com" {
58 t.Log("\tShould have an Email.", checkMark)
59 } else {
60 t.Error("\tShould have an Email.", ballotX, u.Email)
61 }
62 }
63 }
示例
01 // 这个示例程序展示如何编写基础示例
02 package handlers_test
03
04 import (
05 "encoding/json"
06 "fmt"
07 "log"
08 "net/http"
09 "net/http/httptest"
10 )
11
12 // ExampleSendJSON提供了基础示例
13 func ExampleSendJSON() {
14 r, _ := http.NewRequest("GET", "/sendjson", nil)
15 rw := httptest.NewRecorder()
16 http.DefaultServeMux.ServeHTTP(rw, r)
17
18 var u struct {
19 Name string
20 Email string
21 }
22
23 if err := json.NewDecoder(w.Body).Decode(&u); err != nil {
24 log.Println("ERROR:", err)
25 }
26
27 // 使用fmt将结果写到stdout来检测输出
28 fmt.Println(u)
29 // Output:
30 // {Bill bill@ardanstudios.com}
31 }
示例基于已经存在的函数或者方法。我们需要使用Example代替Test作为函数名的开始。在代码清单9-26的第13行中,示例代码的名字是ExampleSendJSON。
对于示例代码,需要遵守一个规则。示例代码的函数名字必须基于已经存在的公开的函数或者方法。我们的示例的名字基于handlers包里公开的SendJSON函数。如果没有使用已经存在的函数或者方法,这个示例就不会显示在包的Go文档里。
写示例代码的目的是展示某个函数或者方法的特定使用方法。为了判断测试是成功还是失败,需要将程序最终的输出和示例函数底部列出的输出做比较
如果启动一个本地的godoc服务器(godoc -http=":3000"),并找到handlers包,就能看到包含示例的文档,
由于这个示例也是测试的一部分,可以使用go test工具来运行这个示例函数
运行测试后,可以看到测试通过了。这次运行测试时,使用-run选项指定了特定的函数ExampleSendJSON。-run选项接受任意的正则表达式,来过滤要运行的测试函数。这个选项既支持单元测试,也支持示例函数。如果示例运行失败
基准测试
基准测试是一种测试代码性能的方法。想要测试解决同一问题的不同方案的性能,以及查看哪种解决方案的性能更好时,基准测试就会很有用。基准测试也可以用来识别某段代码的CPU或者内存效率问题,而这段代码的效率可能会严重影响整个应用程序的性能。许多开发人员会用基准测试来测试不同的并发模式,或者用基准测试来辅助配置工作池的数量,以保证能最大化系统的吞吐量。
01 // 用来检测要将整数值转为字符串,使用哪个函数会更好的基准
02 // 测试示例。先使用fmt.Sprintf函数,然后使用
03 // strconv.FormatInt函数,最后使用strconv.Itoa
04 package listing28_test
05
06 import (
07 "fmt"
08 "strconv"
09 "testing"
10 )
11
12 // BenchmarkSprintf对fmt.Sprintf函数
13 // 进行基准测试
14 func BenchmarkSprintf(b *testing.B) {
15 number := 10
16
17 b.ResetTimer()
18
19 for i := 0; i < b.N; i++ {
20 fmt.Sprintf("%d", number)
21 }
22 }
go test -v -run="none" -bench="BenchmarkSprintf"
在这次go test调用里,我们给-run选项传递了字符串"none",来保证在运行制订的基准测试函数之前没有单元测试会被运行。这两个选项都可以接受正则表达式,来决定需要运行哪些测试。由于例子里没有单元测试函数的名字中有none,所以使用none可以排除所有的单元测试。
最后,运行基准测试输出了ok,表明基准测试正常结束。之后显示的是被执行的代码文件的名字。最后,输出运行基准测试总共消耗的时间。默认情况下,基准测试的最小运行时间是1 秒。你会看到这个测试框架持续运行了大约1.5秒。如果想让运行时间更长,可以使用另一个名为-benchtime的选项来更改测试执行的最短时间。
我们之前一直没有提到这3个基准测试里面调用b.ResetTimer的作用。在代码开始执行循环之前需要进行初始化时,这个方法用来重置计时器,保证测试代码执行前的初始化代码,不会干扰计时器的结果。为了保证得到的测试结果尽量精确,需要使用这个函数来跳过初始化代码的执行时间。
让这3个函数至少运行3秒后,我们得到图9-16所示的结果。
图9-16 运行所有3个基准测试
这个结果展示了BenchmarkFormat测试函数运行的速度最快,每次操作耗时45.9纳秒。紧随其后的是BenchmarkItoa,每次操作耗时49.4 ns。这两个函数的性能都比Sprintf函数快得多。
运行基准测试时,另一个很有用的选项是-benchmem选项。这个选项可以提供每次操作分配内存的次数,以及总共分配内存的字节数。