StreamBox/docs/02-go-进阶-并发与工程化.md

4.7 KiB
Raw Blame History

第 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 动手练习

  1. 用 goroutine + channel 实现并发求 1..100 的和,对比串行版本。
  2. 用 context.WithTimeout 实现一个 2 秒超时的 HTTP 请求。
  3. 给 internal/service/ping.go 补充单测并用 go test -race 验证。