标签 泛型 下的文章

泛型重塑 Go 错误检查:errors.As 的下一站 AsA?

本文永久链接 – https://tonybai.com/2025/08/23/proposal-errors-asa

大家好,我是Tony Bai。

Go 1.13 引入 errors.Is 和 errors.As 以来,Go 语言的错误处理进入了一个结构化、可追溯的新时代。然而,errors.As 的使用方式,对于追求代码简洁与优雅的 Gopher 而言,始终存在一丝“不和谐”:开发者必须预先声明一个目标错误类型的变量,然后将其指针传入函数。

随着 Go 1.18 泛型的正式落地,一个酝酿已久的问题浮出水面:我们能否利用类型参数,彻底重塑这一核心错误检查机制,终结那些恼人的样板代码?GitHub 上的 Issue #51945 正是这场变革的中心舞台。它不仅是一个新函数AsA的提案,更深刻地揭示了 Go 社区是如何在 API 设计、性能、向后兼容性与语言哲学之间反复权衡,以决定 errors.As 的未来。那么,AsA 会是 errors.As 的下一站吗?在这篇文章中,我就和大家一起来看一下Go社区和Go团队针对这一提案的讨论和决策过程。

现状之痛:errors.As 的人体工程学难题

要理解为何需要“重塑”,我们必须先审视 errors.As 带来的便利与痛点,我们先来看一下现状:

// Go 1.13 至今的标准模式
err := someOperation()
if err != nil {
    var myErr *MyCustomError
    if errors.As(err, &myErr) {
        // myErr 在这里可用,但它的声明却在 if 语句之外
        // ...处理 myErr...
    }

    var otherErr *OtherError
    if errors.As(err, &otherErr) {
        // ...处理 otherErr...
    }
    // ...
}

这种模式存在几个显而易见的痛点:

  1. 样板代码: var myErr *MyCustomError 这一行是纯粹的样板代码。
  2. 变量作用域泄露: myErr 的作用域超出了它真正被需要的 if 块,这在 Go 中通常被认为是不够优雅的设计。
  3. C 语言风格的“输出参数”: 通过指针参数来“返回”一个值,是 C 语言的常见模式,但在 Go 中,我们更习惯于通过多返回值来处理。

正是这些“不和谐”之处,催生了用泛型来重塑 errors.As 的强烈动机。

泛型之力:三大核心优势重塑错误检查

提案的核心,是引入一个利用类型参数的新函数,社区讨论最终倾向于命名为 AsA。这个新函数将彻底改变错误检查的写法,使其更符合 Go 开发者熟悉的“逗号, ok”模式:

// 提案中的理想模式
err := someOperation()
if err != nil {
    if myErr, ok := errors.AsA[*MyCustomError](err); ok {
        // myErr 的作用域被完美限制在此 if 块内
        // ...处理 myErr...
    } else if otherErr, ok := errors.AsA[*OtherError](err); ok {
        // ...处理 otherErr...
    }
    // ...
}

这场“重塑”的背后,是泛型带来的三大核心优势:

优势一:人体工程学与代码可读性

这是最直观的优点。新的 if shortVarDecl, ok := … 形式是 Go 语言中最深入人心的模式之一,用于类型断言、map 查询等众多场景。将错误检查统一到这个模式下,降低了开发者的心智负担。

尽管有社区成员指出现有的 errors.As 也可以通过 if pe := new(os.PathError); errors.As(err, &pe) 这种巧妙的写法实现单行和作用域限制,但其他成员普遍认为这种写法“非常微妙”、“难以阅读”,且容易误用。这恰恰反衬出泛型版本在清晰度和直观性上的巨大优势。

优势二:编译时类型安全

这是泛型版本一个被低估但至关重要的优势。errors.As 的第二个参数类型是 any(interface{}),这意味着编译器无法在编译时对其进行严格的类型检查。任何不满足“指向 error 实现类型的非空指针”这一约束的用法,都只能在运行时 panic 或被 go vet 捕获。

而泛型版本则将这个检查提前到了编译时。类型参数 T 被约束为 error,任何不满足此约束的类型参数都会导致编译失败。这无疑是向 Go 的核心价值——静态类型安全——迈出的重要一步。

优势三:显著的性能提升

这可能是最令人意外,也是最有说服力的论据。errors.As 的实现严重依赖反射,以便在运行时处理 any 类型的 target。反射在 Go 中是出了名的慢。

有社区成员提供了他的开源库 errutil 中的纯泛型实现 Find,并给出了详尽的 benchmark 数据。其核心思想是,在泛型函数内部,可以直接使用类型断言 (err.(E)),完全绕开反射。并且,其提供的 benchmark 结果令人震惊:在绝大多数场景下,纯泛型实现的性能比 errors.As 快 50% – 70%。此外,由于避免了为 target 变量在堆上分配内存(new(E)),纯泛型版本在很多情况下可以做到零堆分配

