Go 语言入门:用一个并发小工具理解 goroutine 与 channel
刚接触 Go 的开发者常会问:这门语言到底“简单”在哪里?答案不在语法糖的多少,而在它把并发做成了语言的一部分。本文用一个真实可跑的小工具——并发批量检查网址状态——带你走完 Go 入门最关键的几步:环境与模块管理、基本语法、goroutine 与 channel、错误处理。读完之后,你能独立写出并运行这个小工具,也能把同样的思路用到自己的日常脚本里。
一、准备工作:安装与第一个模块
先到 Go 官网下载对应系统的安装包,安装完成后在终端执行 go version,能看到版本号就说明环境就绪。Go 从 1.11 起用模块(module)管理依赖,写小工具不需要复杂的工程结构,新建一个空目录并执行:
mkdir urlcheck && cd urlcheck
go mod init urlcheck
go mod init 会生成 go.mod 文件,记录模块名和 Go 版本。之后所有代码都放在这个目录下,go run . 就能直接运行,go build -o urlcheck . 则会编译出可执行文件。养成一个习惯:任何一个新想法,先建模块、再写代码,依赖和版本从一开始就是清晰的。
二、从串行到并发:为什么需要 goroutine
假设要检查十个网址是否可达,最直觉的写法是 for 循环逐个请求:
for _, u := range urls {
resp, err := http.Get(u)
// 处理结果
}
串行的问题很直观:每个请求都要等上一个完成,十个网址各耗时一秒,总耗时就是十秒。而这些请求之间互不依赖,完全可以同时进行。Go 的解法是 goroutine——由 Go 运行时调度的轻量级线程,启动它只需要一个 go 关键字:
go check(url)
一个 goroutine 的初始栈只有几 KB,创建成千上万个的成本也远低于操作系统线程。主函数 main 结束时程序会直接退出,所以必须有办法等所有 goroutine 跑完,这就要用到 sync.WaitGroup。
三、用 channel 收集结果
并发跑起来之后,下一个问题是结果怎么汇总。直接让多个 goroutine 写同一个变量会有数据竞争,Go 推荐的做法是 channel:goroutine 把结果“发”进通道,主函数再从通道里“收”。
type Result struct {
URL string
Status int
Err error
}
results := make(chan Result, len(urls))
带缓冲的 channel 可以让发送方不必等待接收方,配合 WaitGroup 就能写出清晰的模式:启动 N 个 goroutine 做事,每个完成时调用 wg.Done(),另起一个 goroutine 等 wg.Wait() 返回后关闭 channel,主函数用 for r := range results 逐条消费。这种“生产者—通道—消费者”的结构,是 Go 并发程序最常见的骨架,学会它就等于掌握了 Go 并发的一半。
四、完整代码:并发网址检查器
把上面的思路拼起来,完整程序不到一百行:
package main
import (
"fmt"
"net/http"
"sync"
"time"
)
type Result struct {
URL string
Status int
Err error
}
func check(url string, wg *sync.WaitGroup, ch chan<- Result) {
defer wg.Done()
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Get(url)
if err != nil {
ch <- Result{URL: url, Err: err}
return
}
defer resp.Body.Close()
ch <- Result{URL: url, Status: resp.StatusCode}
}
func main() {
urls := []string{
"https://www.huaweicloud.com",
"https://developer.huaweicloud.com",
"https://go.dev",
}
ch := make(chan Result, len(urls))
var wg sync.WaitGroup
for _, u := range urls {
wg.Add(1)
go check(u, &wg, ch)
}
go func() {
wg.Wait()
close(ch)
}()
for r := range ch {
if r.Err != nil {
fmt.Printf("%s 失败:%v\n", r.URL, r.Err)
} else {
fmt.Printf("%s 状态码:%d\n", r.URL, r.Status)
}
}
}
运行 go run .,三个网址的检查几乎同时完成。把列表换成你真正关心的服务地址,它就是一个能用的健康检查小工具。
五、写并发代码最容易踩的四个坑
第一,忘记设置超时。没有 Timeout 的 HTTP 客户端可能一直挂起,整个程序被一个慢接口拖死,超时是并发程序的必需品。第二,无限制地启动 goroutine。网址从三个变成三万个时,直接起三万个 goroutine 会耗尽连接和内存,应该用固定数量的 worker 加任务队列来限流。第三,忘记关闭 channel 或者在错误分支里漏发结果,主函数的 range 会永远等下去。第四,在 goroutine 里共享变量而不加锁或不用 channel,go run -race 可以帮你发现这类数据竞争,开发阶段值得常开。
六、接下来学什么
跑通这个小工具之后,建议按这个顺序深入:先学 context,用它实现可取消的超时控制;再学标准库的 net/http 服务端写法,自己写一个返回检查结果的接口;然后学 testing,给 check 函数写单元测试;最后了解 errgroup,它能把 WaitGroup 和错误收集封装得更简洁。这条路上的每一步都有现成的小工具可以练手,边做边学比只看语法书快得多。
Go 的入门门槛确实不高,但“写出能跑的并发程序”和“写出可靠的并发程序”之间隔着超时、限流、错误处理这些工程细节。从一个几十行的小工具开始,把这些细节逐个补齐,就是最扎实的 Go 入门方式。现在就动手试试吧。
- 点赞
- 收藏
- 关注作者
评论(0)