项目目录结构
标准项目布局
care-mate/
├── cmd/ # 主应用程序入口
│ ├── care-mate-server/ # API 服务
│ │ └── main.go
│ ├── care-mate-worker/ # 异步任务 worker
│ │ └── main.go
│ └── care-mate-cli/ # CLI 工具
│ └── main.go
├── internal/ # 私有应用代码(不可被外部导入)
│ ├── handler/ # HTTP 处理器(控制器层)
│ │ ├── user_handler.go
│ │ └── order_handler.go
│ ├── service/ # 业务逻辑层
│ │ ├── user_service.go
│ │ └── order_service.go
│ ├── repository/ # 数据访问层
│ │ ├── user_repo.go
│ │ └── order_repo.go
│ ├── model/ # 数据模型
│ │ ├── entity/ # 数据库实体
│ │ │ ├── user.go
│ │ │ └── order.go
│ │ └── dto/ # 数据传输对象
│ │ ├── user_dto.go
│ │ └── order_dto.go
│ ├── middleware/ # HTTP 中间件
│ │ ├── auth.go
│ │ ├── cors.go
│ │ └── logging.go
│ ├── config/ # 配置加载与定义
│ │ └── config.go
│ └── server/ # 服务器初始化
│ ├── http.go
│ └── grpc.go
├── pkg/ # 可被外部导入的公共库
│ ├── logger/ # 日志工具
│ ├── database/ # 数据库连接
│ ├── response/ # 统一响应格式
│ └── utils/ # 通用工具函数
├── api/ # API 定义文件
│ ├── openapi/
│ │ └── swagger.yaml
│ └── proto/ # gRPC proto 文件
│ └── user.proto
├── configs/ # 配置文件模板
│ ├── config.yaml
│ └── config.test.yaml
├── migrations/ # 数据库迁移脚本
│ ├── 001_create_users.sql
│ └── 002_add_orders.sql
├── scripts/ # 构建/部署脚本
│ ├── build.sh
│ └── deploy.sh
├── deployments/ # 部署配置
│ ├── docker/
│ │ └── Dockerfile
│ └── k8s/
│ ├── deployment.yaml
│ └── service.yaml
├── test/ # 集成测试与 E2E 测试
│ ├── integration/
│ └── e2e/
├── docs/ # 项目文档
├── .golangci.yml # golangci-lint 配置
├── Makefile # 构建命令
├── go.mod
├── go.sum
├── .gitignore
└── README.md
关键目录职责对比
| 目录 | 职责 | 是否可外部导入 | 命名约束 |
|---|
cmd/ | 程序入口,main() 所在 | 否 | 每个子目录一个可执行文件 |
internal/ | 核心业务代码 | 否(Go 编译器强制) | 按职责分层 |
pkg/ | 可复用的公共库 | 是 | 按功能域划分 |
api/ | API 协议定义 | - | openapi/proto |
configs/ | 配置模板 | - | 按环境区分 |
test/ | 集成/E2E 测试 | - | 不含单元测试(单元测试与源码同目录) |
分层架构详解
请求流向:HTTP Request → Handler → Service → Repository → Database
响应流向:Database → Repository → Service → Handler → HTTP Response
Handler 层(控制器)
// internal/handler/user_handler.go
package handler
import (
"net/http"
"github.com/go-chi/chi/v5"
)
type UserHandler struct {
userSvc UserServiceInterface
}
func NewUserHandler(svc UserServiceInterface) *UserHandler {
return &UserHandler{userSvc: svc}
}
func (h *UserHandler) Register(r chi.Router) {
r.Route("/api/v1/users", func(r chi.Router) {
r.Get("/{id}", h.GetUser)
r.Post("/", h.CreateUser)
r.Put("/{id}", h.UpdateUser)
r.Delete("/{id}", h.DeleteUser)
})
}
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
user, err := h.userSvc.GetByID(r.Context(), id)
if err != nil {
response.Error(w, http.StatusNotFound, "user not found")
return
}
response.JSON(w, http.StatusOK, user)
}
Service 层(业务逻辑)
// internal/service/user_service.go
package service
import "context"
type UserServiceInterface interface {
GetByID(ctx context.Context, id string) (*dto.UserDTO, error)
Create(ctx context.Context, req *dto.CreateUserReq) (*dto.UserDTO, error)
}
type UserService struct {
userRepo repository.UserRepositoryInterface
}
func NewUserService(repo repository.UserRepositoryInterface) *UserService {
return &UserService{userRepo: repo}
}
func (s *UserService) GetByID(ctx context.Context, id string) (*dto.UserDTO, error) {
entity, err := s.userRepo.FindByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("get user %s: %w", id, err)
}
return dto.FromEntity(entity), nil
}
Repository 层(数据访问)
// internal/repository/user_repo.go
package repository
import (
"context"
"gorm.io/gorm"
)
type UserRepositoryInterface interface {
FindByID(ctx context.Context, id string) (*entity.User, error)
Create(ctx context.Context, user *entity.User) error
}
type UserRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) *UserRepository {
return &UserRepository{db: db}
}
func (r *UserRepository) FindByID(ctx context.Context, id string) (*entity.User, error) {
var user entity.User
err := r.db.WithContext(ctx).First(&user, "id = ?", id).Error
if err != nil {
return nil, err
}
return &user, nil
}
简单项目布局(小型服务)
对于小项目,不需要完整分层,可以简化:
simple-service/
├── cmd/
│ └── server/
│ └── main.go
├── internal/
│ ├── handler.go # 直接放 handler
│ ├── service.go # 直接放 service
│ ├── model.go # 直接放 model
│ └── store.go # 直接放数据访问
├── configs/
│ └── config.yaml
├── go.mod
└── Makefile
常见反模式
| 反模式 | 问题 | 正确做法 |
|---|
把所有代码放 src/ 目录 | Go 没有 src/ 概念,会与 GOROOT 混淆 | 用 cmd/ + internal/ |
utils 包塞满所有函数 | 没有内聚性,变成垃圾桶 | 按功能拆分:strutil/timeutil |
internal 外放业务代码 | 外部可以导入,破坏封装 | 业务代码一律放 internal/ |
model 目录混放所有模型 | 无法区分 entity/dto/vo | 按用途分 entity//dto//vo/ |
单个 main.go 写所有逻辑 | 不可测试、不可复用 | 拆分到 internal/ 各层 |
| 配置硬编码在代码中 | 环境切换困难 | 用 configs/ + 配置加载器 |
Makefile 模板
.PHONY: build run test lint clean docker
APP_NAME := care-mate
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
BUILD_TIME := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
LDFLAGS := -X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME)
build:
CGO_ENABLED=0 go build -ldflags "$(LDFLAGS)" -o bin/$(APP_NAME) ./cmd/server
run:
go run ./cmd/server
test:
go test -v -race -cover ./...
lint:
golangci-lint run ./...
clean:
rm -rf bin/
docker:
docker build -t $(APP_NAME):$(VERSION) -f deployments/docker/Dockerfile .
proto:
protoc --go_out=. --go-grpc_out=. api/proto/*.proto
migrate:
go run ./cmd/migrate up