前路挑战:从 switch 困境到 API 哲学的权衡

尽管优势明显,但“重塑”之路并非一帆风顺。Go 核心团队和社区的审慎讨论,揭示了在标准库中引入新 API 的复杂性。

考量一:历史的包袱与设计的初心

一些Go核心团队成员提及,在 errors.As 最初的设计阶段,rsc (Russ Cox) 曾认为,var myErr *MyError 的显式声明,虽然冗长,但明确地向读者展示了代码正在寻找的错误类型,具有清晰性的优点。这体现了 Go 早期设计中对“明确优于隐晦”的极致追求。

考量二:switch 语句的困境

这是泛型版本最主要的“人体工程学”短板。errors.As 可以非常优雅地与 switch 语句结合,形成强大的多错误类型处理模式:

var myErr *MyCustomError
var otherErr *OtherError

switch {
case errors.As(err, &myErr):
    // ...
case errors.As(err, &otherErr):
    // ...
}

然而,返回 (T, bool) 的泛型函数无法直接用在 case 语句中,这破坏了一种现有的、被广泛接受的优雅模式。

考量三:API 的膨胀与命名难题

在标准库中增加一个与现有函数功能高度重叠的新 API,是一项需要慎之又慎的决定。它会带来“API 膨胀”的问题,并引发关于命名的激烈讨论。从最初的 IsA,到社区热议的 AsA、AsOf、Find、Has,每一个名字都有其合理性与不足。

小结:尘埃落定:AsA,迈向未来的下一站?

经过长达数年的讨论、辩论与社区探索,在 neild 的总结陈词下,提案目前已经收敛并被 Go 团队选中,进入了 “Active” 审查阶段。这标志着 Go 官方已经基本认可了引入泛型 errors.As 的价值。

最终的提案形态如下:

package errors

// AsA finds the first error in err's tree that has the type E, and if one is found, returns that error value and true.
// Otherwise it returns the zero value of E and false.
func AsA[E error](err error) (_ E, ok bool)

这个版本的暂时胜出,也是多方权衡的结果:

  • 双返回值形式 (_ E, ok bool) 在人体工程学和性能上全面优于指针参数形式。
  • AsA 的命名最大程度上保留了与 As 的关联性。
  • 尽管存在 switch 语句的短板,但其在 if 语句中的巨大优势、编译时类型安全和显著的性能提升,最终压倒了所有顾虑。

这场关于 errors.As 泛型化的深度辩论,生动地展示了 Go 语言的演进过程:它不是一蹴而就的激进变革,而是在尊重历史、充分听取社区声音、深入权衡利弊后,做出的稳健而有力的前行。而泛型的引入,也正在为 Go 社区提供一个重新审视和打磨既有 API 的宝贵契机。让我们有理由相信 Go 的错误检查也将因此被成功“重塑”,变得更加安全、高效和优雅。

资料链接:https://github.com/golang/go/issues/51945


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

从 Rob Pike 的提案到社区共识:Go 或将通过 new(v) 彻底解决指针初始化难题

本文永久链接 – https://tonybai.com/2025/08/17/create-pointer-to-simple-types

大家好,我是Tony Bai。

在 Go 中创建一个指向基本类型(如 int 或 string)的指针,为何比创建一个指向结构体的指针更繁琐?这个长期存在的“人体工程学”问题,由 Go 语言的共同创造者之一 Rob Pike 在提案 #45624 中再次带入公众视野,并由此引发了一场长达数年、充满深度思辨的社区大讨论。最终,在权衡了多种方案的利弊后,社区逐渐形成共识,Go 提案委员会倾向于接受 new(v) 语法。本文将和大家一起回顾这场关于指针初始化的“十年之辩”,深入探讨各种方案的优劣,并解读为何 new(v) 可能成为最终赢家。

背景:一个困扰开发者多年的“小”问题

在 Go 中,我们可以用 p := &S{a: 3} 这样简洁的语法,一步到位地创建一个指向已初始化结构体的指针。但如果我们想创建一个指向 int 值 3 的指针,就必须写成:

a := 3
p := &a

这种不对称性在处理大量使用指针来表示“可选”字段的场景时(例如,与 JSON、Protobuf 或 AWS SDK 交互),会变得异常繁琐。开发者往往不得不在项目中定义或引入大量的辅助函数,如:

func StringPtr(s string) *string {
    return &s
}
// 还有 Int64Ptr, BoolPtr, Float64Ptr...

正如 @adonovan 在提案讨论中通过代码分析所展示的,这种模式在 Go 开源生态中极为普遍,存在数千个这样的辅助函数和数十万次的调用。这清晰地表明,语言层面提供一个更简洁的解决方案是众望所归。

方案之争:一场关于语法、语义与哲学的辩论

Rob Pike 的提案及其漫长的讨论过程,涌现了多种解决方案,每种方案都代表了一种不同的语言设计哲学。

