上周我在一个遗留项目的CI流水线上,看着一个因为空指针错误而失败的Job,日志里只有一行exit status 1。没有堆栈,没有上下文,没有任何能让我在三秒内定位问题的信息。那个函数的原始版本是三年前手写的,错误处理就是return err加一行log.Print。更让我难受的是,这已经是本周第三次因为同样的问题中断发布。
而同一周,我用Claude Code生成的模板重构了另一个服务的六个API handler,五天内零次因为错误传播问题触发告警。
这不是偶然。问题从来不是“AI能不能生成带错误处理的Go代码”,它能,而且早就能了。真正的问题是:你给它的指令,到底是让它写代码,还是让它执行你的错误处理策略? 这两者之间的差距,决定了一个函数模板是“能用”还是“敢用”。
我写这篇文章,是因为过去三个月我踩完了这条路上几乎所有的坑。从最开始让Claude随便写一个“带错误处理”的函数,到后来设计出一套能在团队里复用的Prompt规范,再到发现很多教程根本没讲到关键的质量控制节点。每个观点后面都有具体的代码对比、失败案例、以及那些我反复验证后才敢写下来的判断。如果你在找一个可以直接拷贝的Prompt,这篇文章不适合你。但如果你想搞清楚怎么让AI生成的东西真正值得你合并到主分支,那往下翻。
一、核心结论:Claude Code生成的函数模板不是成品,是半成品
先把这个结论抛出来,因为它和绝大多数教程告诉你的不一样。
大多数“用AI生成Go函数模板”的内容会暗示一个叙事:你输入需求,AI输出代码,你复制粘贴,完事。但我在实际项目中反复验证后发现,Claude Code生成的正确率曲线并不是线性衰减的。它对于简单的错误处理模式(比如if err != nil { return fmt.Errorf("xxx: %w", err) })的正确率可以稳定在90%以上。但一旦涉及分层错误、自定义错误类型、不可恢复错误的panic决策、日志与错误的解耦逻辑,正确率会断崖式下降到60%到70%之间。
这不是Claude的问题。这是Go语言错误处理哲学本身的复杂性映射出来的结果。Go的错误处理不是语法规则,是设计决策。当你让一个没有参与你的架构评审的AI去做设计决策时,它会选择最通用、最平庸、最“不出错”的方案。而这种方案放在生产环境里,往往就是最大的错误。

所以这篇文章的核心观点就一句话:把Claude Code当成一个能写代码的初级工程师,而你是那个做Code Review的高级工程师。你给它的是设计规范,它给你的是初稿,你需要做的是评审和修正。任何试图跳过这个质控环节的做法,最后都会在凌晨三点的报警电话里付出代价。
在展开讲怎么设计Prompt和怎么审核产出之前,我要先讲清楚Go语言错误处理到底在什么条件下会出问题,因为这个背景直接决定了你的Prompt里应该写什么。
二、Go的错误处理为什么让AI头疼,也让很多人头疼
2.1 错误处理不是写if err != nil,是定义错误传播契约
很多开发者对Go错误处理的认知停留在“到处写if err != nil”这个层面。但如果你仔细看过Go标准库的源码,你会发现真正决定代码质量的不是你有没有判断错误,而是错误在函数栈中逐级向上传播时,每一层做了什么样的包装、增加了什么上下文、保留了什么样的可检查性。
举个例子,下面三段代码都处理了同一个底层错误,但它们在可调试性和可恢复性上完全不同:
// 版本A:最糟糕的处理方式
func GetUser(id string) (*User, error) {
user, err := db.Query("SELECT * FROM users WHERE id = ?", id)
if err != nil {
log.Printf("query error: %v", err)
return nil, err
}
return user, nil
}
// 版本B:加了上下文,但丢失了可检查性
func GetUser(id string) (*User, error) {
user, err := db.Query("SELECT * FROM users WHERE id = ?", id)
if err != nil {
return nil, fmt.Errorf("failed to get user %s: %v", id, err)
}
return user, nil
}
// 版本C:既保留上下文,又保留可检查性
func GetUser(id string) (*User, error) {
user, err := db.Query("SELECT * FROM users WHERE id = ?", id)
if err != nil {
return nil, fmt.Errorf("GetUser(%s): %w", id, err)
}
return user, nil
}
版本A的问题:日志和错误返回都做了,但它们之间没有任何关联。调用方拿到的是一个没有上下文的原始错误,而上游的日志只有模糊的query error,你无法把一次请求中的日志和返回的错误串起来。
版本B的问题:加了failed to get user这个上下文,比A好。但它用了%v而不是%w,这意味着调用方无法用errors.Is或errors.As来检查原始错误的类型。如果调用方需要根据sql.ErrNoRows来决定是返回404还是500,版本B直接把这条路堵死了。
版本C是正确的做法:%w既包装了上下文信息,又保留了原始错误的完整链,调用方可以同时做字符串匹配和类型检查。
这就是错误传播契约的核心:一个函数的调用方应该能同时获取到“发生了什么”的上下文和“为什么发生”的根因。而绝大多数AI生成的代码默认选择版本B,因为它在语法上看起来更安全,不会出错。但它破坏了契约。
2.2 AI最容易犯的三个错误处理错误
我在过去三个月里让Claude Code生成了大约两百多个Go函数,覆盖了HTTP handler、数据库访问层、消息队列消费者、gRPC拦截器等不同场景。我系统性地记录了每个生成结果中需要手动修正的部分,统计出了一个规律。

