让 AI 搭骨架:目录分层、接口签名与占位实现

骨架(scaffold)阶段的目标不是"写出功能",而是让工程编译通过、测试包跑得起来、接口形状定下来。这一步做对了,后面所有代码生成都是在确定的框里填内容;做错了,接口天天变,AI 每轮都要连带改十几个调用点,diff 迅速失控。

本章要解决的问题

  • 一上手就让 AI 写完整实现,几百行一次涌出,根本无法审查;
  • 接口签名由 AI 随手定,参数一会儿是 struct、一会儿是散开的三个参数;
  • 分层混乱:handler 里直接写 SQL,repo 里返回 HTTP 状态码;
  • 占位实现返回 nil,编译通过但运行时静默错数据。

先定分层,再让 AI 填

Go 后端常见的分层与依赖方向(箭头表示"可以引用"):

cmd/api            → 组装依赖、读配置、启动 HTTP 服务
internal/handler   → 协议转换:HTTP/JSON ↔ 领域对象
internal/service   → 业务规则:事务边界、领域校验
internal/repo      → 持久化:SQL / Redis / 外部调用
internal/model     → 纯数据结构与哨兵错误,不依赖任何上层

规则只有一条:依赖只能向下。model 不 import 任何内部包,handler 不写 SQL,repo 不认识 http.StatusNotFound。

骨架阶段的三样产出物

产出物检查方式
目录与包声明go list ./... 输出了预期的包路径
接口签名(interface + 方法)go build ./... 无错误通过
占位实现(panic 或哨兵错误)grep -rn 'TODO:' 能定位到全部未实现点

提示词:只出骨架,不出逻辑

在 internal/coupon 下搭建优惠券核销功能的骨架,严格遵守:

1. 分层:handler / service / repo 三层,依赖方向 handler → service → repo,model 包不引用其他内部包;
2. 第一步只给出文件清单与每层的 interface 定义,等我确认,不要直接写代码;
3. 确认后每个方法写占位实现,函数体只允许是 panic("TODO: ...") 或 return nil, ErrNotImplemented;
4. 不写任何 SQL、不引入第三方库、不写业务判断;对外方法第一个参数必须是 context.Context;
5. 完成后运行 gofmt -l . && go build ./... && go vet ./...,把命令输出原样贴出来。

骨架长什么样

// internal/coupon/repo.go
package coupon

import (
	"context"
	"time"
)

// Repo 声明核销所需的持久化能力,由具体存储实现。
// 只保留调用方真正需要的方法:接口越小,越容易被替换和伪造。
type Repo interface {
	GetForUpdate(ctx context.Context, couponID int64) (*Coupon, error)
	MarkUsed(ctx context.Context, couponID int64, usedAt time.Time) (bool, error)
}
// internal/coupon/service.go
package coupon

import (
	"context"
	"errors"
	"time"
)

// ErrNotImplemented 表示骨架阶段尚未实现的占位错误。
var ErrNotImplemented = errors.New("coupon: not implemented")

// Service 提供优惠券核销的业务能力。
type Service struct {
	repo  Repo
	nowFn func() time.Time // 由 cmd/api 注入,测试里可替换
}

// Redeem 核销指定用户的优惠券,规格见 docs/spec/coupon-redeem.md。
func (s *Service) Redeem(ctx context.Context, userID, couponID int64) error {
	panic("TODO: Service.Redeem")
}

注意两点:Repo 只声明两个方法,而不是把整张表的增删改查全铺开;nowFn 把时间变成可注入依赖,避免骨架阶段就埋下 time.Now()。

第一道验收:编译通过 + 空实现

骨架阶段没有业务可测,跑这四条即可:

gofmt -l .              # 有输出说明格式不合规,直接让 AI 改
go build ./...          # 类型、签名、包依赖是否正确
go vet ./...            # 未使用的变量、可疑的 Printf 等
go test ./... -run XXX  # 让测试包也能编译(-run XXX 匹配不到任何用例)

再加一条"未实现点可见性检查",防止占位实现被悄悄写成假逻辑:

grep -rn 'TODO:' internal/ | wc -l

这个数字应当等于你预期的未实现方法数。对不上,要么 AI 多写了实现,要么留下了空函数体。

骨架阶段该做与不该做

该做不该做
定包名、目录、依赖方向借搭骨架"顺便"实现业务逻辑
定 interface 的最小方法集把 struct 全部字段暴露成接口
用哨兵错误定下错误契约每个方法各自造一种错误类型
注入 clock、ID 生成器等外部依赖函数体里直接调 time.Now()、uuid.New()

常见坑

  • 骨架里塞实现:一旦 AI 在骨架阶段写了 30 行逻辑,你就失去了"先对齐形状"的机会,后面的审查面积成倍增加;
  • 占位返回 nil:return nil 会让调用方以为成功,务必用 panic 或 ErrNotImplemented;
  • 接口按表设计:Repo 里出现没人调用的 ListAll、UpdateAny,等于给未来挖坑;
  • 循环依赖:model 里 import 了 service,go build 立刻报 import cycle,应让 AI 调整依赖方向,而不是用 interface{} 绕过;
  • 不跑 gofmt:格式不一致会让后续 diff 充满噪音,掩盖真实改动。

小结:骨架阶段只交付目录分层、接口签名和占位实现,验收标准是 gofmt -l . && go build ./... && go vet ./... 干净通过,外加 grep -rn 'TODO:' 的数量可核对。接口形状一旦定下,后续每次代码生成都变成在确定的框里填内容,diff 自然可控。

笔记加载中…