4.7 KiB
4.7 KiB
第 2 章 Go 进阶:并发与工程化
学完本章你将:用 goroutine/channel 写并发、用 context 控制生命周期、用 sync 解决共享状态、会写测试并掌握常用工具链。
2.1 goroutine 轻量线程
Go 并发的最小单元是 goroutine,由运行时调度,创建成本约几 KB。
// 启动一个 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 间通信的管道,语义是“不要通过共享内存来通信,通过通信来共享内存”。
// 无缓冲:发送阻塞直到有人接收
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。
// 超时控制
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 互斥锁
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 等待组
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 单次执行
var once sync.Once
var inst *Service
once.Do(func() { inst = &Service{} })
本项目 logger.Init 虽未用 Once,但生产中常用来保证日志单例初始化一次。
2.5 并发模式示例
// 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 测试
// 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)
}
}
}
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 动手练习
- 用 goroutine + channel 实现并发求 1..100 的和,对比串行版本。
- 用
context.WithTimeout实现一个 2 秒超时的 HTTP 请求。 - 给
internal/service/ping.go补充单测并用go test -race验证。