Go 轻量分层入门:Handler、Service、Store 怎么刚刚好

本文用一个小型任务 API 讲解 Go 项目中轻量分层的做法,说明 Handler、Service、Store 的职责边界和避免过度设计的方法。

分层不是为了画图好看

Go 项目写到一定规模后,总会遇到一个问题:HTTP handler 里东西越来越多。它既解析 JSON,又校验参数,又查数据库,又拼响应,还打日志。短期写得快,长期很难改。于是很多人开始学习“Clean Architecture”“DDD”“六边形架构”。这些思想有价值,但入门项目照搬全套目录,很容易过度设计。

对大多数小型 Go 服务来说,轻量分层就够了:Handler 负责 HTTP,Service 负责业务规则,Store 负责数据访问。它没有复杂术语,但能把最容易混乱的职责分开。

这篇文章用任务 API 做例子,讲怎么刚刚好地分层。

数据模型

type Task struct {
	ID    int64  `json:"id"`
	Title string `json:"title"`
	Done  bool   `json:"done"`
}

Store 接口:

type TaskStore interface {
	Create(ctx context.Context, title string) (Task, error)
	List(ctx context.Context) ([]Task, error)
	MarkDone(ctx context.Context, id int64) (Task, error)
}

Service:

type TaskService struct {
	store TaskStore
}

func NewTaskService(store TaskStore) *TaskService {
	return &TaskService{store: store}
}

Handler 依赖 Service:

type Handler struct {
	service *TaskService
}

依赖方向很清楚:Handler 调 Service,Service 调 Store。业务规则不应该反过来依赖 HTTP。

Service 放业务规则

func (s *TaskService) Create(ctx context.Context, title string) (Task, error) {
	title = strings.TrimSpace(title)
	if title == "" {
		return Task{}, fmt.Errorf("title is required")
	}
	if len([]rune(title)) > 80 {
		return Task{}, fmt.Errorf("title is too long")
	}
	return s.store.Create(ctx, title)
}

func (s *TaskService) MarkDone(ctx context.Context, id int64) (Task, error) {
	if id <= 0 {
		return Task{}, fmt.Errorf("invalid task id")
	}
	return s.store.MarkDone(ctx, id)
}

标题不能为空、不能太长、ID 必须合法,这些都是业务或应用规则,放在 Service 比放在 Handler 更容易复用。将来如果有命令行工具或后台任务也要创建任务,可以直接调用 Service。

Handler 只处理 HTTP

type createTaskRequest struct {
	Title string `json:"title"`
}

func (h *Handler) createTask(w http.ResponseWriter, r *http.Request) {
	var req createTaskRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeError(w, http.StatusBadRequest, "invalid json")
		return
	}

	task, err := h.service.Create(r.Context(), req.Title)
	if err != nil {
		writeError(w, http.StatusBadRequest, err.Error())
		return
	}

	writeJSON(w, http.StatusCreated, task)
}

Handler 关心 HTTP 请求体、状态码、JSON 响应。它不直接操作数据库,也不重复业务校验。这样 handler 测试可以专注 HTTP 边界,service 测试可以专注业务规则。

Store 只处理数据

内存实现:

type MemoryTaskStore struct {
	mu     sync.Mutex
	nextID int64
	tasks  map[int64]Task
}

func NewMemoryTaskStore() *MemoryTaskStore {
	return &MemoryTaskStore{
		nextID: 1,
		tasks:  make(map[int64]Task),
	}
}

func (s *MemoryTaskStore) Create(ctx context.Context, title string) (Task, error) {
	s.mu.Lock()
	defer s.mu.Unlock()

	task := Task{ID: s.nextID, Title: title}
	s.tasks[task.ID] = task
	s.nextID++
	return task, nil
}

将来换成 SQL 实现:

type SQLTaskStore struct {
	db *sql.DB
}

只要满足 TaskStore 接口,Service 不需要知道底层是内存还是数据库。

什么时候不要继续拆

不要一开始就拆出 domainusecaserepositorydtoassemblerfactorymanager 一大堆层。层数越多,简单需求跳转越累。

一个实用标准:如果一层没有独立职责,只是在传参,那就先不要拆。比如:

func (u *CreateTaskUsecase) Execute(ctx context.Context, title string) (Task, error) {
	return u.service.Create(ctx, title)
}

如果它没有增加规则、事务、编排或边界,可能只是多了一层。

小结

Go 小服务的轻量分层可以很简单:Handler 处理 HTTP,Service 处理业务规则,Store 处理数据访问。依赖通过构造函数显式注入,接口定义在使用方需要的位置,层数随着复杂度增长。

分层不是为了显得专业,而是为了让代码更容易改。刚刚好的结构应该让读者更快找到规则在哪里、数据在哪里、HTTP 边界在哪里。如果分层让简单功能变得更难读,那就是过度设计。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页