导航菜单

工程实践与常见陷阱

Go 的工程实践包括项目结构、依赖管理、测试、错误处理等,以及开发中常见的陷阱。

项目结构

标准项目布局

myapp/
├── cmd/                    # 主应用程序
│   ├── server/            # 服务端入口
│   │   └── main.go
│   └── client/            # 客户端入口
│       └── main.go
├── internal/              # 私有应用和库代码
│   ├── auth/              # 认证模块
│   └── database/          # 数据库模块
├── pkg/                   # 可被外部使用的库
│   └── utils/
├── api/                   # API 定义文件
│   ├── http/              # HTTP API
│   └── grpc/              # gRPC API
├── web/                   # Web 静态资源
├── configs/               # 配置文件
├── scripts/               # 构建、安装、分析脚本
├── test/                  # 额外的测试数据和工具
├── docs/                  # 设计文档
├── go.mod
└── go.sum

模块划分原则

  • cmd/:每个子目录一个可执行程序
  • internal/:私有代码,外部无法导入
  • pkg/:可复用的库代码
  • api/:协议定义(OpenAPI、protobuf 等)

依赖管理

Go Modules(推荐):

# 初始化模块
go mod init github.com/user/myapp

# 添加依赖
go get github.com/gin-gonic/gin

# 添加指定版本
go get github.com/gin-gonic/gin@v1.9.0

# 更新依赖
go get -u ./...
go mod tidy

# 查看依赖
go list -m all

# 下载依赖
go mod download

# 验证依赖
go mod verify

go.mod 示例:

module github.com/user/myapp

go 1.21

require (
    github.com/gin-gonic/gin v1.9.0
    github.com/go-redis/redis/v8 v8.11.5
)

exclude (
    github.com/bad/dependency v1.0.0
)

replace (
    github.com/local/dependency => ../local/dependency
)

错误处理

Error 接口

// error 接口
type error interface {
    Error() string
}

// 创建错误
err := errors.New("something went wrong")

// 格式化错误
err := fmt.Errorf("invalid value: %d", value)

错误包装(Go 1.13+)

// 包装错误(添加上下文)
if err != nil {
    return fmt.Errorf("failed to open file: %w", err)
}

// 解包错误
if errors.Is(err, os.ErrNotExist) {
    // 文件不存在
}

// 类型断言
var pathErr *os.PathError
if errors.As(err, &pathErr) {
    fmt.Println("Path:", pathErr.Path)
}

自定义错误

// 自定义错误类型
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed: %s %s", e.Field, e.Message)
}

// 使用
func Validate(user User) error {
    if user.Name == "" {
        return &ValidationError{
            Field:   "name",
            Message: "required",
        }
    }
    return nil
}

// 处理
if err := Validate(user); err != nil {
    var ve *ValidationError
    if errors.As(err, &ve) {
        fmt.Printf("Field %s: %s\n", ve.Field, ve.Message)
    }
}

Panic vs Error

// ❌ 不要用 panic 处理可预见的错误
func Divide(a, b int) int {
    if b == 0 {
        panic("division by zero")  // 错误
    }
    return a / b
}

// ✅ 使用 error
func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

何时使用 panic:

  • 不可恢复的错误(配置错误)
  • 程序初始化失败
  • 断言失败(开发阶段)

何时使用 error:

  • 可预见的错误(网络、IO、业务逻辑)
  • 需要上层处理的错误

Defer + Recover

func SafeOperation() (err error) {
    defer func() {
        if r := recover(); r != nil {
            // 记录 panic
            log.Printf("panic recovered: %v", r)
            
            // 转换为 error
            err = fmt.Errorf("panic: %v", r)
        }
    }()
    
    // 可能 panic 的代码
    panic("something bad")
}

func main() {
    if err := SafeOperation(); err != nil {
        log.Printf("error: %v", err)
    }
}

测试

单元测试

// calculator_test.go
package main

import "testing"

func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive", 1, 2, 3},
        {"negative", -1, -2, -3},
        {"mixed", 1, -2, -1},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Add(tt.a, tt.b)
            if result != tt.expected {
                t.Errorf("Add(%d, %d) = %d; want %d",
                    tt.a, tt.b, result, tt.expected)
            }
        })
    }
}

表驱动测试

func TestValidate(t *testing.T) {
    tests := []struct {
        name    string
        input   User
        wantErr bool
    }{
        {
            name:    "valid",
            input:   User{Name: "test", Age: 20},
            wantErr: false,
        },
        {
            name:    "empty name",
            input:   User{Name: "", Age: 20},
            wantErr: true,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := Validate(tt.input)
            if (err != nil) != tt.wantErr {
                t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
            }
        })
    }
}

并发测试

func TestCounterConcurrency(t *testing.T) {
    counter := NewCounter()
    iterations := 1000
    
    // 启动多个 goroutine
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for j := 0; j < iterations; j++ {
                counter.Increment()
            }
        }()
    }
    
    wg.Wait()
    
    expected := 10 * iterations
    if counter.Value() != expected {
        t.Errorf("Counter = %d; want %d", counter.Value(), expected)
    }
}

Mock 测试

// 使用接口 mock
type Database interface {
    GetUser(id int) (*User, error)
}

type MockDatabase struct {
    GetUserFunc func(id int) (*User, error)
}

func (m *MockDatabase) GetUser(id int) (*User, error) {
    return m.GetUserFunc(id)
}