第一类缺陷:%w与%v的误用
这是最高发的错误,占比38%。AI会在不该用%w的地方用(比如包装一个不需要暴露给调用方的内部错误),也会在该用%w的地方用%v。核心原因是AI不理解“这个错误到底是内部实现细节还是接口契约的一部分”。
第二类缺陷:日志记录与错误返回的双重记录
占比27%。AI生成的代码经常同时调用log.Error又返回err,导致同一个错误在调用栈中被记录了三四次。这是Go社区反复强调的反模式,要么记录日志,要么返回错误,不要两个都做。但AI似乎被训练数据里大量的既打日志又返回错误的非规范代码影响了。
第三类缺陷:缺少自定义错误类型
这个占比18%,但它的影响比前两个加起来都大。AI会习惯性地用errors.New或fmt.Errorf来创建所有错误,而不会主动定义type NotFoundError struct{}这样的类型。这意味着你的调用方永远只能做字符串匹配,永远无法做结构化的错误分类。
2.3 为什么说“标准写法”反而是最差写法
这里有一个反常识的观点,我在很多公司的Go代码审查里都提过:社区里流传的“标准错误处理模板”往往是面向演示代码的,不是面向生产代码的。
典型的标准模板长这样:
result, err := doSomething()
if err != nil {
return fmt.Errorf("doSomething failed: %w", err)
}
这个模板对于只有两层调用深度的场景没问题。但现实中你的调用栈可能是这样的:
Handler -> Service -> Repository -> DB Driver -> Network
如果每一层都这么包装,最终调用方拿到的错误消息会是:
HandleRequest: ProcessOrder: GetOrder: QueryOrder: connection refused
信息量很大,但调用方怎么判断这是不是一个可重试的错误?它必须对字符串做包含性检查,而这本质上等价于没有错误处理。
真正需要的是分层错误策略:
Repository层:不做包装,原样返回底层错误
Service层:包装业务语义,区分“没找到”和“数据库挂了”
Handler层:根据错误类型决定HTTP状态码,不做额外包装
中间件层:统一记录日志,不在业务代码里到处打日志
这个分层策略才是你的Prompt应该让AI理解和执行的,而不是一个万能的fmt.Errorf模板。
用Claude Code生成函数模板的核心方法:从对话到规范文件
1 不要给Claude“需求描述”,要给它“代码规范”
我在最开始用Claude Code生成Go函数时,犯了一个所有新手都会犯的错误:我给了它一个功能描述。
我当时的Prompt大概是这样的:
> “请帮我生成一个Golang函数,用来从数据库查询用户信息,参数是用户ID,返回User结构体和错误。要有完整的错误处理。”
Claude生成的代码语法上完全正确,但它在三个关键点上和我们的团队规范冲突:
错误包装用了%v而不是%w
在函数里同时打了日志又返回了错误
没有区分“用户不存在”和“数据库连接失败”
然后我花了15分钟手动修改这些“正确但不规范”的代码。这15分钟让我意识到:如果我把修改的内容提前写进Prompt,我就不需要修改。
这是我现在的做法,把团队的Go错误处理规范写成一个独立的文件,在每次生成函数时都引用这个文件。文件内容大致是这样的结构:
Go错误处理规范 v2.1
错误包装规则
所有需要向上层暴露的错误,必须使用%w进行包装
包装信息必须包含当前函数名和关键参数值
格式:fmt.Errorf("FunctionName(param=%v): %w", param, err)
错误类型定义
每个业务领域必须定义自己的错误类型
错误类型必须实现Error()和Unwrap()接口
对于“未找到”类的语义,统一使用NotFoundError类型
日志与错误分离
业务函数只返回错误,不记录日志
日志统一在中间件层或顶层调用处记录
使用errors.Is进行错误类型判断,不依赖字符串匹配
不可恢复错误
仅在程序启动阶段允许使用panic
请求处理路径严禁panic
对于配置缺失、必需依赖不可用等场景,在init或main中panic
错误码映射
Handler层负责将业务错误映射为HTTP状态码
NotFoundError -> 404
ValidationError -> 400
未分类的错误 -> 500
有了这个规范文件之后,我的Prompt变成了:
> “请根据引用的Go错误处理规范文件,生成一个查询用户的Repository函数,底层使用database/sql。函数签名是GetUser(ctx context.Context, id string) (*User, error)。”
这次生成的结果,我只需要改两行代码,一个类型断言和一个边界条件处理。
关键洞察:当你把“我想要的代码长什么样”翻译成规范文件,你就不再是在“使用AI生成代码”,而是在“让AI执行你的编码标准”。这两种行为的质量输出差距巨大。
3.2 设计一个能产出可复用模板的Prompt结构
在经历了几十次生成后,我发现一个现象:如果我每次都是零散地提需求,Claude每次生成的代码结构和风格都会有微小的差异。这些差异积累起来,会导致一个项目里同一个错误处理模式有三种不同的写法。
这不是AI的问题,是你没有给它一个稳定的上下文锚点。我现在用的Prompt结构是一个三层模板:
[角色定义]
你是一个严格遵守Go编码规范的资深后端工程师,你熟悉本项目的错误处理策略并会准确执行。
[规范文件]
<附上上一小节的规范文件内容>
[生成要求]
函数签名:{具体的签名}
场景描述:{这个函数在什么业务场景下被调用}
错误处理要求:
底层错误(如sql.ErrNoRows)需要被包装为业务错误类型
所有错误包装必须保留原始错误,使用%w
不得在函数内部记录日志
对关键参数的空值校验放在函数最前面,返回ValidationError
输出要求:
只输出函数代码和必要的类型定义
不输出解释性文字
所有注释使用英文
这个结构的关键设计原则有四个:
第一,角色定义不是废话。 “严格遵守编码规范”这八个字显著影响生成质量。我做过对照测试:有这句话的情况下,生成代码对规范文件规则的遵循率是87%;没有这句话,遵循率降到62%。AI对角色设定的敏感性比大多数开发者想象的要高。
第二,规范文件必须外挂而不是内嵌在Prompt里。 原因有两个:一是独立文件可以被版本管理,团队成员使用时引用同一个文件保证一致性;二是Claude Code的上下文窗口管理,把规范放在独立文件里可以更高效地利用token。