方案一:扩展 & 操作符

这是最直观的想法,主要有两种变体:

  1. &T(v) (让类型转换变得可寻址): p := &int(3)。这是 Rob Pike 最初提出的方案之一。它利用了“类型转换必然会创建新值”这一语义,逻辑自洽。
  2. &v (让非地址表达式变得可寻址): p := &3 或 p := &time.Now()。这个方案更通用,但也最危险。正如 rsc 和其他核心成员指出的,这会产生严重的歧义。例如,&m[k] 在 m 是 slice 时是取地址,但在 m 是 map 时却变成了“拷贝值并取地址”,这会引入大量难以察觉的 bug。

由于存在严重的“最小惊动原则”问题,扩展 & 的方案最终未被采纳。

方案二:引入新的泛型内建函数

随着 Go 1.18 泛型的引入,一个显而易见的解决方案是提供一个泛型辅助函数。

// 可以是内置的,也可以是开发者自己写的
func ptr[T any](v T) *T {
    return &v
}

// 使用方式:
p := ptr(3)
p2 := ptr(time.Now())

这个方案得到了许多开发者的支持,因为它无需对语言规范做任何大的改动。然而,它的缺点也很明显:

  • 命名之争:应该叫 ptr, ref, addr, newOf 还是 varOf?每种名称都有其支持者和反对者。例如,ptr 和 ref 可能会让人误以为是取现有变量的引用,而不是创建一个新的拷贝。
  • 标准库位置:这样一个基础的函数应该放在哪里?builtin?还是一个新的标准库包?这本身就是一个难题。

方案三:扩展 new 内建函数 (最可能的胜出者)

这是提案的核心,也是最终获得Go提案委员会青睐的方向。它同样有几种变体:

  1. new(T, v):new 接受一个可选的第二个参数用于初始化。例如 p := new(int, 3)。这非常明确,但缺点是类型 T 往往是冗余的,显得很“啰嗦”,例如 new(time.Duration, time.Second)。
  2. new(v):new 可以直接接受一个值,并根据值的类型推断出要分配的指针类型。例如 p := new(3) 会创建一个 *int。这是最简洁的方案。

new(v) 的核心争议与共识

new(v) 的主要争议在于语法歧义。当看到 new(pkg.X) 时,读者无法仅从语法上判断 pkg.X 是一个类型(new(T))还是一个常量值(new(v))。

然而,经过深入讨论,提案委员会认为:
* 这种歧义在实践中问题不大,因为绝大多数情况下,上下文足以让开发者区分类型和值。
* 相比于 &v 带来的严重语义混乱,new(v) 的语法歧义是次要的、可接受的。
* new 这个词本身就清晰地传达了“创建新事物”的意图,避免了 & 操作符的“拷贝还是引用”的混淆。
* 考虑到 new(T) 的使用频率远低于 &T{},将其“回收”并赋予更强大的功能,是对语言的一次有益的“清理”。

最终,提案委员会倾向于接受 new(expr) 的形式。

new(expr) 将如何工作

根据讨论的共识,未来的 new(expr) 将遵循以下规则:

  • 基本用法: p := new(3) 将创建一个 *int,其值为 3。s := new(“hello”) 将创建一个 *string,其值为 “hello”。
  • 类型推断: 对于无类型常量,将使用 Go 的默认类型规则(例如,整数默认为 int,浮点数默认为 float64)。
  • 显式类型: 如果需要指定不同于默认的类型,需要使用类型转换:p64 := new(int64(3))来创建一个int64类型变量p64,而不是默认的int指针类型变量。
  • 无上下文类型推断: new(v) 不会根据赋值的上下文来推断类型。例如,var p *int64 = new(3) 将会编译失败,因为 new(3) 的类型是 *int,不能赋值给 *int64。

结论:小改动,大便利

从 Rob Pike 最初的提案,到社区长达数年的激烈辩论,new(v) 的最终可能胜出是 Go 语言演进过程的一个缩影。它通过一个微小但精心设计的语法扩展,解决了困扰社区多年的一个普遍痛点。

这个决策过程本身,也充分体现了 Go 团队的设计哲学:

  1. 优先考虑语言的一致性和无歧义性,因此拒绝了看似更简洁但充满陷阱的 &expr 方案。
  2. 在不破坏兼容性的前提下,勇于重塑旧有特性,将使用率不高的 new 重新利用,赋予其更强大的生命力。
  3. 充分倾听并分析社区的真实数据,@adonovan 的大规模代码分析为该功能的需求提供了强有力的数据支撑。

虽然我们仍需等待该提案在未来某个 Go 版本中正式落地,但可以预见,当它到来时,我们代码库中那些重复的 Ptr 辅助函数将成为历史。这正是 Go 语言持续进化、不断提升开发者幸福感的魅力所在。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言进阶课 Go语言精进之路1 Go语言精进之路2 Go语言第一课 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats