两个月,59 篇开发日志,一个从零起步、最后能跑在真实业务里的 Golang 商业级项目。这事儿干完之后,我最大的感受不是"AI 真好用",而是"我以前用 AI 的方式太糙了"。前面那篇聊的是工具选择、环境搭建、模型能力边界这些偏"外功"的东西,这一篇我想把内功部分摊开讲——同样一个 AI,为什么有的人用起来像给自己配了个能扛事的工程师,有的人用起来像在跟一个记性差、还老爱自由发挥的实习生扯皮。差别不在模型,在于你给它什么上下文、你让它按什么节奏干活、你用什么方式验收它的产出。这篇内容适合已经能写 Golang、准备接第一个商业项目的人,也适合写了几年业务代码、想把手上的重复劳动真正压下去的同行,如果你刚开始接触 AI 编程,里面的日志体系和提示词骨架也能直接抄去用。
1. 心法总纲:AI 是需要带教的初级工程师,不是许愿池
1.1 从"帮我写个功能"到"这是一份任务单"
我前二十篇日志里最大的浪费,全花在"许愿式提问"上。什么叫许愿式?就是丢一句"帮我写个订单支付模块",然后等它吐出一大坨代码。它确实会吐,而且看起来还挺像回事:结构体、接口、方法名都齐了,甚至还有注释。但你一旦把它放进真实项目里,问题立刻露出来——它不知道你的订单状态机是怎么定义的,不知道你的金额用的是 int64 存分还是 decimal,不知道你的日志组件封装在哪一层,于是它自己发明了一套,还发明得挺自信。
后来我改了做法,把每一次请求都当成给一个刚入职的初级工程师下任务单。任务单里必须有的东西:这个功能在业务里扮演什么角色、输入输出的边界在哪、依赖哪些已有的包、错误该怎么往外抛、有没有并发要求、验收标准是什么。听起来麻烦,但实际写下来也就七八行字,省下来的是后面两小时的返工。
举个真实对照。许愿式:"写一个用户余额扣减的接口。"任务单式:"在 internal/service 下新增 BalanceService.Deduct,输入 userID int64、amount int64(单位:分)、bizNo string,要求:1)用现有 repository 层的 TxManager 开事务;2)扣减前先查余额,不足时返回 ErrInsufficientBalance(已定义在 internal/errs);3)同一 bizNo 重复调用必须幂等,靠唯一索引兜底;4)失败时用项目里已有的 logx.WithCtx 记录结构化日志,字段包含 userID、amount、bizNo;5)不要引入新依赖。"后者出来的代码,一次就能编译过,八成能直接用。
心得:你给 AI 的每一条约束,都是在减少它"自由发挥"的空间。约束不是限制它的能力,是限制它的想象力的跑偏方向。
1.2 上下文预算:把对话窗口当纸巾用
我踩过最狠的坑,是一个对话窗口从项目第一天用到第十五天。前期它确实越来越"懂我",到了后期,它开始把三天前改掉的字段名当成现存的,把已经废弃的接口继续调用,甚至把两个不同模块的命名风格混在一起。这不是模型变笨了,是上下文里塞了太多过期信息,它分不清哪条是当前有效的。
我的处理方式后来固定成两条规则。第一,一个任务一个窗口,任务做完就关,不心疼。第二,跨任务需要共享的信息,落到文件里,不靠对话记忆。落到文件里的东西就是我的那 59 篇开发日志,以及项目根目录下的一个 ARCH.md。
ARCH.md 我控制在 200 行以内,只写三样:目录结构树、每一层的职责一句话、全局约定(命名、错误码分段、日志字段规范)。每次开新窗口,第一件事就是让它读这个文件,再读相关的三五个源码文件。这样做的好处是上下文永远是"当前快照",而不是一锅陈年乱炖。实测下来,同一个需求,用干净窗口加 ARCH.md 的方式,返工率大概能降一半以上。
还有一个细节:不要用"接着上面的改"这种指代。指代意味着你要依赖对话历史,而对话历史一旦被截断或者被摘要压缩,指代就断了。我在日志里吃过一次亏,第七天说的"上面的那个 handler",第二十一天再让它改,它改的是另一个文件。从那以后我要求自己每次引用都写全路径、全文件名、全函数名。
1.3 让它先复述,再动手
这是我在第四十篇日志之后才养成的习惯,但价值极高。发完任务单之后,我不直接让它写代码,而是先让它用五句话复述:它认为要改哪些文件、每个文件做什么、有哪些不确定的地方。这一步基本上每次都能抓出问题——要么它理解偏了,要么它发现我的任务单里漏了边界条件,要么它指出某个依赖在当前代码里不存在。
这个"复述-确认-执行"的三段式,看起来多花了两分钟,实际上把沟通成本从"写完再发现方向错了"降到了"还没写就纠正了"。尤其是涉及数据迁移、对外接口变更这类不好回滚的操作,这两分钟能救你一整天。
2. 提示词在 Golang 工程里的落地姿势
2.1 一条能反复复用的提示词骨架
网上那些万能提示词模板,大部分在真实项目里不好使,因为它们假设你面对的是一个空白项目。商业项目的特点是:有历史包袱、有既定风格、有不能碰的红线。所以我的提示词骨架是围绕"约束"组织的,不是围绕"角色扮演"组织的。
我常用的骨架长这样,顺序基本固定:
- 背景:这是什么项目,什么语言版本,什么框架,代码在哪个目录。
- 任务:一句话说清要做什么,动词开头。
- 边界:允许改哪些文件,不允许动哪些文件,不允许引入什么依赖。
- 规范:命名风格、错误处理方式、日志方式、注释语言。
- 验收:怎么算做完了,需要哪些测试,需要跑哪些命令。
- 输出格式:只给完整文件内容,不要给 diff 片段,不要给省略号。
最后一条特别重要。如果你不强调"给完整文件",模型很喜欢给你一个片段,中间写个 // ... 省略,你还得自己去拼接。在 Golang 里一个文件动辄几百行,拼接一次就是一次出错机会。我一般在任务单末尾加一句"输出必须是可以直接保存并编译的完整文件内容"。
另外关于语言版本要写清楚,比如 go 1.22,因为不同版本的写法差异很大。举个最直观的:老版本循环变量捕获问题,Go 1.22 之后每一轮迭代都是新变量,但如果你不告诉它版本,它可能给你加一堆多余的局部变量拷贝,看起来像是老代码,风格上很割裂。
2.2 先让它写测试,再让它写实现
这条是我整个项目里收益最高的单个技巧。流程是:我先手写接口签名和数据结构(这部分绝不交给 AI,因为它是设计的核心),然后把签名丢给 AI,让它先写表驱动测试用例,覆盖正常路径、边界、错误分支。测试写完我先审一遍——审测试比审实现快得多,因为测试揭露的是"你到底想让这个函数干什么"。
type DeductReq struct { UserID int64 Amount int64 // 单位:分 BizNo string } func TestBalanceService_Deduct(t *testing.T) { cases := []struct { name string balance int64 req DeductReq wantErr error }{ {"正常扣减", 10000, DeductReq{1, 500, "b1"}, nil}, {"余额不足", 100, DeductReq{1, 500, "b2"}, errs.ErrInsufficientBalance}, {"金额为零", 10000, DeductReq{1, 0, "b3"}, errs.ErrInvalidAmount}, {"重复单号", 10000, DeductReq{1, 500, "b1"}, nil}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { // 依赖用内存实现替身,不连真实数据库 }) } }测试确认之后,再让它基于测试写实现。这时候它的目标非常明确:让这些用例全绿。你会发现它几乎不会跑偏,也不太会自作主张加一堆你没要求的功能。这就是先立靶子再开枪的道理——没有靶子,它就会自己画一个。
注意:AI 写的测试里有个常见毛病,喜欢断言具体的错误字符串而不是错误值。这在 Golang 里是反模式,因为字符串一变测试就碎。我在任务单里会明确要求用 errors.Is 做断言,禁用字符串比较。
2.3 让它自己找反例
还有一个我很喜欢的用法:实现写完之后,让 AI 扮演"挑刺的代码审查者",专门找自己刚才写的代码的问题。提示词大概是"你现在是资深 Golang 审查者,请找出上面这段实现的至少五个潜在问题,重点是并发安全、错误处理、资源释放、边界条件,不要夸,只列问题"。
这招之所以有用,是因为"生成"和"批判"是两种不同的任务模式,模型在批判模式下的注意力会集中在缺陷上。我实测它能揪出不少真问题,比如忘记 defer cancel、比如在循环里起了 goroutine 但没做错误收集、比如把 map 当成并发安全的结构用。当然它也会报一些假警报,这需要你自己判断。
3. 59 篇开发日志:这套体系是怎么滚出来的
3.1 日志模板与颗粒度控制
先说清楚,这 59 篇日志不是流水账,也不是日记。它是给"未来的自己和未来的 AI"看的工程档案。我的模板固定三段:今天做了什么(客观描述)、为什么这么做(决策理由)、留下什么悬念(未解决的问题、下次的入口)。
颗粒度上我摸索出一个平衡点:一篇日志控制在 300 到 600 字。太短了没信息量,第二天自己都看不懂;太长了写起来有负担,坚持不下来。我一般在每天收工前花十分钟写,正好也是回顾当天决策的过程。
写日志最重要的一点:记录"被否决的方案"。这个是我以前完全忽略的。第一次写的时候我只记做了什么,结果第五周遇到一个问题,明明感觉以前考虑过类似方案,但完全想不起来当时为什么放弃了,只能重新踩一遍坑。后来我加了"否决记录",比如"考虑过用缓存做幂等,但因为要引入额外组件且当前 QPS 不需要,暂时用唯一索引兜底"。这类信息在两个月后回头看,价值比代码本身还高。
3.2 日志反向投喂,给 AI 建一份长期项目记忆
日志写完了还有第二步:它得能喂给 AI。这就要求日志里的关键决策要能被检索。我的做法是每篇日志标题带上模块前缀和日期,正文里的关键决策用固定句式写,比如"决策:xxx。理由:xxx。影响范围:xxx。"这样我只要关键词搜一下,就能把某个模块的所有决策拉出来,拼成一段上下文丢给 AI。
这个习惯带来的直接好处是:当我半个月后重新回到订单模块改东西时,不需要重新读代码理解设计,直接把那五六篇相关日志丢给 AI,它立刻就知道当前的架构约定是什么、哪些方案已经被否了。相当于给它装了一份持续更新的项目记忆,而不是每次都从零开始。
还有个小技巧:日志里我会顺手记下"AI 在这块常犯的错"。比如"这个模块 AI 容易把分转成元、容易漏判负数、容易在事务里做网络调用"。下次在同一个模块让它写代码时,我把这条贴在任务单里,命中率明显提升。这算是一种针对特定代码库的"防错清单",比通用提示词有效得多。
3.3 三类必须记的东西和两类不必记的
必须记的第一类是接口契约变更,尤其是对外暴露的接口。这个不用解释,漏记一次就是线上事故。第二类是数据结构变更,特别是数据库字段的增删改,连带迁移脚本的版本号。第三类是环境相关的坑,比如某个依赖在特定内核版本下需要额外参数、某个测试必须串行跑否则会互相污染。
不必记的有两类。一类是纯粹的语法细节,比如某个函数怎么调用,这个查文档比翻日志快。另一类是当时的情绪,比如"这个 bug 折腾了我三小时",写一次无妨,写多了日志就变成流水账,检索价值下降。
经验:日志的价值不在于记录量,在于可检索性。我宁可用统一句式写 300 字,也不用自由发挥写 1000 字。
4. 商业级项目里的 AI 协作细节
4.1 目录分层与包边界:先把规矩定死
项目一开始我就定了一层不太标准的目录结构,但我觉得对 AI 协作特别友好。大致是:cmd 放入口,internal/handler 只做协议转换,internal/service 放业务逻辑,internal/repo 放数据访问,internal/errs 集中放错误定义,internal/logx 放日志封装,internal/config 放配置加载,pkg 放确实需要对外暴露的通用件。
为什么这套结构对 AI 友好?因为边界清晰。handler 层不许碰数据库,service 层不许直接读配置文件,repo 层不许写业务判断。这些"不许"我在 ARCH.md 里写一次,之后每次任务单里提一句"遵循 ARCH.md 的分层约定",它基本就不会越界。反过来,如果项目结构是那种"哪里方便写哪里"的风格,AI 就会随机选一个文件塞代码,最后你得到一个几百行的巨型文件。
命名风格也是同理,早定早省事。我要求接口用动词开头加 -er 后缀(Deductor、Notifier 这类),错误变量统一 Err 前缀,测试函数统一 Test 加被测对象加方法名。定好之后,AI 生成的代码风格一致性比我预想的好很多,几乎看不出来是"人机混合"的产物。
4.2 自定义 error:AI 最容易写出"看着对"的地方
这块必须单独拎出来讲,因为我在日志里专门记了七次相关踩坑。AI 写错误处理有几大顽疾:一是喜欢用 errors.New 到处建新错误,导致上层没法用 errors.Is 判断;二是喜欢用 fmt.Errorf 包了但不加 %w,链条断掉;三是喜欢定义一大堆平行的错误类型,最后谁也不认识谁。
我的做法是,在 internal/errs 里集中定义一套结构,然后把这个文件作为固定上下文每次都喂给 AI。结构大概是这样:
type AppError struct { Code string // 业务错误码,如 "user.balance.insufficient" Message string // 面向用户的可读信息 Cause error // 底层原因,可为 nil } func (e *AppError) Error() string { if e.Cause == nil { return e.Code + ": " + e.Message } return e.Code + ": " + e.Message + ": " + e.Cause.Error() } func (e *AppError) Unwrap() error { return e.Cause } func New(code, msg string) *AppError { return &AppError{Code: code, Message: msg} } func Wrap(err error, code, msg string) *AppError { if err == nil { return nil } return &AppError{Code: code, Message: msg, Cause: err} }关键的几个约定我在任务单里反复强调:跨层传递错误必须用 Wrap 保留 Cause;判断错误统一用 errors.Is 或 errors.As,禁止比较字符串;对外返回给客户端时只暴露 Code 和 Message,Cause 只写日志不外泄。最后这条是安全考量,Cause 里经常带着 SQL 语句或者内部路径,直接返给前端是隐患。
还有一点很实用:我让 AI 帮我写了一个错误码检查的小工具,编译期扫描所有 New 和 Wrap 的调用,把 Code 参数和 errs 包里的常量做比对,出现的字面量就直接报错。这样能防止有人(包括 AI)随手写个字符串当错误码,导致错误码表失控。
4.3 并发、context 与资源释放:三个高频翻车点
并发这块 AI 的表现很有意思:它能写出语法正确的并发代码,但经常漏掉生命周期管理。我总结下来三个高频问题。
第一个是 context 传递断裂。比如 handler 里带了 ctx,service 层签名里也有 ctx,但中间某个辅助函数偷懒没传,导致超时和取消信号传不下去。我的对策是在任务单里硬性规定:任何可能阻塞的函数第一个参数必须是 context.Context,命名统一用 ctx。并且我让 AI 帮我加了一个静态检查规则,凡是函数体里出现网络调用或者数据库调用但签名里没有 ctx 的,直接标记。
第二个是 goroutine 泄漏。典型场景是起了 goroutine 却没等它返回,或者忘了在 ctx 取消时退出。我后来基本不用裸 go 关键字,统一用 errgroup:
g, ctx := errgroup.WithContext(ctx) g.SetLimit(8) // 控制并发度,防止下游被压垮 for _, id := range ids { id := id g.Go(func() error { return s.fetchOne(ctx, id) }) } if err := g.Wait(); err != nil { return errs.Wrap(err, "batch.fetch.failed", "批量获取失败") }SetLimit 这个细节值得说一下。不加限制地并发,在本地测试时看不出问题,一上量就把下游打挂。我踩过一次,批量任务并发几百个请求,把内部服务打出了限流告警。后来所有批量并发都强制带并发度上限,这个值我一般取下游 QPS 配额的十分之一作为起点,再压测调整。
第三个是资源释放。defer 写是写了,但写在错误的位置。比如在循环里 defer file.Close(),看起来没问题,但如果循环几千次,文件句柄就爆了。或者先判断 err 再 defer,结果 err 分支里直接 return,资源没释放。这类问题 AI 生成的代码里出现频率不低,我在审查时会把所有 defer 单独过一遍。
提示:凡是看到
defer出现在for循环体内,先停下来想三秒。多数情况下应该把循环体抽成函数,让 defer 跟着函数作用域走。
4.4 数据访问层:生成代码要验收,不要迷信
我们这个项目的数据访问层用了代码生成的路子:SQL 写在单独的 .sql 文件里,通过生成工具产出类型安全的 Go 代码。AI 在这个环节的角色是"写 SQL 和写调用方",两边都需要人把关。
SQL 这边,AI 特别容易漏的几件事:分页查询没加稳定的排序字段导致翻页重复或漏数据;统计查询忘了处理 NULL 导致扫描报错;批量更新没限制影响行数导致全表更新。第二条我印象很深,一个 sum 查询在数据为空时返回 NULL,扫进 int64 直接报错,测试环境数据多没暴露,一上线就炸了。后来我要求所有聚合查询必须显式处理 NULL,在 SQL 里用 COALESCE 或者在 Go 侧用 sql.NullXxx,二选一,不能含糊。
调用方这边,重点验收三件事:事务边界对不对、错误有没有正确包装、ctx 有没有一路传下来。事务边界我一般会画个简单的时间线在脑子里过一遍——哪些操作必须在同一个事务里,哪些可以异步。AI 很容易把发消息、调外部接口这类操作塞进事务里,导致事务时间被拉长,锁竞争加剧。这个必须人工改。
另外索引这块,AI 写的查询语句有时会出现在索引列上做函数运算,导致索引失效。我养成了一个习惯:写完关键查询后,用 EXPLAIN 看一眼执行计划。这一步花不了一分钟,但能挡住相当一部分性能问题。慢查询在开发环境几乎看不出来,因为数据量小,全表扫也是毫秒级,一上线就是秒级。
4.5 启动流程与依赖装配:让 AI 帮你画依赖图
依赖装配这块我用了显式注入,没用反射那套自动注入框架。原因是排查问题时,显式注入的代码你能一眼看出谁依赖谁,自动注入虽然写得爽,但出问题的时候调试成本高。AI 在这块的用法是:让它根据构造函数签名,帮我生成装配顺序和缺失依赖的提示。
具体做法是,让 AI 扫描所有 service 和 repo 的 New 函数,列出依赖关系,然后按照拓扑顺序生成启动代码。它做得比我手动排列靠谱,而且当我新增一个模块忘了注册时,让它对比一遍就能发现。启动流程我要求全部显式报错返回,不用 panic,因为启动失败的消息需要清晰可读,比如"配置项 db.max_open_conns 缺失",而不是一个栈追踪。
还有个小细节:优雅退出。这部分 AI 经常写得不够完整,只处理了 HTTP 服务的 Shutdown,忘了后台任务、定时器、连接池的关闭顺序。我的做法是明确在任务单里列出要关闭的组件和顺序,让它照着写。顺序错了会导致正在处理的任务被中途打断,日志里出现一堆"context canceled",排查起来很烦。
5. 踩坑实录与排查速查表
5.1 常见问题速查
我把两个月里反复出现的坑整理成了一张表,基本上每周都会翻一次。
| 现象 | 常见根因 | 快速排查 | 处理方式 |
|---|---|---|---|
| 编译过但运行报错 | 字段名对不上旧结构 | 全局搜字段名 | 先对齐 ARCH.md 再改 |
| 错误判断失效 | 用了 fmt.Errorf 没加 %w | 搜 fmt.Errorf | 换成 errors.Wrap 或加 %w |
| 分页数据重复 | 排序字段不唯一 | 看 ORDER BY | 加主键作为兜底排序 |
| 批量任务打挂下游 | 并发无上限 | 看 go 关键字数量 | 换 errgroup 加 SetLimit |
| 内存缓慢增长 | goroutine 泄漏 | 看 goroutine 数量曲线 | 检查 ctx 退出路径 |
| 事务超时 | 事务里做了网络调用 | 看事务代码块 | 把外部调用移出事务 |
| 日志缺字段 | 用了全局 logger | 搜日志调用点 | 统一走 ctx 里的 logger |
| 空数据扫描失败 | 聚合返回 NULL | 看 SQL 聚合函数 | COALESCE 或 Null 类型 |
这张表的价值在于,它是我自己项目里真实出现过的问题,不是通用清单。你的项目大概率会有一张不同的表,但整理方法是一样的:每次修完 bug,问自己一句"这个坑能不能被一句话描述、被一句话排查",能就记下来。
5.2 几条不太上桌面的经验
第一条,让 AI 改代码时,别用"优化一下"这种动词。优化是主观的,它会把能跑的代码改成另一种能跑的代码,风格还变了,review 起来比重写还累。我会具体说"减少一次循环内分配"或者"把这里的两次数据库查询合并成一次"。
第二条,涉及金额、时间、权限的判断,永远自己写。不是不信任 AI,是这三类逻辑出错成本太高,而且错得不明显。金额的单位转换、时间的时区处理、权限的边界条件,我全部手写,AI 只负责写测试来验证我的手写实现。
第三条,AI 生成的注释要删掉一部分。它写的注释风格是"这个函数做什么",而好的注释应该写"为什么要这么做"。前者在代码改名后会立刻过期,变成误导。我现在的做法是让 AI 不要写解释性注释,只在有非显然决策的地方写一行理由,其余交给清晰的函数命名。
第四条,定期做一次"AI 视角自检"。具体做法是,把当前模块的目录结构和一个典型文件丢给 AI,问它"如果让你在这个模块加一个新功能,你会加在哪个文件、用什么命名、遵循什么错误处理方式"。如果它的回答和你的预期一致,说明这个模块的约定足够清晰;如果它答错了,说明你的目录不够自解释,或者约定没落到文件里,只存在你脑子里。这个自检我做了三次,每次都发现了一些可以改进的地方。
第五条关于测试数据。AI 生成的测试用例,边界值往往只覆盖了 0 和负数,但真实业务里的边界更微妙:刚好达到限额、刚好超出一天、刚好在有效期最后一秒。这类边界需要你把业务规则明确写出来,它才能生成。所以我在写任务单时,会把相关业务规则原文贴进去,哪怕它看起来跟当前函数没关系。
两个月 59 篇日志写下来,我最大的体会是:AI 提效的前提是"你自己先想清楚"。想清楚接口签名、想清楚错误语义、想清楚并发边界,这些事它替不了你,也不该替你。它真正擅长的,是把你想清楚的东西快速落成一致的代码,以及在你想漏的地方帮你挑刺。所以我现在的工作节奏变成了:先花十分钟把任务单和边界写清楚,再让它写测试,测试确认后让它写实现,最后让它挑自己的刺,我负责拍板和验收。这套流程跑顺之后,一个中等复杂度的模块从设计到可测,基本能压在半天之内,而代码质量比我以前纯手写时更稳定,因为约束是提前定死的,不会写着写着就跑偏。如果你刚开始搭这套流程,我建议先从一个独立的小模块试起,把日志模板和提示词骨架固定下来,跑通两三轮之后再往核心业务上铺,这样试错成本最低。