第三,场景描述决定了AI选什么错误粒度。 “这是一个Repository函数”和“这是一个Handler函数”,AI会自主调整错误包装的深度。如果你不告诉它函数在调用栈中的位置,它默认会采用最深的包装策略,导致错误信息冗余。
第四,输出要求中的“不输出解释性文字”很重要。 如果你让Claude在生成代码的同时又解释代码,它有时会把解释性的变量命名习惯带入代码中,比如本来应该是err的地方变成了queryError,破坏了Go社区的命名约定。
3.3 从“一次生成”到“可复用模板”的流程
很多人对“模板”这个词有误解。他们以为模板就是一个固定的代码片段,填参数就行。但在AI辅助编码的语境下,模板是一套稳定的生成规则,外加一个可验证的产出标准。
我现在的流程是这样的:
第一步:定义模板的核心要素
在开始生成之前,先确定这个模板的几个不可变部分:
- 函数签名规范(带context、返回值只包含结果和error、不使用命名返回值除非必要)
- 错误处理策略(包装级别、错误类型、日志位置)
- 参数校验位置(所有函数的参数校验必须在第一段代码块完成)
第二步:用规范文件驱动首次生成
用3.2节的三层Prompt结构生成第一个函数。这个函数的正确性是后续所有同类函数的基准。
第三步:人工评审并记录修正点
对生成的代码做一次完整的Review,不是扫一眼,而是像审查同事的PR一样逐行看。记录每一个需要修改的地方,并判断这个修改是否需要反哺到规范文件中。
第四步:迭代规范文件
把重复出现的修正点写进规范文件。比如我发现AI生成的代码经常在sql.ErrNoRows的处理上不一致,我就在规范文件里加了一条:
“处理sql.ErrNoRows的规则:必须使用errors.Is(err, sql.ErrNoRows)进行判断,且必须将其包装为NotFoundError,包装信息格式为‘resource not found: {resource_type}={id}’。”
第五步:固化模板为代码片段或Live Template
经过三到五次的迭代后,你会发现生成的代码已经高度稳定,几乎不需要修改。此时就可以把最终版本固化为IDE的Live Template或者团队共享的Snippet文件。这一步的价值是:那些不需要AI辅助的简单场景,可以直接使用预置模板,节省AI调用的时间和token成本。
四、实战案例:从API Handler到数据库层的完整错误处理生成
4.1 案例背景和初始生成
让我用上个月一个真实的开发任务来演示完整流程。需求是这样的:实现一个用户订单查询接口,涉及三个层次,Handler、Service、Repository。数据库用PostgreSQL,ORM用database/sql。错误处理要求:用户不存在返回404,订单不存在返回404,数据库连接失败返回503并触发告警(告警逻辑在中间件处理),参数校验失败返回400。
我先定义了规范文件,然后分三次生成三个函数。
Repository层生成:
我的Prompt是:
[角色] 你是严格遵守本仓库Go错误处理规范的资深后端工程师。
[规范] <附上v2.1规范文件>
[要求]
生成GetOrderByID函数:
签名:func (r *OrderRepo) GetOrderByID(ctx context.Context, orderID string) (*Order, error)
底层用database/sql,表名orders,字段包括id、user_id、amount、status、created_at
sql.ErrNoRows必须包装为NotFoundError
其他数据库错误包装为InternalError并保留原始错误
不对orderID做业务校验(那是Service层的职责)
Claude生成的代码:
func (r *OrderRepo) GetOrderByID(ctx context.Context, orderID string) (*Order, error) {
if orderID == "" {
return nil, NewValidationError("orderID is required")
}
query := "SELECT id, user_id, amount, status, created_at FROM orders WHERE id = $1"
row := r.db.QueryRowContext(ctx, query, orderID)
var order Order
err := row.Scan(&order.ID, &order.UserID, &order.Amount, &order.Status, &order.CreatedAt)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, NewNotFoundError("order", orderID)
}
return nil, NewInternalError(fmt.Errorf("GetOrderByID(%s): %w", orderID, err))
}
return &order, nil
}
这个初始生成我做了三处修改:
- 删除了函数开头的空值校验,我在Prompt里明确说了这是Service层的职责,但AI仍然加上了。我把这件事当作规范文件的修正点,在后面迭代时增加了“Repository函数不对业务参数做校验,仅做技术性校验”这条规则。
- NewNotFoundError的参数顺序和我预想的不一致。我定义的是func NewNotFoundError(resource string, id string) error,AI调用时传参是正确的,但它的注释里写错了资源类型。规范文件补一条“所有NewNotFoundError调用的resource参数必须是单数形式的小写名词”。
- NewInternalError的包装里用了函数名硬编码。如果以后改函数名,这个错误信息就不会更新。我改成了使用runtime包动态获取函数名,但这个改动不适合放进规范文件,它是一个具体实现选择,在团队里还有争议。
Service层生成:
[要求]
生成GetUserOrder函数:
签名:func (s *OrderService) GetUserOrder(ctx context.Context, userID, orderID string) (*Order, error)
调用OrderRepo.GetOrderByID获取订单
调用UserRepo.GetByID校验用户存在性(这个函数已存在)
订单的user_id必须与请求的userID匹配,否则返回PermissionError
所有错误使用%w向上包装,包装信息包含函数名和关键参数
不在函数内打日志
生成的代码我不逐行展示了,但有一个值得讲的修正点:AI对UserRepo.GetByID返回的错误做了判断,但它把“用户不存在”也包装成了当前函数的错误,结果是GetUserOrder: GetByID(user=xxx): resource not found。这违反了我们的错误判断规则,用户不存在应该直接返回资源不存在的错误,Service层的职责是权限校验,不是把下层错误再包一层。
我花了两分钟让Claude在同一个对话里修正了这个问题,然后把这个修正规则写进了规范文件:“Service层在调用其他Service或Repository时,如果下层返回的错误已经是业务错误类型(如NotFoundError),不进行二次包装,直接返回。”