func TestService(t *testing.T) {
    mockDB := &MockDatabase{
        GetUserFunc: func(id int) (*User, error) {
            return &User{Name: "test"}, nil
        },
    }
    
    service := NewService(mockDB)
    user, err := service.GetUser(1)
    
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    
    if user.Name != "test" {
        t.Errorf("got name %s; want test", user.Name)
    }
}

常见陷阱

1. nil 切片 vs 空切片

var s1 []int              // nil 切片
s2 := []int{}            // 空切片
s3 := make([]int, 0)     // 空

// len(s1) == len(s2) == 0
// s1 == nil, s2 != nil, s3 != nil

// JSON 序列化
json.Marshal(s1) // null
json.Marshal(s2) // []

2. range 迭代变量

// ❌ 错误:迭代变量被复用
funcs := []func(){}
for _, v := range []int{1, 2, 3} {
    funcs = append(funcs, func() {
        fmt.Println(v)  // 所有函数都输出 3
    })
}

// ✅ 正确:创建局部变量
funcs := []func(){}
for _, v := range []int{1, 2, 3} {
    val := v  // 局部变量
    funcs = append(funcs, func() {
        fmt.Println(val)
    })
}

3. goroutine 循环变量

// ❌ 错误:所有 goroutine 使用最后一个值
for i := 0; i < 3; i++ {
    go func() {
        fmt.Println(i)  // 输出 3, 3, 3
    }()
}

// ✅ 正确:传递参数
for i := 0; i < 3; i++ {
    go func(n int) {
        fmt.Println(n)  // 输出 0, 1, 2
    }(i)
}

4. channel 关闭

// ❌ 错误:关闭已关闭的 channel
var ch chan int
close(ch)
close(ch)  // panic

// ✅ 正确:使用 sync.Once
var once sync.Once
once.Do(func() {
    close(ch)
})

5. defer 执行顺序

// defer 是 LIFO(后进先出)
func example() {
    defer fmt.Println(1)
    defer fmt.Println(2)
    defer fmt.Println(3)
    // 输出:3, 2, 1
}

6. slice 扩容

// 切片扩容会创建新数组
s1 := []int{1, 2, 3}
s2 := s1[:2]
s2 = append(s2, 99)  // 不影响 s1

s1 = append(s1, 4)   // 可能触发扩容,创建新数组

7. Map 并发读写

// ❌ 错误:Map 并发不安全
var m = make(map[string]int)
go func() { m["key"] = 1 }()
go func() { _ = m["key"] }()  // panic

// ✅ 正确:使用 sync.Map 或加锁
var mu sync.Mutex
var m = make(map[string]int)
go func() {
    mu.Lock()
    m["key"] = 1
    mu.Unlock()
}()
go func() {
    mu.Lock()
    _ = m["key"]
    mu.Unlock()
}()

8. interface nil

// 返回 nil interface
func returnsError() error {
    var p *MyError = nil
    return p  // 返回的是 nil 指针,不是 nil interface
}

// 检查
err := returnsError()
fmt.Println(err == nil)  // false
fmt.Println(err)         // <nil>

最佳实践

1. 错误处理

// ✅ 尽早处理错误
func Process() error {
    data, err := fetchData()
    if err != nil {
        return fmt.Errorf("fetch data: %w", err)
    }
    
    result, err := process(data)
    if err != nil {
        return fmt.Errorf("process: %w", err)
    }
    
    return save(result)
}

2. 资源清理

// ✅ 使用 defer 确保资源释放
func Process(filename string) error {
    f, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer f.Close()
    
    // 处理文件
    return nil
}

3. 并发安全

// ✅ 使用 channel 通信
type Counter struct {
    ch chan func()
}

func NewCounter() *Counter {
    c := &Counter{ch: make(chan func())}
    go c.run()
    return c
}

func (c *Counter) run() {
    count := 0
    for f := range c.ch {
        f()
    }
}

func (c *Counter) Increment() {
    c.ch <- func() {
        count++
    }
}

4. 配置管理

// ✅ 使用结构体配置
type Config struct {
    Host     string
    Port     int
    Timeout  time.Duration
}

func NewConfig() *Config {
    return &Config{
        Host:    "localhost",
        Port:    8080,
        Timeout: 30 * time.Second,
    }
}

// 支持环境变量覆盖
func (c *Config) LoadFromEnv() {
    if host := os.Getenv("HOST"); host != "" {
        c.Host = host
    }
    // ...
}

常见面试题

1. Go 项目如何组织?

  • cmd/:主程序入口
  • internal/:私有代码
  • pkg/:可复用库
  • api/:API 定义
  • 使用 go modules 管理依赖

2. 错误处理最佳实践?

  • 尽早处理错误
  • 添加上下文(fmt.Errorf + %w)
  • 自定义错误类型
  • panic 只用于不可恢复的错误
  • 使用 errors.Is 和 errors.As 检查错误

3. 如何编写可测试的代码?

  • 使用接口解耦依赖
  • 依赖注入
  • 避免全局状态
  • 小函数、单一职责
  • 表驱动测试

4. 常见的 Go 陷阱?

  • nil 切片 vs 空切片
  • range 迭代变量复用
  • goroutine 循环变量捕获
  • channel 关闭 panic
  • map 并发读写

练习题

  1. 实现一个带超时的 HTTP 客户端
  2. 实现一个简单的日志库(支持日志级别)
  3. 编写一个线程安全的缓存(LRU)
  4. 实现一个简单的 worker pool

搜索