name: go-best-practices description: 提供Go语言类型优先开发的模式,包括自定义类型、接口、函数选项和错误处理。在读写Go文件时必须使用。
Go最佳实践
类型优先开发
类型在实现之前定义契约。遵循以下工作流程:
- 定义数据结构 - 首先定义结构体和接口
- 定义函数签名 - 参数、返回类型和错误条件
- 实现以满足类型 - 让编译器指导完整性
- 在边界处验证 - 在数据进入系统时检查输入
使非法状态无法表示
使用Go的类型系统在编译时防止无效状态。
领域模型的结构体:
// 首先定义数据模型
type User struct {
ID UserID
Email string
Name string
CreatedAt time.Time
}
type CreateUserRequest struct {
Email string
Name string
}
// 函数从类型派生
func CreateUser(req CreateUserRequest) (*User, error) {
// 实现
}
领域原语的自定义类型:
// 不同的类型防止ID混淆
type UserID string
type OrderID string
func GetUser(id UserID) (*User, error) {
// 编译器防止在此处传递OrderID
}
func NewUserID(raw string) UserID {
return UserID(raw)
}
// 方法将行为附加到类型
func (id UserID) String() string {
return string(id)
}
行为契约的接口:
// 定义你需要的,而不是你拥有的
type Reader interface {
Read(p []byte) (n int, err error)
}
type UserRepository interface {
GetByID(ctx context.Context, id UserID) (*User, error)
Save(ctx context.Context, user *User) error
}
// 接受接口,返回结构体
func ProcessInput(r Reader) ([]byte, error) {
return io.ReadAll(r)
}
使用iota的枚举:
type Status int
const (
StatusActive Status = iota + 1
StatusInactive
StatusPending
)
func (s Status) String() string {
switch s {
case StatusActive:
return "active"
case StatusInactive:
return "inactive"
case StatusPending:
return "pending"
default:
return fmt.Sprintf("Status(%d)", s)
}
}
// switch中的穷举处理
func ProcessStatus(s Status) (string, error) {
switch s {
case StatusActive:
return "processing", nil
case StatusInactive:
return "skipped", nil
case StatusPending:
return "waiting", nil
default:
return "", fmt.Errorf("unhandled status: %v", s)
}
}
灵活构造的函数选项:
type ServerOption func(*Server)
func WithPort(port int) ServerOption {
return func(s *Server) {
s.port = port
}
}
func WithTimeout(d time.Duration) ServerOption {
return func(s *Server) {
s.timeout = d
}
}
func NewServer(opts ...ServerOption) *Server {
s := &Server{
port: 8080, // 合理的默认值
timeout: 30 * time.Second,
}
for _, opt := range opts {
opt(s)
}
return s
}
// 用法:NewServer(WithPort(3000), WithTimeout(time.Minute))
嵌入用于组合:
type Timestamps struct {
CreatedAt time.Time
UpdatedAt time.Time
}
type User struct {
Timestamps // 嵌入 - User拥有CreatedAt, UpdatedAt
ID UserID
Email string
}
模块结构
优先在包内使用较小的文件:每个文件处理一个类型或关注点。当文件处理多个不相关的类型或超过约300行时进行拆分。将测试放在与实现同名的_test.go文件中。包边界定义API;内部组织是灵活的。
函数式模式
- 当方法不改变状态时使用值接收器;为突变保留指针接收器。
- 避免包级可变变量;通过函数参数显式传递依赖项。
- 返回新的结构体/切片而不是改变输入;使数据流显式。
- 在简化代码的地方使用闭包和高阶函数(例如,
sort.Slice,迭代器)。
指令
- 使用
fmt.Errorf和%w进行包装返回带上下文的错误。这为调试保留了错误链。 - 每个函数返回一个值或一个错误;未实现的路径返回描述性错误。显式失败是可调试的。
- 处理
switch语句中的所有分支;包括一个返回错误的default情况。穷举处理防止静默错误。 - 将
context.Context传递给具有显式超时的外部调用。失控的请求会导致级联故障。 - 为真正不可恢复的情况保留
panic;优先返回错误。Panic会使程序崩溃。 - 为新逻辑添加或更新表驱动测试;覆盖边缘情况(空输入,nil,边界)。
示例
未实现逻辑的显式失败:
func buildWidget(widgetType string) (*Widget, error) {
return nil, fmt.Errorf("buildWidget not implemented for type: %s", widgetType)
}
用上下文包装错误以保留链:
out, err := client.Do(ctx, req)
if err != nil {
return nil, fmt.Errorf("fetch widget failed: %w", err)
}
return out, nil
带默认错误的穷举switch:
func processStatus(status string) (string, error) {
switch status {
case "active":
return "processing", nil
case "inactive":
return "skipped", nil
default:
return "", fmt.Errorf("unhandled status: %s", status)
}
}
使用slog的结构化日志记录:
import "log/slog"
var log = slog.With("component", "widgets")
func createWidget(name string) (*Widget, error) {
log.Debug("creating widget", "name", name)
widget := &Widget{Name: name}
log.Debug("created widget", "id", widget.ID)
return widget, nil
}
配置
- 在启动时从环境变量加载配置;在使用前验证必需值。缺少配置应立即导致退出。
- 将Config结构体定义为单一事实来源;避免
os.Getenv分散在代码中。 - 为开发使用合理的默认值;为生产机密要求显式值。
示例
类型化配置结构体:
type Config struct {
Port int
DatabaseURL string
APIKey string
Env string
}
func LoadConfig() (*Config, error) {
dbURL := os.Getenv("DATABASE_URL")
if dbURL == "" {
return nil, fmt.Errorf("DATABASE_URL is required")
}
apiKey := os.Getenv("API_KEY")
if apiKey == "" {
return nil, fmt.Errorf("API_KEY is required")
}
port := 3000
if p := os.Getenv("PORT"); p != "" {
var err error
port, err = strconv.Atoi(p)
if err != nil {
return nil, fmt.Errorf("invalid PORT: %w", err)
}
}
return &Config{
Port: port,
DatabaseURL: dbURL,
APIKey: apiKey,
Env: getEnvOrDefault("ENV", "development"),
}, nil
}