【go语言圣经1.6】
目标
学习如何利用 Go 语言的 goroutine 和 channel 实现并发网络请求;
理解如何使用 io.Copy 进行流式数据传输,从而避免一次性申请大缓冲区;
掌握如何通过时间测量和错误传递来构建一个健壮、并发友好的网络请求程序。
总之学习Go 语言并发编程的基础,为探索更复杂的并发场景和高级特性打基础。
概念
要点(案例)
这段代码实现了一个并发网络请求程序,其核心目标是同时发起多个 HTTP 请求,并在所有请求结束后报告每个请求的响应大小以及耗时。整体设计思路包括:
- 并发执行:利用 Go 的 goroutine 机制同时发起多个网络请求,使程序总执行时间接近单个耗时最长的请求,而不是所有请求时间的总和。
- 结果同步:通过 channel 在各个 goroutine 与主函数之间传递结果,确保最后输出时不会出现输出交错的问题。
- 性能监控:通过记录程序开始时间和每个请求的耗时,直观地展示并发请求的效率。
-
主函数
func main() { start := time.Now() // 记录程序开始时间 ch := make(chan string) // 建一个用于传递字符串的channel // 针对命令行传入的每个URL,启动一个新的goroutine并发执行fetch函数 for _,url := range os.Args[1:] { go fetch(url, ch) // 使用go关键字启动goroutine } // 循环接收所有goroutine传回的结果,保证每个请求都被处理 for range os.Args[1:] { fmt.Println(<-ch) // 从channel中接收结果并打印 } // 打印整个程序的耗时,单位为秒 fmt.Printf("%.2fs elapsed\n", time.Since(start).Seconds()) }
- goroutine:
- 是 Go 语言中轻量级的并发执行单元,可以理解为比线程更小的执行体。启动一个 goroutine 非常简单,只需在函数调用前加上
go
关键字。go fetch(url, ch)
表示为每个 URL 启动一个新的并发执行实例,彼此之间互不干扰。
- 是 Go 语言中轻量级的并发执行单元,可以理解为比线程更小的执行体。启动一个 goroutine 非常简单,只需在函数调用前加上
- channel:
- 是一种用于不同 goroutine 之间通信的工具,通过发送和接收数据来进行同步。
- 本例中创建了一个
chan string
,用于在各个并发任务与主函数之间传递结果。 - 发送(
ch <- ...
)和接收(<-ch
)操作都是阻塞的,保证数据在通信过程中不会丢失或混乱。
- goroutine:
-
并发函数fetch
func fetch(url string, ch chan<- string) { start := time.Now() // 发起HTTP GET请求 resp, err := http.Get(url) if err != nil { ch <- fmt.Sprint(err) // 出错时,将错误信息发送到channel中 return } // 使用 io.Copy 将响应体拷贝到 ioutil.Discard(垃圾桶),只关心字节数 nbytes, err := io.Copy(ioutil.Discard, resp.Body) resp.Body.Close() // 关闭响应体,防止资源泄露 if err != nil { ch <- fmt.Sprintf("while reading %s: %v", url, err) return } // 计算本次请求的耗时 secs := time.Since(start).Seconds() // 将(耗时、字节数、URL)发送到channel中 ch <- fmt.Sprintf("%.2fs %7d %s", secs, nbytes, url) }
- ioutil.Discard:其实是一个实现了
io.Writer
接口的对象,它会丢弃写入的数据,相当于一个“黑洞”。这样做的目的是:我们只关心数据传输的字节数,而不需要保存数据本身。 - 与一次性将所有数据读取到一个缓冲区相比,
io.Copy
会采用分块(chunk)的方式读取数据,这样能有效降低内存使用,尤其是在响应体较大的情况下。 - 将耗时、读取的字节数以及 URL 格式化为字符串,通过 channel 发送回主函数。这种方式使得各个并发任务的输出能在主函数中统一处理,避免多个 goroutine 同时输出导致的混乱。
- ioutil.Discard:其实是一个实现了
语言特性
- goroutine 的并发执行
- 每个
go fetch(url, ch)
都会在一个独立的执行流中运行,不会阻塞主函数。这意味着所有网络请求是同时进行的,程序总执行时间受限于最慢的请求,而不是所有请求的累加时间。
- channel 的阻塞特性
- 发送阻塞:当一个 goroutine 试图发送数据到 channel 而没有接收者时,它会阻塞,直到有其他 goroutine 从 channel 接收数据。
- 接收阻塞:同理,接收操作在 channel 为空时会阻塞,直到有数据可取。
- 在本例中,主函数通过循环接收(
<-ch
)确保每个 fetch 的结果都被正确处理。这样既实现了任务同步,又避免了输出顺序混乱。
- 错误传递
- 每个 fetch 中的错误都会通过 channel 传递到主函数,这种方式使得错误处理集中且易于管理
总结与反思
-
并发执行实例:
指的是程序中可以同时运行的多个任务或执行单元。这些任务可以独立执行,共享程序的资源,并在某种程度上并行或交替运行。
-
goroutine:
是 Go 语言中用于实现并发执行的核心概念。它是一种轻量级的执行单元,由 Go 运行时调度管理,相比于传统的线程开销更低。每个 goroutine 都可以看作是一个并发执行实例,它们共同构成了程序的并发部分。
-
每个通过
go
关键字启动的函数调用就是一个 goroutine,也就是一个并发执行实例。Go 语言利用 goroutine 提供了简单而高效的并发模型,让你可以轻松启动成百上千的并发任务,而不必担心传统线程所带来的高昂开销。