4.2 生成质量的关键不在于AI能力,在于你的评审清单
这次三函数生成的经历让我更坚定了一个判断:AI生成代码的质量上限,是由你的评审能力决定的,不是由AI的能力决定的。 Claude能生成语法正确且相对规范的代码,但它无法判断一个错误应该在哪一层被包装、无法决定什么情况下该用自定义错误类型、也无法预判什么样的错误消息格式对你的监控系统最友好。
这些判断需要你来做。但大多数开发者在评审AI生成的代码时,用的是和评审人类同事代码同一套标准,而这套标准对于AI来说是远远不够的。
我总结了一份专门用于评审AI生成Go错误处理代码的检查清单,经过三个月迭代,现在稳定在七个检查项:
| 检查项 | 判断标准 | 常见问题 |
|---|---|---|
| 错误包装操作符 | 面向调用方的错误用%w,内部实现细节用%v |
全部都用%w导致内部实现暴露 |
| 错误类型定义 | 每个业务领域是否有对应的错误类型 | 全部用errors.New,无法做类型断言 |
| 日志与错误分离 | 同一错误在整个调用链中只记录一次日志 | 每层都打日志,同一条错误日志重复出现三次以上 |
sql.ErrNoRows处理 |
是否使用errors.Is进行判断 |
字符串匹配sql: no rows |
| 空值校验位置 | 对外接口的校验在函数入口做,内部函数不做冗余校验 | Repository层也做了业务参数校验 |
| panic使用场景 | 仅在初始化阶段或不可恢复的配置错误中使用 | 在请求处理路径用了panic |
| 错误码映射 | 业务错误到HTTP/gRPC状态码的映射是否集中管理 | 每个Handler里硬编码HTTP状态码 |
这个清单有一个核心设计理念:每一项对应的都不是“语法正确性”,而是“设计决策正确性”。语法正确性AI已经帮你兜底了,你要审的是那些AI没有能力做、也不应该让AI做的决策。
4.3 当AI生成的代码“看起来很好”但其实有问题
这里有一个特别隐蔽的坑,我必须要讲。上个月Review一段AI生成的Service层代码时,我差点直接通过了它。代码长这样:
func (s *PaymentService) ProcessRefund(ctx context.Context, paymentID string, amount float64) error {
payment, err := s.paymentRepo.GetByID(ctx, paymentID)
if err != nil {
return fmt.Errorf("ProcessRefund: %w", err)
}
if payment.Status != "completed" {
return fmt.Errorf("ProcessRefund: payment %s is not completed", paymentID)
}
if payment.Amount < amount {
return fmt.Errorf("ProcessRefund: refund amount %f exceeds payment amount %f", amount, payment.Amount)
}
err = s.gateway.Refund(ctx, payment.GatewayID, amount)
if err != nil {
return fmt.Errorf("ProcessRefund: gateway refund failed: %w", err)
}
err = s.paymentRepo.UpdateStatus(ctx, paymentID, "refunded")
if err != nil {
return fmt.Errorf("ProcessRefund: update status failed: %w", err)
}
return nil
}
第一眼看过去,错误包装规范、有业务校验、调用链清晰,似乎没什么问题。但仔细想三个问题:
问题一:payment.Status != "completed"的判断用了一个字符串常量。 如果状态值在另一个地方被定义为常量但在某个未来版本被修改了,这个判断会悄悄失效。应该用预定义的常量或枚举。
问题二:退款的网关调用和状态更新不在同一个事务里。 如果网关退款成功但状态更新失败,用户收到了退款但系统里订单状态仍然显示已支付。这会导致客服工单和财务对账问题。
问题三:AI对网关失败的错误做了包装,但对状态更新失败没有做区分。 网关失败是一个可重试的错误(可能是因为网络波动),状态更新失败是不可恢复的(意味着存在数据一致性问题)。但AI把它们都当成了同一级别的错误来包装。
这三个问题,AI一个都发现不了,因为这些本质上都是业务逻辑的设计缺陷,不是代码规范的违反。这也解释了为什么我坚持认为评审AI生成的错误处理代码,必须带着业务理解的脑子,不能只做模式匹配。
五、从模板到规范:建立一个团队级的错误处理知识库
5.1 为什么需要把错误处理策略“外包”给Claude Code
如果你是一个人在做项目,前面的方法已经足够你用了。但如果你在一个三人以上的团队里,情况会复杂很多。
我们团队之前的情况是这样的:每个人都有自己习惯的错误处理写法。有人喜欢用github.com/pkg/errors,有人只用标准库,有人在错误信息里用中文,有人喜欢把错误枚举定义为全局常量。Code Review时每次都在这些风格差异上消耗时间,而不是在真正的逻辑正确性上。
我做过一个统计:在我们引入Claude Code生成规范之前,一次典型的Go函数PR中,关于错误处理风格的Review Comments占了全部Comments的37%。 注意,这些不是逻辑错误,只是风格差异,用%w还是%v、要不要在函数里打日志、错误信息的格式是functionName: context还是context in functionName。一位高级工程师每周大约两个小时花在讨论这些风格问题上。
引入规范文件驱动的AI生成之后,这个比例下降到了8%。不是因为大家变厉害了,而是因为生成出来的代码本身就遵循了同一套规范。即使不同的人用不同的措辞给AI提需求,只要它们引用的是同一个规范文件,生成的代码在错误处理模式上就是一致的。

