保留错误链
var ErrNotFound = errors.New("order not found")
order, err := repo.Find(ctx, id)
if err != nil {
return Order{}, fmt.Errorf(
"find order %s: %w", id, err,
)
}
if errors.Is(err, ErrNotFound) { ... }
Go 的价值不只是语法简单,而是把编译、依赖、并发、工具链与部署收敛成一套可预测的工程体验。先看清边界,再进入细节。
少做隐式魔法,
多做清晰选择。
显式错误、结构化并发、组合优于继承、小接口、快速构建、单二进制交付。
Go 优先优化的是团队长期维护的总成本,而不是单段代码的炫技空间。
赋值、传参和返回值默认都发生复制。指针不是“更快”的默认答案,它表达的是共享身份、可变性或避免大对象复制。
type Counter struct { n int }
// 值接收者读取副本;指针接收者修改共享对象。
func (c Counter) Value() int { return c.n }
func (c *Counter) Inc() { c.n++ }
c := Counter{}
c.Inc() // 编译器自动取地址
fmt.Println(c.Value())
// 接口由使用者定义,靠近消费端。
type OrderFinder interface {
Find(ctx context.Context, id string) (Order, error)
}
type Service struct { orders OrderFinder }
func (s Service) Get(ctx context.Context, id string) (Order, error) {
return s.orders.Find(ctx, id)
}
func Map[S ~[]E, E any, R any](src S, fn func(E) R) []R {
out := make([]R, len(src))
for i, item := range src {
out[i] = fn(item)
}
return out
}
names := Map(users, func(u User) string { return u.Name })
复制便宜,所有权清晰,减少别名。
方法要改变接收者可观察状态。
先考虑零值、额外布尔值或 Option 结构,再用裸指针。
由使用者定义,只包含当前调用方真正需要的方法。
类型差异是唯一变化;若行为不同,接口更清晰。
点击选项,检验值类型与指针类型的方法集。
type Flusher interface { Flush() error }func (*Buffer) Flush() error选择一个答案
错误值应该保留因果链,Context 应传递取消与时间预算。两者都不是日志容器,也不应携带可选业务参数。
var ErrNotFound = errors.New("order not found")
order, err := repo.Find(ctx, id)
if err != nil {
return Order{}, fmt.Errorf(
"find order %s: %w", id, err,
)
}
if errors.Is(err, ErrNotFound) { ... }
defer cancel()。ctx.Done()。记录后再返回同一个错误,会让每一层都打印一遍。选择负责处理错误的边界记录一次,并附带 trace 与业务上下文。
并发设计的核心是生命周期:谁启动、谁等待、谁取消、谁关闭 channel、积压时怎么办。先建立边界,再追求吞吐。
原因
错误、取消、等待形成一个结构化边界。
状态归属清晰时,比 channel 更直接。
让生产者与消费者在通信点同步。
并发安全地执行一次初始化。
保护下游、连接池或外部配额。
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(8)
for _, id := range ids {
id := id
g.Go(func() error {
return syncOne(ctx, id)
})
}
if err := g.Wait(); err != nil {
return fmt.Errorf("sync batch: %w", err)
}
每个 goroutine 在创建时都要能回答:它在哪些条件下退出?谁能触发退出?调用方是否等待它结束?
Go runtime 在用户代码与操作系统之间管理调度、栈、内存与垃圾回收。理解它不是为了手动控制,而是为了正确解释性能现象。
大量 goroutine 可行,不代表免费。栈、调度元数据、闭包捕获和任务积压仍会消耗内存。
值的生命周期超出当前栈帧或尺寸不适合栈时会进入堆。用 -gcflags=-m 验证,不靠猜。
持续小对象分配会增加 GC CPU。先看 alloc_space、alloc_objects 与 live heap,再决定优化。
字段顺序会影响填充;连续切片通常比指针链更利于 CPU cache,但清晰性仍是第一约束。
return &User{...}可能逃逸返回后仍需存活,但编译器也可能做内联与更精确分析。
fmt.Println(v)可能逃逸接口装箱与调用路径可能让值进入堆,需看实际报告。
make([]byte, n)取决于 n尺寸是否在编译期已知、是否过大、是否跨函数存活都会影响结果。
高价值测试围绕输入、输出和可观察副作用组织。表驱动测试覆盖业务矩阵,race detector 找共享内存错误,fuzz 探索你没想到的输入空间。
tests := []struct {
name string
input string
want int64
wantErr bool
}{
{"yuan", "12.30", 1230, false},
{"invalid", "12.x", 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { ... })
}
func FuzzParseAmount(f *testing.F) {
f.Add("12.30")
f.Fuzz(func(t *testing.T, input string) {
cents, err := ParseAmount(input)
if err == nil && cents < 0 {
t.Fatalf("negative result: %d", cents)
}
})
}
func BenchmarkDecode(b *testing.B) {
payload := loadFixture(b)
b.ReportAllocs()
for b.Loop() {
_, err := Decode(payload)
if err != nil { b.Fatal(err) }
}
}
go test ./...go test -race ./...go test -fuzz=FuzzParse -fuzztime=30sgo test -bench=. -benchmem性能诊断从症状和 SLO 开始,不从代码直觉开始。CPU、内存、阻塞、锁与调度各有证据入口,先选对 profile。
定位 CPU 时间花在哪里,关注 flat 与 cumulative 排名,并结合调用图确认热点来自自身还是下游调用路径。
go tool pprof -http=:0 cpu.pprof
哪个 SLO 变坏?范围、时段、版本、流量是否变化?
对比正常窗口,确认 CPU、内存、GC、队列和下游变化。
选择 profile、trace 或 benchmark,避免同时改多个变量。
复测吞吐、P99、资源与正确性,记录适用边界。
算法与 I/O → 不必要工作 → 分配与数据布局 → 锁竞争 → 微小语法差异。越靠后,收益通常越依赖场景。
生产质量不仅是“没有 panic”。服务必须有边界、预算、健康信号、优雅退出和可复现的构建,故障时还要留下足够证据。
勾选生产清单后完成本章。下一轮学习请带着真实服务的 profile、故障记录或代码边界回来复盘。