项目布局最佳实践
推荐布局(中型项目)
care-mate/
├── cmd/
│ └── server/
│ └── main.go # 程序入口,只做初始化和启动
├── internal/
│ ├── app/ # 应用层
│ │ ├── server.go # HTTP 服务器初始化
│ │ ├── routes.go # 路由注册
│ │ └── middleware/
│ │ ├── auth.go
│ │ ├── cors.go
│ │ ├── logging.go
│ │ └── recovery.go
│ ├── domain/ # 领域层
│ │ ├── user/
│ │ │ ├── entity.go # 领域实体
│ │ │ ├── repository.go # 仓储接口
│ │ │ └── service.go # 领域服务
│ │ └── order/
│ │ ├── entity.go
│ │ ├── repository.go
│ │ └── service.go
│ ├── infra/ # 基础设施层
│ │ ├── persistence/
│ │ │ ├── user_repo.go # 仓储实现
│ │ │ └── order_repo.go
│ │ ├── cache/
│ │ │ └── redis.go
│ │ ├── mq/
│ │ │ └── kafka.go
│ │ └── external/
│ │ └── client.go
│ ├── interfaces/ # 接口层
│ │ ├── http/
│ │ │ ├── handler/
│ │ │ │ ├── user_handler.go
│ │ │ │ └── order_handler.go
│ │ │ ├── dto/
│ │ │ │ ├── request.go
│ │ │ │ └── response.go
│ │ │ └── middleware/
│ │ └── grpc/
│ │ └── handler/
│ └── config/
│ └── config.go # 配置定义与加载
├── pkg/ # 可复用的公共库
│ ├── logger/
│ ├── response/ # 统一响应格式
│ ├── validator/ # 请求验证
│ └── errors/ # 错误码定义
├── api/
│ └── openapi/
│ └── swagger.yaml
├── configs/
│ ├── config.yaml
│ ├── config.dev.yaml
│ └── config.prod.yaml
├── deployments/
│ ├── docker/
│ │ └── Dockerfile
│ └── k8s/
│ ├── deployment.yaml
│ └── service.yaml
├── scripts/
│ ├── build.sh
│ └── migrate.sh
├── migrations/
│ ├── 001_init.up.sql
│ └── 001_init.down.sql
├── .golangci.yml
├── Makefile
├── go.mod
└── go.sum
main.go 应该多简洁
// cmd/server/main.go
package main
import (
"context"
"log/slog"
"os"
"os/signal"
"syscall"
"github.com/qingsongchou/care-mate/internal/app"
"github.com/qingsongchou/care-mate/internal/config"
)
// 由 ldflags 注入
var (
Version = "dev"
BuildTime = "unknown"
Commit = "unknown"
)
func main() {
// 1. 加载配置
cfg, err := config.Load()
if err != nil {
slog.Error("failed to load config", "error", err)
os.Exit(1)
}
// 2. 初始化日志
logger := initLogger(cfg)
logger.Info("starting server",
"version", Version,
"build_time", BuildTime,
"commit", Commit,
)
// 3. 初始化应用
app, err := app.New(cfg, logger)
if err != nil {
logger.Error("failed to init app", "error", err)
os.Exit(1)
}
// 4. 启动(在 goroutine 中)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
if err := app.Run(); err != nil {
logger.Error("server error", "error", err)
cancel()
}
}()
// 5. 等待中断信号
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
sig := <-sigCh
logger.Info("received signal, shutting down", "signal", sig)
// 6. 优雅关闭
if err := app.Shutdown(ctx); err != nil {
logger.Error("shutdown error", "error", err)
os.Exit(1)
}
logger.Info("server stopped")
}
统一响应格式
// pkg/response/response.go
package response
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
TraceID string `json:"trace_id,omitempty"`
}
func JSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
json.NewEncoder(w).Encode(Response{
Code: 0,
Message: "success",
Data: data,
})
}
func Error(w http.ResponseWriter, status int, code int, message string) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
json.NewEncoder(w).Encode(Response{
Code: code,
Message: message,
})
}
// 分页响应
type PageResponse struct {
List interface{} `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
Size int `json:"size"`
Pages int `json:"pages"`
}
func PageJSON(w http.ResponseWriter, list interface{}, total int64, page, size int) {
pages := int(math.Ceil(float64(total) / float64(size)))
JSON(w, http.StatusOK, PageResponse{
List: list,
Total: total,
Page: page,
Size: size,
Pages: pages,
})
}
错误码体系
// pkg/errors/codes.go
package errors
type AppError struct {
Code int `json:"code"`
Message string `json:"message"`
Err error `json:"-"`
}
func (e *AppError) Error() string { return e.Message }
func (e *AppError) Unwrap() error { return e.Err }
// 错误码定义(按模块分段)
const (
// 通用错误 10000-19999
ErrInternal = 10000
ErrBadRequest = 10001
ErrUnauthorized = 10002
ErrForbidden = 10003
ErrNotFound = 10004
ErrConflict = 10005
ErrValidation = 10006
ErrRateLimit = 10007
// 用户模块 20000-29999
ErrUserNotFound = 20001
ErrUserExists = 20002
ErrInvalidPassword = 20003
ErrTokenExpired = 20004
// 订单模块 30000-39999
ErrOrderNotFound = 30001
ErrOrderStatus = 30002
)
var errorMessages = map[int]string{
ErrInternal: "内部错误",
ErrBadRequest: "请求参数错误",
ErrUnauthorized: "未授权",
ErrForbidden: "无权限",
ErrNotFound: "资源不存在",
ErrUserNotFound: "用户不存在",
ErrUserExists: "用户已存在",
ErrInvalidPassword: "密码错误",
}
func New(code int) *AppError {
msg, ok := errorMessages[code]
if !ok {
msg = "未知错误"
}
return &AppError{Code: code, Message: msg}
}
func Wrap(code int, err error) *AppError {
return &AppError{Code: code, Message: errorMessages[code], Err: err}
}
// 错误码 → HTTP 状态码
func HTTPStatus(code int) int {
switch {
case code >= 10000 && code < 20000:
switch code {
case ErrBadRequest, ErrValidation:
return http.StatusBadRequest
case ErrUnauthorized, ErrTokenExpired:
return http.StatusUnauthorized
case ErrForbidden:
return http.StatusForbidden
case ErrNotFound:
return http.StatusNotFound
case ErrConflict:
return http.StatusConflict
case ErrRateLimit:
return http.StatusTooManyRequests
default:
return http.StatusInternalServerError
}
case code >= 20000 && code < 30000:
if code == ErrUserNotFound || code == ErrUserExists {
return http.StatusBadRequest
}
return http.StatusBadRequest
default:
return http.StatusBadRequest
}
}