最佳实践
本文档总结了 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包设计原则
- 单一职责:每个包应该有明确的职责
- 高内聚低耦合:包内紧密相关,包间依赖最小
- 避免循环依赖:合理组织包结构
- 使用 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]) // 13. nil 接口和 nil 值
go
var p *Person = nil
var i interface{} = p
if i == nil {
fmt.Println("nil") // 不会执行
}
// i 的类型是 *Person,值是 nil,但接口本身不是 nil4. 方法接收者
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总结
- 遵循 Go 惯例:使用标准库和社区最佳实践
- 保持简单:避免过度设计
- 编写测试:确保代码质量
- 性能考虑:在需要时优化,但不要过早优化
- 持续学习:关注 Go 语言的新特性和最佳实践
下一步
接下来我们将学习:
