文章

项目目录结构

项目目录结构

标准项目布局

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