让 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 自然可控。