200 lines
4.7 KiB
Markdown
200 lines
4.7 KiB
Markdown
# 第 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` 验证。
|