golang-idioms
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseGo Idioms
Go语言惯用模式
Error Handling
错误处理
go
// Return errors, never panic in library code
func LoadConfig(path string) (Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return Config{}, fmt.Errorf("reading config %s: %w", path, err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return Config{}, fmt.Errorf("parsing config: %w", err)
}
return cfg, nil
}Rules:
- Always wrap errors with context using
fmt.Errorf("context: %w", err) - Use to allow callers to use
%wanderrors.Iserrors.As - Handle errors at the appropriate level; do not log and return the same error
- Define sentinel errors for expected conditions
go
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
)
func GetUser(id string) (User, error) {
user, ok := store[id]
if !ok {
return User{}, fmt.Errorf("user %s: %w", id, ErrNotFound)
}
return user, nil
}
// Caller
user, err := GetUser(id)
if errors.Is(err, ErrNotFound) {
http.Error(w, "user not found", http.StatusNotFound)
return
}go
// Return errors, never panic in library code
func LoadConfig(path string) (Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return Config{}, fmt.Errorf("reading config %s: %w", path, err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return Config{}, fmt.Errorf("parsing config: %w", err)
}
return cfg, nil
}规则:
- 始终使用为错误添加上下文信息
fmt.Errorf("context: %w", err) - 使用允许调用方使用
%w和errors.Is判断错误类型errors.As - 在合适的层级处理错误;不要既记录日志又返回同一个错误
- 为预期的错误场景定义哨兵错误
go
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
)
func GetUser(id string) (User, error) {
user, ok := store[id]
if !ok {
return User{}, fmt.Errorf("user %s: %w", id, ErrNotFound)
}
return user, nil
}
// Caller
user, err := GetUser(id)
if errors.Is(err, ErrNotFound) {
http.Error(w, "user not found", http.StatusNotFound)
return
}Interface Design
接口设计
go
// Keep interfaces small (1-3 methods)
type Reader interface {
Read(p []byte) (n int, err error)
}
type UserStore interface {
GetUser(ctx context.Context, id string) (User, error)
CreateUser(ctx context.Context, u User) error
}
// Accept interfaces, return structs
func NewService(store UserStore, logger *slog.Logger) *Service {
return &Service{store: store, logger: logger}
}Rules:
- Define interfaces where they are used (consumer side), not where they are implemented
- Prefer small, composable interfaces over large ones
- Use ,
io.Reader,io.Writerfrom the standard libraryfmt.Stringer - An interface with one method should be named after the method + suffix
er
go
// Keep interfaces small (1-3 methods)
type Reader interface {
Read(p []byte) (n int, err error)
}
type UserStore interface {
GetUser(ctx context.Context, id string) (User, error)
CreateUser(ctx context.Context, u User) error
}
// Accept interfaces, return structs
func NewService(store UserStore, logger *slog.Logger) *Service {
return &Service{store: store, logger: logger}
}规则:
- 在接口的使用方(消费者侧)定义接口,而非实现方
- 优先选择小型、可组合的接口,而非大型接口
- 使用标准库中的、
io.Reader、io.Writer等接口fmt.Stringer - 只有一个方法的接口,命名应采用“方法名+er”后缀
Goroutine and Channel Patterns
Goroutine与Channel模式
Worker Pool
工作池
go
func process(ctx context.Context, jobs <-chan Job, workers int) <-chan Result {
results := make(chan Result, workers)
var wg sync.WaitGroup
for range workers {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
select {
case <-ctx.Done():
return
case results <- job.Execute():
}
}
}()
}
go func() {
wg.Wait()
close(results)
}()
return results
}go
func process(ctx context.Context, jobs <-chan Job, workers int) <-chan Result {
results := make(chan Result, workers)
var wg sync.WaitGroup
for range workers {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
select {
case <-ctx.Done():
return
case results <- job.Execute():
}
}
}()
}
go func() {
wg.Wait()
close(results)
}()
return results
}Fan-out/Fan-in
扇出/扇入
go
func fanOut[T, R any](ctx context.Context, items []T, fn func(T) R, concurrency int) []R {
sem := make(chan struct{}, concurrency)
results := make([]R, len(items))
var wg sync.WaitGroup
for i, item := range items {
wg.Add(1)
sem <- struct{}{}
go func() {
defer func() { <-sem; wg.Done() }()
results[i] = fn(item)
}()
}
wg.Wait()
return results
}Rules:
- Always pass as the first parameter
context.Context - Always ensure goroutines can be stopped (via context cancellation or channel close)
- Use to wait for goroutine completion
sync.WaitGroup - Use buffered channels when producer and consumer run at different speeds
- Never start a goroutine without knowing how it will stop
go
func fanOut[T, R any](ctx context.Context, items []T, fn func(T) R, concurrency int) []R {
sem := make(chan struct{}, concurrency)
results := make([]R, len(items))
var wg sync.WaitGroup
for i, item := range items {
wg.Add(1)
sem <- struct{}{}
go func() {
defer func() { <-sem; wg.Done() }()
results[i] = fn(item)
}()
}
wg.Wait()
return results
}规则:
- 始终将作为第一个参数传递
context.Context - 确保Goroutine可以被终止(通过上下文取消或通道关闭)
- 使用等待Goroutine完成
sync.WaitGroup - 当生产者和消费者速度不同时,使用带缓冲的Channel
- 绝不要在不知道如何终止的情况下启动Goroutine
Context Propagation
上下文传递
go
func (s *Service) HandleRequest(ctx context.Context, req Request) (Response, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
user, err := s.store.GetUser(ctx, req.UserID)
if err != nil {
return Response{}, fmt.Errorf("getting user: %w", err)
}
ctx = context.WithValue(ctx, userKey, user)
return s.processRequest(ctx, req)
}Rules:
- Pass context as the first parameter of every function that does I/O
- Use or
context.WithTimeoutfor all external callscontext.WithDeadline - Always after creating a cancellable context
defer cancel() - Use sparingly (request-scoped values only: trace IDs, auth info)
context.WithValue - Never store context in a struct
go
func (s *Service) HandleRequest(ctx context.Context, req Request) (Response, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
user, err := s.store.GetUser(ctx, req.UserID)
if err != nil {
return Response{}, fmt.Errorf("getting user: %w", err)
}
ctx = context.WithValue(ctx, userKey, user)
return s.processRequest(ctx, req)
}规则:
- 所有执行I/O操作的函数,都要将上下文作为第一个参数传递
- 所有外部调用都使用或
context.WithTimeout设置超时context.WithDeadline - 创建可取消的上下文后,始终
defer cancel() - 谨慎使用(仅用于请求作用域的值:跟踪ID、认证信息等)
context.WithValue - 绝不要将上下文存储在结构体中
Table-Driven Tests
表格驱动测试
go
func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
email string
want bool
}{
{"valid email", "user@example.com", true},
{"missing @", "userexample.com", false},
{"empty string", "", false},
{"multiple @", "user@@example.com", false},
{"valid with subdomain", "user@mail.example.com", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ValidateEmail(tt.email)
if got != tt.want {
t.Errorf("ValidateEmail(%q) = %v, want %v", tt.email, got, tt.want)
}
})
}
}go
func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
email string
want bool
}{
{"valid email", "user@example.com", true},
{"missing @", "userexample.com", false},
{"empty string", "", false},
{"multiple @", "user@@example.com", false},
{"valid with subdomain", "user@mail.example.com", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ValidateEmail(tt.email)
if got != tt.want {
t.Errorf("ValidateEmail(%q) = %v, want %v", tt.email, got, tt.want)
}
})
}
}Test Helpers
测试辅助函数
go
func newTestServer(t *testing.T) *httptest.Server {
t.Helper()
handler := setupRoutes()
srv := httptest.NewServer(handler)
t.Cleanup(srv.Close)
return srv
}
func assertEqual[T comparable](t *testing.T, got, want T) {
t.Helper()
if got != want {
t.Errorf("got %v, want %v", got, want)
}
}Use in all test utility functions. Use instead of for test resource cleanup. Use directory for test fixtures.
t.Helper()t.Cleanup()defertestdata/go
func newTestServer(t *testing.T) *httptest.Server {
t.Helper()
handler := setupRoutes()
srv := httptest.NewServer(handler)
t.Cleanup(srv.Close)
return srv
}
func assertEqual[T comparable](t *testing.T, got, want T) {
t.Helper()
if got != want {
t.Errorf("got %v, want %v", got, want)
}
}在所有测试工具函数中使用。使用替代进行测试资源清理。将测试夹具放在目录中。
t.Helper()t.Cleanup()defertestdata/Module Management
模块管理
go.mod structure:
module github.com/org/project
go 1.23
require (
github.com/lib/pq v1.10.9
golang.org/x/sync v0.7.0
)Commands:
bash
go mod tidy # remove unused, add missing
go mod verify # verify checksums
go list -m -u all # check for updates
go get -u ./... # update all dependencies
go mod vendor # vendor dependencies (optional)Use before every commit. Pin major versions. Review changelogs before updating.
go mod tidygo.mod structure:
module github.com/org/project
go 1.23
require (
github.com/lib/pq v1.10.9
golang.org/x/sync v0.7.0
)常用命令:
bash
go mod tidy # remove unused, add missing
go mod verify # verify checksums
go list -m -u all # check for updates
go get -u ./... # update all dependencies
go mod vendor # vendor dependencies (optional)每次提交前都要运行。固定主版本号。更新前查看变更日志。
go mod tidyZero-Value Design
零值设计
Design types so their zero value is useful:
go
// sync.Mutex zero value is an unlocked mutex (ready to use)
var mu sync.Mutex
// bytes.Buffer zero value is an empty buffer (ready to use)
var buf bytes.Buffer
buf.WriteString("hello")
// Custom types: make zero value meaningful
type Server struct {
Addr string // defaults to ""
Handler http.Handler // defaults to nil
Timeout time.Duration // defaults to 0 (no timeout)
}
func (s *Server) ListenAndServe() error {
addr := s.Addr
if addr == "" {
addr = ":8080" // useful default
}
handler := s.Handler
if handler == nil {
handler = http.DefaultServeMux
}
// ...
}Rules:
- Prefer structs with meaningful zero values over constructors
- Use pointer receivers when the method modifies the receiver
- Use value receivers when the method only reads
- Never export fields that users should not set directly; use constructor functions
设计类型时,使其零值具备可用性:
go
// sync.Mutex zero value is an unlocked mutex (ready to use)
var mu sync.Mutex
// bytes.Buffer zero value is an empty buffer (ready to use)
var buf bytes.Buffer
buf.WriteString("hello")
// Custom types: make zero value meaningful
type Server struct {
Addr string // defaults to ""
Handler http.Handler // defaults to nil
Timeout time.Duration // defaults to 0 (no timeout)
}
func (s *Server) ListenAndServe() error {
addr := s.Addr
if addr == "" {
addr = ":8080" // useful default
}
handler := s.Handler
if handler == nil {
handler = http.DefaultServeMux
}
// ...
}规则:
- 优先选择零值有意义的结构体,而非构造函数
- 当方法需要修改接收者时,使用指针接收者
- 当方法仅读取接收者时,使用值接收者
- 绝不要导出用户不应直接设置的字段;使用构造函数
Structured Logging
结构化日志
go
import "log/slog"
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
logger.Info("request handled",
slog.String("method", r.Method),
slog.String("path", r.URL.Path),
slog.Int("status", status),
slog.Duration("latency", time.Since(start)),
)Use (standard library, Go 1.21+). Use structured fields, never string interpolation. Include request ID, user ID, and operation name in every log entry.
log/sloggo
import "log/slog"
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
logger.Info("request handled",
slog.String("method", r.Method),
slog.String("path", r.URL.Path),
slog.Int("status", status),
slog.Duration("latency", time.Since(start)),
)使用(标准库,Go 1.21+)。使用结构化字段,绝不使用字符串拼接。每条日志条目都要包含请求ID、用户ID和操作名称。
log/slogCommon Anti-Patterns
常见反模式
- Returning /
interface{}instead of concrete typesany - Using for complex setup (makes testing hard)
init() - Ignoring errors with without comment
_ - Using goroutines without lifecycle management
- Mutex contention from overly broad lock scope
- Channel misuse: prefer mutexes for simple shared state
- Naked returns in functions longer than a few lines
- 返回/
interface{}而非具体类型any - 使用进行复杂初始化(会增加测试难度)
init() - 无注释的情况下用忽略错误
_ - 启动Goroutine但不管理其生命周期
- 锁范围过宽导致互斥锁竞争
- 错误使用Channel:简单共享状态优先使用互斥锁
- 多行函数中使用裸返回