29 - 最佳实践:项目布局、代码风格、性能建议、常见坑
29 - 最佳实践
29.1 项目布局
标准布局
myproject/
├── cmd/ # 可执行程序入口
│ ├── server/
│ │ └── main.go
│ └── cli/
│ └── main.go
├── internal/ # 私有代码(不可被外部导入)
│ ├── handler/ # HTTP 处理器
│ ├── service/ # 业务逻辑
│ ├── repository/ # 数据访问
│ ├── model/ # 数据模型
│ └── middleware/ # 中间件
├── pkg/ # 公共库(可被外部导入)
│ ├── logger/
│ └── utils/
├── api/ # API 定义(protobuf、OpenAPI)
│ └── proto/
├── config/ # 配置文件
│ ├── config.go
│ └── config.yaml
├── migrations/ # 数据库迁移
├── scripts/ # 脚本
├── docs/ # 文档
├── testdata/ # 测试数据
├── .github/ # GitHub 配置
│ └── workflows/
├── Dockerfile
├── Makefile
├── go.mod
├── go.sum
└── README.md
💡 技巧:
internal/目录下的包不能被外部项目导入,Go 编译器强制执行cmd/每个子目录对应一个可执行文件- 小项目不需要完整的目录结构,从简单开始
代码分层
// Handler 层:处理 HTTP 请求/响应
// 不包含业务逻辑
type UserHandler struct {
service UserService
}
func (h *UserHandler) GetUser(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
user, err := h.service.GetByID(c.Request.Context(), id)
if err != nil {
c.JSON(404, gin.H{"error": "not found"})
return
}
c.JSON(200, user)
}
// Service 层:业务逻辑
// 不关心数据存储方式
type UserService interface {
GetByID(ctx context.Context, id int) (*User, error)
Create(ctx context.Context, user *User) error
}
type userService struct {
repo UserRepository
}
func (s *userService) GetByID(ctx context.Context, id int) (*User, error) {
user, err := s.repo.FindByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("get user %d: %w", id, err)
}
return user, nil
}
// Repository 层:数据访问
// 不包含业务逻辑
type UserRepository interface {
FindByID(ctx context.Context, id int) (*User, error)
Save(ctx context.Context, user *User) error
}
29.2 代码风格
命名规范
// ✅ 好的命名
type UserService struct { } // 大驼峰(导出)
type userRepository struct { } // 小驼峰(不导出)
func GetUserByID(id int) *User { } // 大驼峰(导出)
func calculateTotal(items []Item) float64 { } // 小驼峰
// 常量
const MaxRetries = 3
const defaultTimeout = 30
// 接口(单方法接口以 -er 结尾)
type Reader interface { Read(p []byte) (n int, err error) }
type UserFinder interface { FindUser(id int) (*User, error) }
// ❌ 避免
var userInfoMap map[int]*User // 不需要加类型后缀
func GetUser() *User // Get 不如直接用名词
错误处理
// ✅ 好:添加上下文
func processOrder(id int) error {
order, err := repo.FindByID(id)
if err != nil {
return fmt.Errorf("find order %d: %w", id, err)
}
if err := validate(order); err != nil {
return fmt.Errorf("validate order %d: %w", id, err)
}
return nil
}
// ❌ 差:没有上下文
func processOrder(id int) error {
order, err := repo.FindByID(id)
if err != nil {
return err // 不知道在哪里失败
}
return nil
}
// ✅ 好:哨兵错误
var ErrUserNotFound = errors.New("user not found")
// ✅ 好:自定义错误类型
type ValidationError struct {
Field string
Message string
}
注释规范
// Package user 提供用户相关的业务逻辑处理。
package user
// UserService 定义了用户服务的接口。
// 它包含用户的 CRUD 操作以及认证相关的功能。
type UserService interface {
// GetByID 根据用户 ID 获取用户信息。
// 如果用户不存在,返回 ErrUserNotFound。
GetByID(ctx context.Context, id int64) (*User, error)
// Create 创建新用户。
// 如果邮箱已存在,返回 ErrEmailExists。
Create(ctx context.Context, user *User) error
}
29.3 性能建议
内存优化
// 1. 预分配切片
func processItems(items []Item) []Result {
results := make([]Result, 0, len(items)) // ✅ 预分配
for _, item := range items {
results = append(results, process(item))
}
return results
}
// 2. 使用 strings.Builder
func buildString(parts []string) string {
var b strings.Builder
totalLen := 0
for _, p := range parts {
totalLen += len(p)
}
b.Grow(totalLen)
for _, p := range parts {
b.WriteString(p)
}
return b.String()
}
// 3. sync.Pool 复用对象
var bufferPool = sync.Pool{
New: func() any { return new(bytes.Buffer) },
}
func processRequest(data []byte) []byte {
buf := bufferPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
bufferPool.Put(buf)
}()
buf.Write(data)
return buf.Bytes()
}
// 4. 避免不必要的指针
type Config struct { // 小结构体用值类型
Port int
Debug bool
Timeout int
}
并发优化
// 1. 限制并发数
func processAll(items []Item) []Result {
maxWorkers := runtime.NumCPU()
sem := make(chan struct{}, maxWorkers)
var wg sync.WaitGroup
results := make([]Result, len(items))
for i, item := range items {
wg.Add(1)
sem <- struct{}{} // 获取信号量
go func(i int, item Item) {
defer func() { <-sem; wg.Done() }()
results[i] = process(item)
}(i, item)
}
wg.Wait()
return results
}
// 2. errgroup 处理并发错误
import "golang.org/x/sync/errgroup"
func fetchAll(ctx context.Context, urls []string) ([]string, error) {
results := make([]string, len(urls))
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(10) // 限制并发数
for i, url := range urls {
i, url := i, url
g.Go(func() error {
data, err := fetch(ctx, url)
if err != nil {
return err
}
results[i] = data
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
29.4 常见陷阱
1. 循环变量捕获
// ❌ Go 1.21- 的陷阱
for _, v := range items {
go func() {
fmt.Println(v) // 所有 goroutine 共享同一个 v
}()
}
// ✅ 修复方案
for _, v := range items {
v := v // 复制一份
go func() {
fmt.Println(v)
}()
}
// ✅ Go 1.22+ 已修复
2. 切片共享底层数组
original := []int{1, 2, 3, 4, 5}
sub := original[1:3] // [2 3]
sub[0] = 999
fmt.Println(original) // [1 999 3 4 5] ← 原数组被修改!
// ✅ 修复:使用三索引
sub := original[1:3:3] // 限制容量
3. map 并发读写
// ❌ 会导致 fatal error
m := make(map[int]int)
go func() { m[1] = 1 }()
go func() { _ = m[1] }()
// ✅ 使用 sync.Map 或 Mutex
4. 接口值比较
var p *int = nil
var i any = p
fmt.Println(i == nil) // false!类型信息非 nil
// ✅ 使用 reflect
fmt.Println(reflect.ValueOf(i).IsNil()) // true
5. defer 参数求值
x := 10
defer fmt.Println(x) // 输出 10(参数在 defer 语句处求值)
x = 20
6. nil 接收者方法
type T struct{}
func (t *T) Foo() { /* 即使 t 是 nil 也可以调用 */ }
var t *T
t.Foo() // OK,t 是 nil 但方法可以被调用
29.5 代码组织原则
| 原则 | 说明 |
|---|---|
| 单一职责 | 每个包/类型只负责一件事 |
| 依赖倒置 | 依赖接口而非具体实现 |
| 显式依赖 | 通过构造函数注入依赖 |
| 最小可见性 | 默认不导出,需要时再导出 |
| 错误包装 | 每层添加上下文信息 |
| 保持简单 | 不要过度工程化 |
🏢 业务场景
- 新项目启动:使用标准布局初始化项目
- 代码审查:检查命名规范、错误处理、测试覆盖
- 性能优化:使用 pprof 找到瓶颈,应用优化技巧
- 重构:识别常见陷阱,安全地重构代码