Skip to content

最佳实践

本文档总结了 Go 语言开发的最佳实践,帮助你写出高质量、可维护的 Go 代码。

代码规范

命名规范

包名

go
// 好的做法:简短、小写、单数
package user
package http

// 不好的做法
package users
package HTTP

函数和变量

go
// 公开函数:首字母大写
func GetUser(id int) *User {}

// 私有函数:首字母小写
func getUser(id int) *User {}

// 变量命名:驼峰式
var userName string
var maxRetries int

常量

go
// 使用常量组
const (
    StatusOK       = 200
    StatusNotFound = 404
)

// 或者使用 iota
const (
    StatusOK = iota
    StatusNotFound
    StatusInternalError
)

代码格式

使用 go fmt 格式化代码:

bash
go fmt ./...

使用 gofumpt 获得更严格的格式:

bash
gofumpt -w .

注释规范

go
// Package user 提供用户相关的功能
package user

// User 表示一个用户
type User struct {
    ID   int
    Name string
}

// GetUser 根据 ID 获取用户
// 如果用户不存在,返回 nil
func GetUser(id int) *User {
    // 实现
}

项目结构

标准项目布局

project/
├── cmd/
│   └── server/
│       └── main.go          # 应用入口
├── internal/
│   ├── config/              # 配置(仅内部使用)
│   ├── handler/             # HTTP 处理器
│   └── service/             # 业务逻辑
├── pkg/
│   └── utils/               # 可复用的包
├── api/                     # API 定义
├── web/                     # Web 资源
├── scripts/                 # 脚本
├── docs/                    # 文档
├── test/                    # 测试数据
├── go.mod
├── go.sum
└── README.md

包设计原则

  1. 单一职责:每个包应该有明确的职责
  2. 高内聚低耦合:包内紧密相关,包间依赖最小
  3. 避免循环依赖:合理组织包结构
  4. 使用 internal 包:限制内部包的可见性

错误处理

错误处理模式

go
// 好的做法:尽早返回错误
func process(data []byte) error {
    if len(data) == 0 {
        return errors.New("数据为空")
    }
    // 继续处理
    return nil
}

// 提供上下文
func readFile(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return fmt.Errorf("打开文件失败 %s: %w", filename, err)
    }
    defer file.Close()
    return nil
}

使用 Sentinel 错误

go
var (
    ErrNotFound    = errors.New("未找到")
    ErrInvalidData = errors.New("无效数据")
)

func findUser(id int) (*User, error) {
    if id <= 0 {
        return nil, ErrInvalidData
    }
    // ...
}

并发编程

使用 Channel 通信

go
// 好的做法:使用 Channel
func worker(jobs <-chan int, results chan<- int) {
    for job := range jobs {
        results <- process(job)
    }
}

// 避免共享内存
var counter int
var mutex sync.Mutex  // 尽量避免

使用 Context

go
// 总是传递 Context
func process(ctx context.Context, data string) error {
    // 检查取消
    if ctx.Err() != nil {
        return ctx.Err()
    }
    // 处理
    return nil
}

避免 Goroutine 泄漏

go
// 好的做法:使用 Context 或 Channel 控制生命周期
func worker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            return
        default:
            // 工作
        }
    }
}

性能优化

1. 避免不必要的内存分配

go
// 不好的做法
func buildString() string {
    var s string
    for i := 0; i < 1000; i++ {
        s += "a"  // 每次分配新内存
    }
    return s
}

// 好的做法
func buildString() string {
    var builder strings.Builder
    for i := 0; i < 1000; i++ {
        builder.WriteString("a")
    }
    return builder.String()
}

2. 预分配切片容量

go
// 好的做法:如果知道大小,预分配
items := make([]Item, 0, 100)
for i := 0; i < 100; i++ {
    items = append(items, Item{})
}

3. 使用 sync.Pool

go
var bufferPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

func getBuffer() *bytes.Buffer {
    return bufferPool.Get().(*bytes.Buffer)
}

func putBuffer(buf *bytes.Buffer) {
    buf.Reset()
    bufferPool.Put(buf)
}

4. 避免不必要的接口

go
// 不好的做法:过度抽象
type Processor interface {
    Process()
}

// 好的做法:只在需要时使用接口
func process(p Processor) {
    p.Process()
}

测试

表驱动测试

go
func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a        int
        b        int
        expected int
    }{
        {"正数", 2, 3, 5},
        {"负数", -2, -3, -5},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Add(tt.a, tt.b)
            if result != tt.expected {
                t.Errorf("期望 %d, 得到 %d", tt.expected, result)
            }
        })
    }
}

测试覆盖率

bash
# 生成覆盖率报告
go test -coverprofile=coverage.out
go tool cover -html=coverage.out

常见陷阱

1. 闭包循环变量

go
// 问题
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)  // 正确
    }(i)
}

2. 切片和数组

go
// 切片是引用类型
slice1 := []int{1, 2, 3}
slice2 := slice1
slice2[0] = 10
fmt.Println(slice1[0])  // 10

// 数组是值类型
arr1 := [3]int{1, 2, 3}
arr2 := arr1
arr2[0] = 10
fmt.Println(arr1[0])  // 1

3. nil 接口和 nil 值

go
var p *Person = nil
var i interface{} = p

if i == nil {
    fmt.Println("nil")  // 不会执行
}

// i 的类型是 *Person,值是 nil,但接口本身不是 nil

4. 方法接收者

go
// 值接收者:不修改原值
func (p Person) SetName(name string) {
    p.Name = name  // 只修改副本
}

// 指针接收者:修改原值
func (p *Person) SetName(name string) {
    p.Name = name  // 修改原值
}

工具推荐

代码检查

bash
# golangci-lint
golangci-lint run

# go vet
go vet ./...

# staticcheck
staticcheck ./...

依赖管理

bash
# 更新依赖
go get -u ./...

# 整理依赖
go mod tidy

# 验证依赖
go mod verify

文档

README.md

每个项目应该有清晰的 README:

markdown
# 项目名称

项目描述

## 安装

```bash
go install github.com/user/project

使用

示例代码

贡献

贡献指南


### 代码文档

使用 `godoc` 生成文档:

```bash
godoc -http=:6060

总结

  1. 遵循 Go 惯例:使用标准库和社区最佳实践
  2. 保持简单:避免过度设计
  3. 编写测试:确保代码质量
  4. 性能考虑:在需要时优化,但不要过早优化
  5. 持续学习:关注 Go 语言的新特性和最佳实践

下一步

接下来我们将学习:

基于 VitePress 构建