# 第 2 章 Go 进阶:并发与工程化 > 学完本章你将:用 goroutine/channel 写并发、用 context 控制生命周期、用 sync 解决共享状态、会写测试并掌握常用工具链。 ## 2.1 goroutine 轻量线程 Go 并发的最小单元是 `goroutine`,由运行时调度,创建成本约几 KB。 ```go // 启动一个 goroutine go func() { fmt.Println("in goroutine") }() // 本项目 cmd/server/main.go 启动 HTTP 服务就是 goroutine go func() { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("listen failed: %v", err) } }() ``` 主 goroutine 结束则程序退出,所以 `main` 中用 `signal.Notify` 阻塞等待退出信号。 ## 2.2 channel 通道 channel 是 goroutine 间通信的管道,语义是“不要通过共享内存来通信,通过通信来共享内存”。 ```go // 无缓冲:发送阻塞直到有人接收 ch := make(chan string) go func() { ch <- "pong" }() msg := <-ch // 接收 // 有缓冲 ch2 := make(chan int, 2) ch2 <- 1 ch2 <- 2 // ch2 <- 3 // 阻塞,缓冲满 // 关闭与遍历 close(ch2) for v := range ch2 { fmt.Println(v) } // 单向 channel(函数签名约束) func producer(ch chan<- int) { ch <- 42 } // 只发 func consumer(ch <-chan int) { fmt.Println(<-ch) } // 只收 // select 多路复用 select { case v := <-ch: fmt.Println(v) case <-time.After(2 * time.Second): fmt.Println("timeout") } ``` 典型模式:用 channel 做信号通知,本项目 `quit := make(chan os.Signal, 1)` 就是 buffered channel 接收系统信号。 ## 2.3 context 上下文 `context` 用于传递取消信号、超时和请求级数据,Gin 的 `c.Request.Context()` 即标准 context。 ```go // 超时控制 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() // 本项目优雅关闭 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() srv.Shutdown(ctx) // 取消 ctx, cancel := context.WithCancel(context.Background()) go func() { <-ctx.Done() fmt.Println("canceled:", ctx.Err()) }() cancel() // 传值(仅放请求级数据如 traceID,不要放业务大对象) ctx = context.WithValue(ctx, "userID", 123) ``` 规则:context 作为函数第一个参数,命名 `ctx`,不要存入结构体。 ## 2.4 sync 同步原语 ### Mutex 互斥锁 ```go var mu sync.Mutex var count int mu.Lock() count++ mu.Unlock() // RWMutex 读写锁 var rw sync.RWMutex rw.RLock(); v := data; rw.RUnlock() rw.Lock(); data = newVal; rw.Unlock() ``` ### WaitGroup 等待组 ```go var wg sync.WaitGroup for i := 0; i < 3; i++ { wg.Add(1) go func(n int) { defer wg.Done() fmt.Println(n) }(i) } wg.Wait() ``` ### Once 单次执行 ```go var once sync.Once var inst *Service once.Do(func() { inst = &Service{} }) ``` 本项目 `logger.Init` 虽未用 Once,但生产中常用来保证日志单例初始化一次。 ## 2.5 并发模式示例 ```go // Worker Pool jobs := make(chan int, 10) results := make(chan int, 10) for w := 0; w < 3; w++ { go func() { for j := range jobs { results <- j * 2 } }() } for i := 0; i < 5; i++ { jobs <- i } close(jobs) ``` ## 2.6 错误与 panic 边界 - 业务错误用 `error` 返回 - 不可恢复错误才 `panic`,并在最外层 `recover`(见 `middleware/recovery.go`) - goroutine 内的 panic 不会被外层 recover 捕获,需在 goroutine 内部 recover ## 2.7 测试 ```go // ping_test.go package service import "testing" func TestPing(t *testing.T) { svc := NewPingService(nil) if got := svc.Ping(); got != "pong" { t.Fatalf("want pong got %s", got) } } // 表驱动测试 func TestAdd(t *testing.T) { cases := []struct{ a, b, want int }{{1,2,3},{0,0,0}} for _, c := range cases { if got := Add(c.a, c.b); got != c.want { t.Errorf("Add(%d,%d)=%d want %d", c.a, c.b, got, c.want) } } } ``` ```bash go test ./... go test -run TestPing -v ./internal/service go test -race ./... # 检测数据竞争 go test -cover ./... # 覆盖率 ``` ## 2.8 工具链 | 命令 | 用途 | |------|------| | `go fmt ./...` | 格式化,对应 `make fmt` | | `go vet ./...` | 静态检查 | | `golangci-lint run` | 综合 lint(需安装) | | `go mod tidy` | 整理依赖,对应 `make tidy` | | `go run ./cmd/server` | 运行,对应 `make run` | | `go build -o bin/streambox ./cmd/server` | 编译,对应 `make build` | ## 2.9 动手练习 1. 用 goroutine + channel 实现并发求 1..100 的和,对比串行版本。 2. 用 `context.WithTimeout` 实现一个 2 秒超时的 HTTP 请求。 3. 给 `internal/service/ping.go` 补充单测并用 `go test -race` 验证。