并发获取多个URL

Go 语言最有意思并且最新奇的特性就是对并发编程的支持。并发编程是一个大话题,在第八章和第九章中会专门讲到。这里我们只浅尝辄止地来体验一下 Go 语言里的 goroutinechannel

下面的例子 fetchall ,和前面小节的 fetch 程序所要做的工作基本一致,fetchall 的特别之处在于它会同时去获取所有的 URL ,所以这个程序的总执行时间不会超过执行时间最长的那一个任务,前面的 fetch 程序执行时间则是所有任务执行时间之和。fetchall 程序只会打印获取的内容大小和经过的时间,不会像之前那样打印获取的内容。

ch1/fetchall
Unresolved include directive in modules/ROOT/pages/ch1/ch1-06.adoc - include::example$/ch1/fetchall/main.go[]

下面使用 fetchall 来请求几个地址:

$ go build gopl.io/modules/fetchall
$ ./fetchall https://golang.org http://gopl.io https://godoc.org
0.14s     6852  https://godoc.org
0.16s     7261  https://golang.org
0.48s     2475  http://gopl.io
0.48s elapsed

goroutine 是一种函数的并发执行方式,而 channel 是用来在 goroutine 之间进行参数传递。main 函数本身也运行在一个 goroutine 中,而 go function 则表示创建一个新的 goroutine ,并在这个新的 goroutine 中执行这个函数。

main 函数中用 make 函数创建了一个传递 string 类型参数的 channel ,对每一个命令行参数,我们都用 go 这个关键字来创建一个 goroutine ,并且让函数在这个 goroutine 异步执行 http.Get 方法。这个程序里的 io.Copy 会把响应的 Body 内容拷贝到 ioutil.Discard 输出流中(译注:可以把这个变量看作一个垃圾桶,可以向里面写一些不需要的数据),因为我们需要这个方法返回的字节数,但是又不想要其内容。每当请求返回内容时,fetch 函数都会往 ch 这个 channel 里写入一个字符串,由 main 函数里的第二个 for 循环来处理并打印 channel 里的这个字符串。

当一个 goroutine 尝试在一个 channel 上做 send 或者 receive 操作时,这个 goroutine 会阻塞在调用处,直到另一个 goroutine 从这个 channel 里接收或者写入值,这样两个 goroutine 才会继续执行 channel 操作之后的逻辑。在这个例子中,每一个 fetch 函数在执行时都会往 channel 里发送一个值( ch <- expression ),主函数负责接收这些值( <-ch )。这个程序中我们用 main 函数来完整地处理/接收所有 fetch 函数传回的字符串,可以避免因为有两个 goroutine 同时完成而使得其输出交错在一起的危险。


练习 1.10: 找一个数据量比较大的网站,用本小节中的程序调研网站的缓存策略,对每个URL执行两遍请求,查看两次时间是否有较大的差别,并且每次获取到的响应内容是否一致,修改本节中的程序,将响应结果输出到文件,以便于进行对比。

练习 1.11: 在 fetchall 中尝试使用长一些的参数列表,比如使用在 alexa.com 的上百万网站里排名靠前的。如果一个网站没有回应,程序将采取怎样的行为?( Section8.9 描述了在这种情况下的应对机制)。