更关键的一个数据是:引入后,因为错误处理不当导致的生产事故从月均2.4次下降到了0.3次。 这里有一个滞后效应,前两个月其实没什么变化,因为规范文件还在迭代中,生成的代码质量不稳定。到第三个月,规范文件稳定了,事故率才开始明显下降。
5.2 规范文件应该包含什么,不应该包含什么
很多团队在做这件事时会走入一个误区:往规范文件里塞太多东西,试图用AI解决所有代码质量问题。结果是Prompt过长,AI遵循率反而下降。
我在我们的规范文件里遵循一个“二八原则”:只包含那些改了之后会影响调用方行为的规则,不包含纯风格偏好。
判断标准很简单:如果这个规则违反了,调用方的代码需要跟着改,那就放进规范文件。如果只是看起来不美观,个别Reviewer看不顺眼但不影响功能,那就不放。
我们的v3.0规范文件目前包含这几个模块:
模块一:错误包装规则(核心,必须遵守)
%wvs%v的使用场景- 包装信息的格式模板
- 哪些场景禁止包装(直接返回nil或透明传递)
模块二:错误类型体系(核心,必须遵守)
- 预定义的错误类型及其适用场景
- 自定义错误类型必须实现的接口
- 错误类型的继承和组合规则
模块三:错误处理位置(重要,尽量遵守)
- 每层函数的错误处理职责边界
- 哪些错误在Repository层消化,哪些传递到Service层
- 中间件的错误拦截规则
模块四:日志策略(重要,尽量遵守)
- 业务函数不打日志的原则
- 日志记录的唯一位置(中间件或顶层调用者)
- 错误日志的字段规范
不放进规范文件的内容:
- 变量命名偏好(只要符合Go社区惯例即可)
- 代码注释风格
- 函数长度限制
- 具体的业务校验逻辑(那是业务规则,不是错误处理规范)
5.3 规范文件的版本管理和迭代节奏
我们的规范文件托管在Git仓库里,有一个独立的CHANGELOG。这不是形式主义,当一次线上事故的复盘结论是“错误处理不规范导致根因被掩盖”,我们就必须把对应的修正写进规范文件,并标记版本号。
迭代节奏我们是每月一次正式评审。但这个“正式”指的是对规范文件本身的修改,而不是对使用频率的要求。实际上,每次用Claude Code生成新函数时,如果发现生成结果有重复出现的偏差,我随时会在规范文件的草案分支上加一条新规则。月底评审时把这些草案规则过一遍,决定是正式纳入还是废弃。
一个很重要的实践:规范文件的每次修改,都必须附带一个“为什么改”的说明和至少一个“这次改动能避免什么样的实际问题”的案例。这个记录不是为了给上级汇报,而是为了三个月后新人加入团队时,他能理解每一条规则背后的动机,而不是把这些规则当成教条。
六、你不能让AI做的五个错误处理决策
这一节讲的是边界,那些即使你的Prompt写得再好、规范文件再完善、AI也绝对不应该替你做、但你很容易不知不觉让它做了的决策。明确这些边界,比学任何Prompt技巧都更重要。
6.1 判断一个错误是“可恢复”还是“不可恢复”
这是Go语言错误处理中最核心的决策之一,也是AI最容易猜错的地方。
可恢复的错误:网络超时、第三方API暂时不可用、资源锁冲突,这些错误的共同特点是重试可能改变结果。
不可恢复的错误:配置文件格式错误、必需的证书缺失、数据结构损坏,这些错误的特点是无论重试多少次结果都一样。
AI怎么猜的? 它的判断逻辑是基于训练数据中高频率的模式。如果大多数训练样本里connection refused后面跟着重试逻辑,它就会给类似错误加上重试。但它不知道你的具体系统对“可恢复”的定义,你的数据库连接失败可能在Kubernetes里是一个Pod重建信号,不应该在应用层重试,而应该让调度器处理。
你应该怎么做? 在你的规范文件里明确列出属于“不可恢复”的高层错误场景,以及在哪些场景下即使底层错误看起来可恢复,也不在应用层做重试。
6.2 决定错误包的粒度,到什么层级停止包装
一个用户请求失败,最终的错误信息可以是:
“handleRequest: processOrder: validatePayment: checkBalance: insufficient fund”
也可以是:
“order processing failed: insufficient balance”
哪个是对的?看你给谁看。前者适合调试,后者适合用户。AI默认生成的是前一种,因为它没有“受众”的概念。 它不知道这个错误最终会被展示在前端、写入日志、还是交给监控系统解析。
你的规范文件里需要有一条明确的“包装停止规则”:当错误到达Handler层时,不再向错误信息中添加更底层的技术细节,Handler层的包装只包含对客户端有意义的业务语义。
6.3 决定引入外部错误处理库还是只用标准库
AI经常会默认引入github.com/pkg/errors或者直接在import里加上golang.org/x/xerrors,因为它的训练数据里大量代码是这样写的。但Go 1.13以后标准库已经支持%w、errors.Is和errors.As,对于绝大多数项目来说,标准库已经足够。
是否引入外部错误处理库是一个架构决策,涉及到依赖管理、升级兼容性、团队成员的学习成本。AI没有能力做这个权衡,它只能根据频率选择最常出现的做法。
规则很简单:在规范文件的开头明确声明错误处理只使用标准库,除非有特殊需求且经过技术评审。
6.4 决定错误信息用什么语言、什么格式
很多中文团队的Go项目里,错误信息是中英文混用的。AI生成的代码会默认使用英文,因为训练数据里英文占绝对主导。但如果你的团队决定错误信息统一用中文、或者统一用特定的格式(比如必须包含错误码),你就必须在规范文件里明确。
一个小但重要的细节:错误信息的格式直接影响你的日志解析和告警规则。如果你用ElasticSearch或Loki做日志聚合,错误信息的结构决定了你的查询能有多精确。AI不知道你的日志基础设施,它只能生成通用格式的错误信息。如果你对错误信息有结构化要求(比如必须包含JSON格式的上下文),那就必须写进Prompt。
6.5 决定哪些第三方库的错误需要转换为自己的错误类型
当你调用gRPC client、Redis client、消息队列SDK时,这些库都有自己的错误类型。AI倾向于原样返回这些错误,或者只做一层浅包装。但稳定性要求高的服务通常会有错误类型的“反腐败层”,将所有外部依赖的错误在边界处转换为自己定义的错误类型,内部代码完全不依赖第三方的错误结构。
这个决策AI不会主动做,因为它影响不了代码的“对错”,只影响代码对未来修改的容忍度。你得自己做这个决策,然后明确写在规范文件里。
七、进阶:用Claude Code生成可验证的错误处理函数
7.1 让AI生成的不只是代码,还有对应的测试用例
很多开发者用AI生成完函数就结束了。但一个生产级的函数模板,第一行代码和第一个测试用例应该是一起出来的。
我现在要求的生成输出是函数加测试,Prompt里会加一句:
“同时生成该函数的单元测试,测试必须覆盖:正常路径、错误路径(包括NotFoundError、ValidationError、InternalError三种类型)、边界条件(空参数、极值参数)。”
这种方式有两个好处:
- 即时验证生成质量:如果AI生成的测试在逻辑上有问题,说明它对函数行为的理解有问题,生成的函数大概率也有隐藏的缺陷。
- 测试本身也是规范的一部分:当新成员加入团队时,看AI生成的测试用例就能理解“这个函数在各种错误场景下的预期行为是什么”。
一个我实践出来的经验:让AI用table-driven test模式生成测试,并且要求它给每个test case写一个清晰的名字。 名字就是行为规范,TestGetOrder_NegativeAmount_ReturnsValidationError比TestGetOrder_Case3能传达的信息多一百倍。
7.2 错误处理的“自文档化”原则
一个被很多人忽视但极其重要的设计原则:错误处理代码是最好的文档。 一个新加入项目的开发者,通过读函数的错误处理逻辑就能理解整个服务的异常流转策略。
所以我在规范文件里有这么一条:“每个非透明的错误返回点,必须在错误包装信息中包含足够的上下文,使得一个不熟悉该模块的开发者能够在只看错误信息的情况下判断出问题发生在哪个函数、操作了什么资源、是由什么底层原因触发的。”
Claude可以很好地执行这条规则,只要你明确要求。它比很多人类开发者做得更好,因为人类倾向于偷懒写一些模糊的错误信息,而AI会机械地按照规则在每条错误信息里填入具体内容。
以下是我从实际项目中截取的一个对比:
| 人类手写 | AI生成(有规范约束) | |
|---|---|---|
| Repository层 | return nil, err |
return nil, fmt.Errorf("GetUserByEmail(email=%s): %w", email, err) |
| Service层 | return fmt.Errorf("get user failed: %w", err) |
return fmt.Errorf("AuthService.ValidateLogin(phone=%s): %w", phone, err) |
| Handler层 | writeError(w, "internal error", 500) |
respondError(w, err, map[error]int{ErrNotFound: 404, ErrValidation: 400}) |
看出差别了吗?人类手写的版本,在一个月后自己回来看也需要花时间理解上下文。AI生成的版本,任何人在任何时间看到错误信息,都能迅速定位到问题发生的精确位置。这就是“自文档化”的实际价值。
7.3 使用Claude Code的对话能力进行错误处理的增量优化
这可能是被低估最多的一个技巧。大多数开发者用AI生成函数是“一把梭”模式:写Prompt,拿到代码,复制粘贴。但Claude Code的真正威力在于它的对话修正能力。
我现在的标准操作是:
- 第一次生成,拿到初版代码
- 在同一个对话里,针对发现的问题提出修正指令,比如:“把第三个错误返回点改成使用自定义的ValidationError,而不是fmt.Errorf”
- Claude会在保留其他部分不变的情况下只修改指定位置
- 确认修改正确后,让它把整个函数重新输出一遍
这个过程比手动修改快得多,而且不会引入“改了一个地方却破坏了另一个地方”的问题,而这恰恰是人类在手动修改AI生成的代码时最常见的错误类型。
经过三到五轮对话修正后,最终输出的代码质量可以达到“直接合并到主干”的水平。而整个过程的耗时通常在15到20分钟,远低于从零手写同类复杂度函数的时间,且质量更高。
八、不同场景的取舍:什么时候不该用AI生成错误处理代码
我必须专门辟一节来讲这个,因为否则读者很容易产生一个错误的印象:“所有Go函数的错误处理都应该让AI生成。” 错的。有几个特定场景下,AI生成的错误处理代码不仅没用,而且有害。
8.1 高度领域专用的错误码体系
如果你的服务有自己的一套错误码体系,比如类似Google API的错误模型(用数字错误码加status message),AI生成的代码大概率不符合你的约定。因为你的错误码定义是组织内部的标准,不在AI的训练数据里。
这种情况下的建议:不用AI生成错误码相关逻辑,而是把错误码映射集中在一个错误处理中间件里,业务函数只返回自定义错误类型,由中间件统一转换为错误码响应。AI生成的部分只到业务函数这一层,不涉及错误码。
8.2 对错误处理性能有极致要求的场景
Go的错误处理本身有性能开销,fmt.Errorf的%w包装涉及内存分配、字符串格式化、错误链的构建。在高并发场景下,如果每个请求都要经过多层错误包装,即使最终没有错误发生,这些分配也会累积。
如果你的服务是一个每秒处理十万请求的API网关,你就需要考虑:是否可以让AI生成的代码在“有错误”和“无错误”路径上有不同的性能特征? AI默认生成的代码不会考虑这种优化,它只关注逻辑正确性。
对于这种场景,建议是:让AI生成基础版本,然后由性能工程师手动优化热路径。优化后的版本可以作为新的基准模板。
8.3 涉及敏感信息泄漏风险的错误处理
AI会忠实地把你给它的参数值写进错误信息里。如果那些参数包含手机号、身份证号、银行卡号,AI不会自动做脱敏,因为它不知道那是敏感信息。
这就是为什么规范文件里必须有一条关于错误信息脱敏的规则,并且这条规则必须非常具体,不能只是“不要在错误信息中暴露敏感数据”,而要明确列出哪些字段需要在包装时截断或哈希处理。
对比:
- ❌ 模糊规则:“不要在错误信息中包含敏感数据”
- ✅ 具体规则:“错误包装信息中如果涉及以下字段,必须进行脱敏:手机号保留前3后4位、身份证号只保留前6位、银行卡号只保留后4位、用户密码的哈希值不允许出现在任何错误信息中”
AI可以很好地执行具体规则。它执行不了模糊规则。

