一、项目结构规范
遵循 Go 模块(Module)标准,使用 micro
工具的 micro new
命令生成项目结构, 建议采用以下分层结构:
project/
├── api/ # API 相关(admin/rpc 接口定义)
│ ├── admin/ # 管理端接口
│ ├── handler/ # 业务处理逻辑
│ └── proto/ # protobuff定义
│ └── rpc/ # 服务间rpc接口
│ ├── handler/ # 业务处理逻辑
│ └── proto/ # protobuff定义
├── assets/ # 项目所需的静态资源
│ ├── approve_desc/ # 单据审批描述定义
│ ├── cancel/ # 单据依赖关系定义
│ └── codelike/ # 编码规则定义
├── bill/ # 业务单据定义
│ ├── contract/ # 合同相关业务单据,也可根据业务模式进行分类,如:fti,cst
│ ├── fund/ # 资金相关业务单据
│ └── stock/ # 库存相关业务单据
├── biz/ # 业务相关的公共实现
├── build/ # 编译相关
│ ├── bin/ # 编译生成的可执行文件与启动/停用脚本
│ └── conf/ # 配置文件
├── errors/ # 自定义错误定义
├── model/ # 业务表生成的定义文件
├── tests/ # 集成测试(区分单元测试和集成测试)
├── vm/ # 编译期自动生成的文件
├── docs/ # 文档(API 文档、设计文档、部署说明)
├── go.mod # 模块依赖文件
├── go.sum # 依赖哈希文件
├── .service.yml # 服务相关的配置,如服务ID、服务名、服务版本、服务使用的tables
└── README.md # 项目说明
规范要求:
内部包与公开包分离:
internal
目录下的包不可被外部模块引用,防止接口不稳定导致的依赖问题。单一职责原则:每个目录 / 文件仅处理单一功能(如
handlers
处理 HTTP 请求,service
处理业务逻辑)。配置管理:配置文件支持环境变量覆盖,通过
viper
或config
包加载,避免硬编码。
二、命名规范
1. 包名
使用小写单词,多个单词使用下划线,无驼峰(如
database
、logger
,避免dbUtils
)。包名与目录名一致,禁止使用复数(如目录
models
对应包名model
)。避免模糊命名(如
util
、common
,建议更具体如timeutil
、stringutils
)。
2. 标识符(变量、函数、结构体)
导出规则:首字母大写表示可导出(公共接口),小写表示包内私有。
正确:
User
(结构体)、GetUser()
(函数)、MaxSize
(常量)。错误:
user
(公共结构体未导出)、getUser()
(公共函数未导出)。
变量名:使用有意义的全称(如
userID
而非uId
),布尔变量以Is
/Has
/Can
开头(如isActive
、hasPermission
)。函数名:动词或动词短语(如
CreateUser
、ValidateInput
),返回错误的函数以Err
结尾(如GetUserErr
)。结构体 / 接口名:名词或名词短语(如
UserRepository
、CacheInterface
)。
3. 常量与枚举
常量全大写,单词间下划线分隔(如
MAX_CONNECTIONS = 100
)。枚举使用
iota
定义,搭配String()
方法(示例如下):type Status int const ( StatusPending Status = iota StatusActive StatusInactive StatusDeleted ) func (s Status) String() string { return [...]string{"Pending", "Active", "Inactive", "Deleted"}[s] }
三、编码规范
1. 格式与风格
代码格式化:强制使用
go fmt
或 IDE 自动格式化,禁止手动调整缩进(缩进为 4 个空格,Go 工具会自动处理)。导入顺序
:按标准库、第三方库、本地包分组,每组内按字母顺序排列,组间空一行:
import ( "fmt" "net/http" "github.com/gin-gonic/gin" "project/internal/model" "project/pkg/utils" )
行长度
:建议不超过 120 字符,长参数列表换行时保持缩进对齐:
result, err := someFunction( param1, param2, param3, )
2. 控制结构
条件语句 : 避免复杂条件,拆分为多个
if
语句;if
后紧跟小括号,无空格:if err != nil { ... } // 正确 if (err != nil) { ... } // 错误(多余括号)
循环: 仅用
for
循环;range
遍历切片 / 映射时,避免忽略索引或值(用_
占位):for i, user := range users { ... } // 正确 for _, user := range users { ... } // 正确(忽略索引) for i := range users { ... } // 正确(仅用索引)
错误优先返回: 函数返回值优先处理错误,保持代码扁平:
func getUser(id int) (User, error) { if id <= 0 { return User{}, fmt.Errorf("invalid user ID: %d", id) } // 后续逻辑 }
3. 函数与接口
函数参数: 参数超过 3 个时,使用结构体传参(提高可读性):
// 推荐 func sendEmail(ctx context.Context, opts EmailOptions) error { ... } type EmailOptions struct { To string Subject string Body string }
接口设计: 接口名以
er
结尾(如Reader
、Writer
),单个方法的接口使用func
类型别名(函数式接口):type Validator func(data interface{}) error // 单个方法接口,替代传统接口定义
defer 语句:用于资源释放(如文件、数据库连接),避免滥用(过多 defer 可能影响性能和可读性)。
四、错误处理规范
1. 基础规则
禁止忽略错误,必须显式处理(使用
//nolint
注释例外,但需说明原因):// 错误:忽略错误 result, _ := someFunction() // 正确:处理错误 result, err := someFunction() if err != nil { return fmt.Errorf("process failed: %w", err) // 使用 %w 包裹错误 }
使用
fmt.Errorf
+%w
实现错误链,便于追踪根本原因:return fmt.Errorf("database query failed: %w", err)
2. 自定义错误
业务错误定义为实现
Error() string
方法的结构体,支持附加元数据:
type BusinessError struct { Code int // 错误码(如 400、500) Message string // 错误信息 Err error // 原始错误(可选) } func (e *BusinessError) Error() string { if e.Err != nil { return fmt.Sprintf("%d: %s: %v", e.Code, e.Message, e.Err) } return fmt.Sprintf("%d: %s", e.Code, e.Message) }
3. 日志规范
区分日志级别(
debug
、info
、warn
、error
、fatal
),使用服务的VM
包输出日志:vm.Info("user logged in", zap.Int("user_id", user.ID)) // 结构化日志示例
错误日志必须包含上下文信息(如请求 ID、用户 ID、时间戳),便于排查。
五、测试规范
1. 测试文件命名
测试文件以
_test.go
结尾,位于被测试文件同目录或tests/
目录(集成测试)。测试函数命名为
TestXXX
(导出函数)或testXXX
(非导出函数,包内测试)。
2. 单元测试
使用表驱动测试(Table-Driven Tests)覆盖正常、边界、异常场景:
func TestAdd(t *testing.T) { tests := []struct { name string a, b int want int wantErr bool }{ {"positive numbers", 2, 3, 5, false}, {"negative numbers", -2, 3, 1, false}, {"error case", 0, 0, 0, true}, // 示例错误场景 } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := Add(tt.a, tt.b) if (err != nil) != tt.wantErr { t.Errorf("Add() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { t.Errorf("Add() = %v, want %v", got, tt.want) } }) } }
覆盖率要求:核心业务代码覆盖率 ≥ 80%,通过
go test -coverprofile=cover.out
检查。
3. 集成测试
集成测试需启动真实服务(如数据库、HTTP 服务器),使用测试数据库(名称后缀加
_test
)。使用
testing
包的T
或TB
接口,避免依赖外部测试框架。
4. 基准测试
性能敏感函数需添加基准测试(
BenchmarkXXX
),关注内存分配和耗时:
func BenchmarkAdd(b *testing.B) { for i := 0; i < b.N; i++ { Add(1, 2) } }
六、依赖管理与版本控制
1. Go Module 规范
使用
go mod
管理依赖,禁止手动修改go.mod
,通过go get
/go mod tidy
维护。依赖版本固定,提交
go.mod
和go.sum
到版本控制。避免循环依赖:通过接口隔离、依赖倒置原则解耦模块。
2. 第三方库选择
优先使用内部已筛选过的三方库,不能满足业务的可选择社区主流库(如
gin
用于 HTTP,gorm
用于 ORM),避免维护成本高的小众库。定期更新依赖(每月一次),使用
go list -u
检查可用更新,通过go get -u
升级。敏感操作库(如加密、认证)需经过安全审计(如
golang.org/x/crypto
而非自定义实现)。
七、代码审查与协作
1. 审查要点
设计合理性:是否符合单一职责、开闭原则,接口是否稳定。
错误处理:是否遗漏错误,错误链是否正确构建,日志是否足够。
测试覆盖:单元测试是否覆盖所有分支,集成测试是否验证端到端流程。
性能与安全:是否存在内存泄漏、竞态条件,敏感数据是否加密存储。
命名与注释:标识符是否清晰,复杂逻辑是否有注释(见下方注释规范)。
2. 分支策略
使用
master
作为生产分支,dev
作为开发分支。功能分支命名:
feature-v3.0.0
。临时分支命名:
dev-temp250302
,为临时处理需求的分支。禁止直接提交到
master
分支,必须通过代码审查。
八、注释规范
1. 包注释
每个包的 doc.go
或包内首个文件添加注释,说明包的功能、设计原则:
// user 包提供用户管理核心逻辑,包括用户创建、查询、更新和删除功能。
// 所有接口遵循错误优先返回原则,业务错误通过 BusinessError 结构体传递。
package user
2. 接口与结构体注释
导出的接口 / 结构体必须添加注释,说明用途和字段含义:
// User 表示系统中的用户实体。
// 字段说明:
// - ID: 用户唯一标识符(自增整数)
// - Name: 用户名(最大长度 50 字符)
// - Email: 注册邮箱(唯一索引)
type User struct {
ID int64
Name string
Email string
}
3. 函数注释
导出函数必须添加注释,说明功能、参数、返回值、错误场景:
// CreateUser 创建新用户,用户名和邮箱需唯一。
// 参数:
// - ctx: 上下文(用于传递请求范围数据和取消信号)
// - req: 创建用户请求,包含用户名、邮箱、密码
// 返回:
// - user: 创建成功的用户实体
// - err: 错误信息,可能为 ErrUserExists(用户名或邮箱已存在)、ErrInvalidInput(参数校验失败)
func CreateUser(ctx context.Context, req CreateUserRequest) (User, error) { ... }
4. 复杂逻辑注释
非导出函数或复杂逻辑块添加注释,解释算法、设计决策或注意事项:
// 此处使用双重检查锁定(Double-Check Locking)实现单例模式,
// 需注意并发安全(通过 sync.Once 保证初始化仅执行一次)。
var once sync.Once
var instance *Singleton
九、最佳实践与禁忌
1. 并发安全
使用
sync.Mutex
/sync.RWMutex
保护共享资源,避免竞态条件(通过go test -race
检测)。限制 Goroutine 数量,使用
sync.WaitGroup
等待所有 Goroutine 完成,避免泄漏。
2. 性能优化
避免频繁内存分配:使用
sync.Pool
重用对象,预分配切片容量(make([]T, 0, capacity)
)。合理使用
interface{}
:类型断言和反射有性能开销,优先使用具体类型。性能敏感代码使用
pprof
分析(CPU / 内存 / 阻塞),避免过早优化。
3. 避免常见陷阱
切片陷阱:注意切片共享底层数组,修改可能影响其他变量。
空指针与零值:结构体指针需初始化(避免
nil
解引用),合理利用 Go 的零值初始化。import 副作用:禁止无意义的
import "package"
(仅为触发init()
函数),改用显式初始化函数。
十、工具链与 IDE 配置
1. 必备工具
格式化:
go fmt
(自动格式化)、goimports
(自动整理导入)。静态检查:
go vet
(类型检查)、golangci-lint
(集成多个 linter,如govet
、gocyclo
、ineffassign
)。调试与性能:
delve
(调试器)、pprof
(性能分析)。依赖管理:
go mod
(官方工具)、go install
(安装工具)。
2. IDE 配置
VS Code:安装 Go 扩展(
gopls
),启用自动格式化、代码补全、错误提示。GoLand:默认配置符合 Go 规范,建议启用代码审查插件(如 SonarLint)。
3. CI/CD 流程
每次提交触发:
golangci-lint run
(静态检查)、go test -race ./...
(竞态检测)、单元测试。合并到
main
分支前触发:集成测试、构建二进制文件、生成覆盖率报告。发布时:使用
go build -ldflags="-s -w"
生成无调试信息的二进制,确保版本号正确(-X main.version=1.0.0
)。