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

200 lines
4.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 第 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` 验证。