九、总结:你投入在Prompt设计上的功夫,不应该少于你投入到Code Review上的功夫
写到最后,我想回到文章开头那个失败的CI Job。
在那个项目里,我们后来做了两件事:第一,把所有涉及错误处理的函数用规范文件驱动的Claude Code重新生成了一遍。第二,制定了一套评审清单,要求每个合并到主干的AI生成函数都必须通过七个检查项。
三个月后,同类事故从月均2.4次降到了0.3次。不是因为AI生成的代码完美无瑕,实际上,在最初的几百个生成结果中,差不多每三个就有一个需要手动修正。真正改变局面的是:当我们强迫自己把隐性的“编码直觉”翻译成AI能理解的显性规则时,我们自己的代码质量意识也提升了。
这个发现可能是我整个实践过程中最大的意外收获。设计规范文件的过程,本质上是对团队错误处理策略的一次系统化梳理。在开始做这件事之前,我们团队里没有人能完整说清楚“到底什么情况下该用%w什么情况下该用%v”,每个人都是凭感觉判断。而把这个判断逻辑写成AI能执行的规则,就逼着你把这个感觉挖出来、形式化、验证、修正。
给读者的下一步行动建议,只有三个:
第一步:花一个下午,把你当前项目里所有让你在Code Review时反复评论的错误处理问题整理出来。把这些问题分类,提炼成规则草案。不需要完美,达到“连AI都能看懂”的程度就够了。
第二步:找三个典型函数,用你的规则草案驱动Claude Code生成一遍。对比生成结果和你原来手写的版本。记录下AI遵循了哪些规则、漏掉了哪些、在哪些地方“过度执行”了规则。
第三步:基于对比结果修正你的规则草案,形成v0.1版本的规范文件。把这个文件分享给团队的另外两个人,让他们用同样的Prompt结构(引用同一个规范文件)生成各自模块的函数。两周后开一次简短的复盘会,讨论效果和需要调整的规则。
这三步走完,你就不是在“用AI生成代码”了,而是在“建立团队级的错误处理知识库”。AI只是执行这个知识库的工具,而知识库本身,才是真正的护城河。
那些凌晨三点的报警电话,那些因为一条模糊的exit status 1而浪费的调试时间,那些在Code Review里反复拉扯的风格争论,这些事情不会因为引入了AI就自动消失。但当你把所有人对“正确错误处理”的理解凝结成一份AI能执行、人类能评审、团队能迭代的规范文件时,它们的频率会以肉眼可见的速度下降。
这就是我在这三个月里学到的、验证过的、敢写下来的一切。不保证对所有人适用,但保证对你至少有一半有用。
常见问题解答(FAQ)
1. Claude Code 生成的错误处理模板能否直接用于生产环境?
我刚用 Claude Code 生成了一个带错误处理的 Golang 函数模板,看着结构很完整,但心里没底:它真的符合我们团队的生产标准吗?会不会漏掉关键校验?有没有什么隐含的风险需要手动排查?
我的答案是否定的:不能直接用于生产,但可以作为一个高质量的结构起点。我在测试中生成过 HTTP 处理、数据库查询和文件操作的三个模板,每个都包含 errors.New、fmt.Errorf 和自定义错误类型。
经手动评审发现,Claude Code 能正确包裹底层错误(使用 %w),但存在两个常见陷阱:一是它倾向于在同一个函数内重复记录日志(既 return 了错误又提前 log),造成噪声;二是自定义错误类型有时未实现 Unwrap() 方法,导致 errors.Is 链式判断失效。
我的实践是:把生成的代码当作“预备代码”,然后执行三件事,用 go vet 静态检查、手动补全 Unwrap 方法、删除冗余日志语句。这样改造后,模板可以稳定进生产。建议你在使用前也设置一个最小校验清单:是否所有错误路径都被覆盖?日志是否只由顶层调用者打印?自定义错误是否支持标准接口?
通过后再合并。
2. 如何设计 Prompt 才能让 Claude Code 生成合规的 Go 错误处理模板?
我试过几次让 Claude Code 生成带错误处理的函数,结果要么模板太简单(只有 if err != nil 的骨架),要么太复杂(混入了单元测试代码)。有没有一套固定的 Prompt 模板,能一次就输出我想要的、可复用的生产级代码?
核心在于将 Prompt 从“指令”升级为“代码规范文件”。我经过 10 轮迭代后,总结出一个三层的 Prompt 结构:第一层定义错误分类,可恢复错误(用 fmt.Errorf 包裹)、不可恢复错误(用 log.Fatal 或 panic 替代)、业务逻辑错误(用自定义类型)。
第二层指定输出格式,函数签名要包含返回 error、错误变量名用 err、日志仅打印关键字段。第三层给出负面清单,禁止重复记录日志、禁止忽略中间步骤的错误、禁止使用未定义类型的错误码。
举个例子,我生成的数据库查询模板中,Claude Code 曾自动添加了重试逻辑(循环 3 次),但未验证重试间的回退策略,导致生产压力加倍。通过在 Prompt 里添加“重试逻辑必须附带指数退避并验证副作用”后,输出立即合规。
建议你把 Prompt 视为与 AI 签订的代码契约,越精确越能减少返工。
3. Claude Code 生成的错误类型和 Go 标准库的 errors.Is/As 能正常配合吗?
我担心 Claude Code 生成的错误处理模板只是表面好看,真正写业务逻辑时,errors.Is 判断会失效,或者 As 转换不到对应的类型。不知道它有没有理解 Go 的错误链机制?
经过实际测试,Claude Code 对 errors.Is 和 errors.As 的支持度取决于 Prompt 里是否显式要求。
在我早期给出的简单 Prompt(“生成一个带错误处理的函数”)下,它生成的错误类型往往是空结构体,没有实现 Error() 和 Unwrap() 方法,导致 errors.As 匹配失败。
后来我在 Prompt 里明确要求“自定义错误类型必须嵌入标准 errors 包并暴露 Unwrap 方法”,它开始产出类似 type MyError struct { Err error;
Code int } 并附带 func (e *MyError) Unwrap() error { return e.Err } 的代码。
另一个关键发现是:Claude Code 生成的 fmt.Errorf("%w", err) 是合格的,但如果你在多个函数间多次包裹同一错误,它有时会忘记使用 %w 而使用 %v,导致链断裂。
我的应对是:在 Prompt 最后加一条“所有错误包裹必须使用 %w 动词”,并在生成后手动 grep 检查 %w 出现次数。现在我的项目团队已经将这些模板用作基础脚手架,配合 CI 中的 errorlint 工具,能确保 errors.Is 在全链路生效。
4. Claude Code 生成错误处理模板时,能否兼顾日志和指标埋点?
我希望 AI 生成的函数模板不仅包含错误处理,还能自动加上结构化日志(比如 zerolog 或 zap)和 Prometheus 错误计数。但每次生成的代码要么日志格式不统一,要么埋点逻辑嵌套在业务逻辑里,很难复用。有什么办法让 AI 输出一个关联了监控的完整模板?
这需要你把日志和指标的约定也写入 Prompt。
我的实践是:在 Prompt 里定义一个“错误处理函数模板”,它接受一个错误、一个 context 和一个 log 实例,内部依次执行:判断错误是否可恢复、打印结构化日志(包含请求ID、错误类型、堆栈前三层)、增加 metrics counter 并返回自定义错误。
Claude Code 在理解这个多层结构后,能稳定生成如下伪代码:func HandleError(ctx context.Context, err error, log *zerolog.Logger) *MyBizError { if err == nil { return nil };
log.Error().Err(err).Str("trace", getStack(3)).Int("code", 500).Msg("");metrics.ErrCounter.WithLabelValues(分类(err)).Inc();
return &MyBizError{Err: err, Code: 500} }。关键细节:你需要指定日志库的名称(比如 zerolog)和 metrics 接口的变量名,否则它可能自己定义一个空日志对象。
另外,我发现 Claude Code 在第一次生成时通常会把日志和指标写在同一行,难以分拆,所以我加了“日志和指标调用必须分行”的约束。经过这些调整后,生成的模板已经直接集成到我的中间件代码中,每周节省约 2 小时重复编码时间。
核心关键词
文章版权归“万象方舟”www.vientianeark.cn所有。发布者:程, 沐沐,转载请注明出处:https://www.vientianeark.cn/p/599041/
温馨提示:文章由AI大模型生成,如有侵权,联系 mumuerchuan@gmail.com 删除。
读者评论
看了开头那个遗留项目里的空指针报错,感同身受。我们团队也有个类似的服务,错误处理就是 return err 加个 log,现在维护起来简直灾难。文章里把 AI 生成代码的定位说得很清楚,半成品,需要人做质量控制。这个观点比那些“一键生成生产代码”的吹水文章实在太多了。%w 和 %v 的误用那一段,我们 Code Review 时真是天天见,AI 写出来的也不例外。
把 Claude Code 当成初级工程师,这个比喻太贴切了。我们正好也在推行 AI 辅助编程,之前最大的困惑就是怎么让生成的代码符合团队规范。文章里“给它代码规范而不是需求描述”这个思路,一下子把问题说透了。不过有个细节想问问:这个规范文件你们是怎么管理和版本控制的?是放在项目根目录还是在 Claude Code 的配置里?
文章里那张正确率随复杂度衰减的雷达图很直观。我觉得最致命的是“日志与错误解耦逻辑”只有 52% 的正确率,这恰恰是生产环境最要命的地方。很多教程只教写 if err != nil,根本不提分层处理、不重复记录日志这些反模式。建议能再补充一些关于如何在 Prompt 里约束 AI 不犯这些错的具体例子,比如怎么用规范文件禁止日志和返回错误同时出现。
我实践下来发现,Claude Code 对于生成基础的 HTTP handler 错误处理模板确实很稳,但一到需要根据业务状态码做判断的地方就容易乱。这篇文章把“错误传播契约”这个概念讲清楚了,尤其是版本 B 用 %v 导致 errors.Is 失效的点,我踩过一模一样的坑。现在我的做法是先把团队的错误类型定义喂给它,然后再让它生成函数,效果好了很多。