标签 Golang 下的文章

RedMonk最新排行出炉:Go语言稳居Top 12,AI 冲击下 Stack Overflow 权重生变?

本文永久链接 – https://tonybai.com/2025/06/20/redmonk-index-2025-jan

大家好,我是Tony Bai。

编程语言的江湖,总是风起云涌,新旧更迭。而 RedMonk 编程语言排行榜,以其独特的视角(结合 GitHub 的代码活跃度和 Stack Overflow 的讨论热度),长期以来都是我们观察这片江湖风向的重要参考。

就在最近,RedMonk发布了其2025年1月的编程语言排行榜。榜单本身波澜不惊,Top 20 的名单几乎与上一期如出一辙,这似乎预示着编程语言领域正进入一个相对“固化”的时期。然而,在这份看似平静的榜单背后,却潜藏着一个巨大的变量,一个足以让 RedMonk 自身都开始反思其排行方法论的“房间里的大象”——那就是 AI 的崛起,及其对 Stack Overflow 数据源的颠覆性冲击。

今天,我们就来解读这份最新的 RedMonk 排行榜,看看 Go 语言在其中表现如何,更重要的是,探讨在 AI 时代,我们该如何看待这类排行榜,以及 Go 语言的未来又将走向何方。

RedMonk 排行榜:方法论回顾与本次看点

在解读具体排名之前,我们有必要简单回顾一下 RedMonk 排行榜的方法论。它并非统计当前“谁用得多”,而是试图通过两个维度的数据来预测语言的未来采用趋势

  1. GitHub 数据: 主要通过 GitHub Archive 拉取数据,分析代码提交中使用的语言,代表了语言在实际项目开发中的活跃度和受开发者青睐的程度。
  2. Stack Overflow 数据: 通过其 Data Explorer 查询,分析特定语言标签下的问题和讨论数量,代表了语言在开发者社区中的关注度和开发者在学习、使用过程中遇到的问题量(间接反映了活跃度)。

RedMonk 强调,榜单的“分层 (Tiering)”比具体的数字名次更重要,因为精确排名本身就存在误差。同时,对于排名靠后的语言,由于数据量较小,其排名的波动性和不确定性会更大。

本次 2025 年 1 月的排行,最大的看点莫过于 RedMonk 博客作者 Stephen O’Grady 对 Stack Overflow (以下有时简称SO)数据有效性的公开疑虑。他明确指出,随着 ChatGPT、GitHub Copilot 等 AI 工具的普及,开发者遇到问题时,直接向 AI 提问的比例越来越高,而去 Stack Overflow 搜索或提问的需求显著下降。这导致 Stack Overflow 整体流量和特定语言标签下的讨论量都在萎缩,从而可能扭曲了基于 StackOverflow 数据的排名。RedMonk 甚至在考虑未来是否要调整 SO 数据的权重,甚至完全放弃使用它。

这无疑为我们解读本次榜单,尤其是观察那些 SO 数据占比较重的语言,提供了一个全新的、也是更具挑战性的视角。

Go语言:稳坐 Top 12,GitHub 根基深厚

在这样的背景下,我们来看看Go语言的表现:

  • 排名: Go 语言在此次排行中位列 第 12 位,与统计语言 R 并列。
  • 稳定性: Top 20 的榜单几乎“纹丝不动”,Go 的排名也保持了稳定。回顾历史,Go 从 2015 年的第 17 位,稳步上升,并在近几年持续超越了曾经在 JVM 生态中势头强劲的 Scala 和 Kotlin。
  • 解读 Go 的“稳”: 在 Stack Overflow 数据可能“失真”、整体排行趋于“凝固”的大环境下,Go 语言能够牢牢占据 Top 12 的位置,这本身就充分说明了其在 GitHub 上的代码活跃度和开发者基础的极端稳固。这与 Go 在云原生、后端服务、基础设施等领域的深厚积累和广泛应用密不可分。

关键语言动态:Go 在比较中更显价值

RedMonk 的博文还特别点出了一些值得关注的语言动态,通过与这些语言的对比,我们可以更清晰地看到 Go 的独特价值和发展趋势。

  • TypeScript (第 6) 的“平台期”与 Go 的“幕后英雄”角色

尽管 TypeScript 在 JavaScript 生态中不可或缺,其排名也高居第 6,但博文指出它似乎进入了一个“增长平台期”,难以再向上突破。

RedMonk 提到了 TypeScript 在可扩展性 (scalability) 方面可能遇到的挑战,并直接点名了微软决定使用 Go 语言重写 TypeScript 的编译器 (tsc) 和相关工具链这一标志性事件。

当然,这无疑是对 Go 语言在构建大规模、高性能开发工具和基础设施方面能力的最好背书。当连 TypeScript 这样的语言工具自身都遇到扩展性瓶颈时,他们选择了 Go 作为解决方案。这充分证明了 Go 在工程效率、编译速度、并发处理和静态二进制部署等方面的核心优势,使其成为构建下一代开发工具(编译器、Linter、语言服务器等)的优选语言。Go,正在成为越来越多关键技术的“幕后英雄”。

  • Kotlin (并列 14) / Scala (并列 14) 的“增长天花板”

这两位 JVM 生态的“优等生”排名稳定,但向上突破的动力似乎不足。Go 早已在排名上超越它们。

随着 Go 在微软等传统“非 Go”大厂中找到新的应用场景(如上述 TypeScript 工具链),以及 Rust 在对安全和性能有极致要求的服务端负载中逐渐蚕食地盘,Kotlin 和 Scala 的增长路径面临着不小的挑战。

Go 凭借其简洁的语法、高效的并发模型、出色的网络性能、以及与云原生生态的无缝集成,在现代后端服务开发领域,对传统的 JVM 语言形成了持续且强劲的竞争压力。对于追求快速迭代、高并发、低资源占用的新项目,Go 往往是更具吸引力的选择。

  • 新兴语言 (Ballerina, Bicep, Zig 等) 的“SO 困境”

许多被 RedMonk 关注的新兴语言,在本次排名中大多出现了下滑,并且呈现出 GitHub 排名远好于 Stack Overflow 排名的特点。

这很可能就是前文提到的 AI 对 Stack Overflow 数据冲击的直接体现。新兴语言本身在 SO 上的讨论基数就小,当整体 SO 流量下降时,它们受到的负面影响会更加不成比例。

这再次提醒我们,在评估语言趋势时,需要警惕单一数据源(尤其是易受外部因素干扰的数据源)的局限性。Go 之所以能在榜单中保持稳定,更多是依赖其在 GitHub 上庞大且活跃的真实代码贡献和项目应用,这比社区讨论热度更能反映语言的实际生命力。

AI 时代,编程语言排行榜的挑战与 Go 的新机遇

AI 代码助手(如 ChatGPT, GitHub Copilot)的普及,正在深刻改变开发者的工作习惯。遇到问题,许多人可能首先想到的是“问 AI”,而不是去 Stack Overflow 搜索或提问。这对依赖 SO 数据的 RedMonk 排行榜方法论构成了前所未有的挑战。Stephen O’Grady 的坦诚,也预示着未来编程语言趋势的观察方法可能需要革新。

在这样的背景下,Go 语言的机遇何在?

  1. GitHub 数据权重可能提升: 如果 SO 数据权重下降或被弃用,那么更能反映语言实际使用和生态发展的 GitHub 数据将变得更加重要。Go 在这方面一直表现强劲,拥有大量高质量的开源项目和活跃的贡献者。
  2. AI 基础设施的构建者: 正如我在之前的文章中多次提到的,Go 语言凭借其高性能、高并发、易部署的特性,非常适合构建支撑 AI 大模型训练、推理服务的底层基础设施(如分布式计算框架、模型服务平台、向量数据库、数据管道等)。许多流行的 AI 开源项目(如 Ollama)也选择使用 Go。
  3. AI 应用的工程化落地: AI 模型最终需要被集成到实际的应用和服务中才能产生价值。Go 的简洁性、强大的网络库、以及出色的工程化特性(如编译速度、静态部署),使其成为将 AI 模型快速、可靠地工程化、产品化的优秀选择。
  4. “工具的工具”: Go 在构建开发工具方面的优势,在 AI 时代将更加凸显。无论是构建 AI 代码分析工具、模型部署工具,还是 AI 辅助开发平台的后端,Go 都能胜任。
  5. 对 LLM 的“友好性”探索: 虽然目前 Go 在 LLM 训练数据中的占比可能不如 Python,但 Go 语言相对简单的语法、明确的类型系统、以及强大的标准库,是否可能在未来使其更容易被 LLM 理解、分析和生成高质量代码?这是一个值得探索的方向。

小结:喧嚣之中,坚守价值,拥抱未来

RedMonk 的最新编程语言排行榜,在 AI 席卷技术圈的当下,给我们带来了新的思考。Stack Overflow 讨论热度的“失真”,或许只是 AI 改变我们工作和学习方式的一个缩影。

对于 Go 语言而言,其在榜单中的稳定表现,特别是在 GitHub 维度上的持续强势,证明了其深厚的开发者基础和旺盛的生态活力。像微软选择用 Go 重写 TypeScript 工具链这样的行业案例,更是对其核心竞争力的有力印证。

面对 AI 带来的不确定性,Go 语言凭借其在构建高性能网络服务、云原生基础设施、以及高效开发工具等领域的明确价值定位,依然展现出强大的韧性和广阔的前景。未来,它不仅将继续作为这些领域的中流砥柱,更有望在 AI 基础设施和工程化领域扮演越来越重要的角色。

作为 Gopher,我们既要看到排行榜数据的变化,更要理解变化背后的深层逻辑。坚守 Go 语言的核心价值,持续学习和实践,同时对新技术保持开放和探索的心态,这或许才是我们在这个快速变化的时代中,最稳妥的前行之道。

你对这份 RedMonk 榜单有什么看法?AI 的出现改变了你获取技术信息的习惯吗?欢迎在评论区分享你的观点!


精进有道,更上层楼

极客时间《Go语言进阶课》上架刚好一个月,受到了各位读者的热烈欢迎和反馈。在这里感谢大家的支持。目前我们已经完成了课程模块一『语法强化篇』的 13 讲,为你系统突破 Go 语言的语法认知瓶颈,打下坚实基础。

现在,我们已经进入模块二『设计先行篇』,这不仅包括 API 设计,更涵盖了项目布局、包设计、并发设计、接口设计、错误处理设计等构建高质量 Go 代码的关键要素。

这门进阶课程,是我多年 Go 实战经验和深度思考的结晶,旨在帮助你突破瓶颈,从“会用 Go”迈向“精通 Go”,真正驾驭 Go 语言,编写出更优雅、
更高效、更可靠的生产级代码!

扫描下方二维码,立即开启你的 Go 语言进阶之旅!

感谢阅读!


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

Go errors.Join:是“天赐之物”还是“潘多拉魔盒”?——深入错误聚合的适用场景与最佳实践

本文永久链接 – https://tonybai.com/2025/06/20/about-errors-join

大家好,我是Tony Bai。

错误处理,无疑是软件开发中永恒的核心议题之一。Go 语言以其独特的、显式的错误处理机制(即 error 作为普通值返回)而著称,这种设计强调了对错误的关注和及时处理。自 Go 1.13 引入错误包装 (wrapping) 机制以来,Go 的错误处理能力得到了显著增强。而在Go 1.20 版本中,标准库 errors 包更是带来了一个备受关注的新成员:errors.Join() 函数。

这个函数允许我们将多个 error 值合并成一个单一的 error 值,并且合并后的错误依然可以通过 errors.Is 和 errors.As 进行检查。一时间,社区中对其评价不一:有人称之为“天赐之物”,认为它在特定场景下能极大提升代码表达力和用户体验;也有人持审慎态度,强调应坚守“快速失败 (Fail Fast)”的原则,避免滥用错误聚合。

那么,errors.Join() 究竟是解决特定痛点的“良药”,还是可能被误用的“潘多拉魔盒”?它与 Go 一贯倡导的错误处理哲学是相辅相成,还是有所背离?今天,我们就结合社区的讨论,深入探讨 errors.Join() 的适用场景、潜在风险以及最佳实践。

errors.Join():是社区呼声的产物,还是多此一举?

在社区讨论中,有开发者盛赞 errors.Join(),认为它“在需要一次性检查多个不相关错误,或者创建类似伪堆栈跟踪结构以追踪错误传播路径的场景下,是天赐之物,非常棒!”

然而,一些资深 Go 开发者则给出了更审慎的观点:“请不要鼓吹无条件地聚合错误。遵循‘最小惊奇原则’,绝大多数情况下应该在遇到第一个错误时就‘快速失败’。合并错误的场景虽然存在,但合法地罕见。鼓励大家在假设需要合并错误之前,先思考 API 边界及其错误契约。”

这两种截然不同的看法,恰恰反映了 errors.Join() 在实践中可能带来的困惑和需要权衡的场景。

errors.Join() 的“高光时刻”:何时它真的是“天赐之物”?

尽管“快速失败”是处理错误的主流且通常是正确的策略,但在某些特定场景下,聚合多个错误信息并一次性返回,确实能带来显著的收益。社区讨论中,开发者们也分享了他们认为 errors.Join() 非常适用的场景:

输入验证 (Input Validation):一次性告知所有“罪状”

这是被提及最多的场景。当处理用户输入(如表单提交)或 API 请求参数校验时,如果每次只返回第一个发现的校验错误,用户就不得不反复提交、逐个修改,体验极差。此时,将所有校验不通过的字段错误聚合起来,一次性反馈给用户,无疑是更友好的做法。

// https://go.dev/play/p/pK6cVq9exkL
package main

import (
    "errors"
    "fmt"
    "strings"
)

type UserRequest struct {
    Username string
    Email    string
    Password string
}

func validateRequest(req UserRequest) error {
    var errs []error
    if len(req.Username) < 3 {
        errs = append(errs, errors.New("用户名长度不能小于3个字符"))
    }
    if !strings.Contains(req.Email, "@") {
        errs = append(errs, errors.New("邮箱格式不正确"))
    }
    if len(req.Password) < 6 {
        errs = append(errs, errors.New("密码长度不能小于6个字符"))
    }
    // 使用 errors.Join 合并所有验证错误
    // errors.Join 会自动忽略 nil 错误
    return errors.Join(errs...)
}

func main() {
    req1 := UserRequest{"us", "email", "pass"}
    if err := validateRequest(req1); err != nil {
        fmt.Printf("请求1校验失败:\n%v\n", err)
        // 调用方可以通过 errors.Is 或 errors.As 进一步检查具体错误类型
        // 例如,如果错误是自定义类型,可以 errors.As(err, &targetErr)
    }

    req2 := UserRequest{"myuser", "myemail@example.com", "mypassword"}
    if err := validateRequest(req2); err != nil {
        fmt.Printf("请求2校验失败:\n%v\n", err)
    } else {
        fmt.Println("请求2校验通过!")
    }
}

运行该示例的输出如下(对于请求1):

请求1校验失败:
用户名长度不能小于3个字符
邮箱格式不正确
密码长度不能小于6个字符

并行任务的错误聚合:一个都不能少

当启动多个 goroutine 执行并行操作时(例如,并发请求多个下游服务、并行处理一批数据),如果只关心第一个发生的错误,可能会丢失其他并行任务中同样重要的错误信息。此时,等待所有任务完成,收集所有可能发生的错误,并用 errors.Join() 合并,能提供更全面的错误视图。

// https://go.dev/play/p/ZtAm2-Agyo1
package main

import (
    "errors"
    "fmt"
    "sync"
    "time"
)

func processAsyncTask(id int, fail bool) error {
    fmt.Printf("任务 %d 开始...\n", id)
    time.Sleep(time.Duration(id*50) * time.Millisecond) // 模拟不同耗时
    if fail {
        fmt.Printf("任务 %d 失败!\n", id)
        return fmt.Errorf("任务 %d 执行失败", id)
    }
    fmt.Printf("任务 %d 完成。\n", id)
    return nil
}

func main() {
    tasks := []bool{false, true, false, true, false} // 任务是否失败的标志
    var wg sync.WaitGroup
    errs := make([]error, len(tasks)) // 用于收集每个任务的错误

    for i, failFlag := range tasks {
        wg.Add(1)
        go func(idx int, fail bool) {
            defer wg.Done()
            errs[idx] = processAsyncTask(idx+1, fail)
        }(i, failFlag)
    }

    wg.Wait()

    // 使用 errors.Join 合并所有任务的错误
    // errors.Join 会自动过滤掉结果为 nil 的 errs[idx]
    combinedErr := errors.Join(errs...)

    if combinedErr != nil {
        fmt.Printf("\n并行任务执行完毕,发生以下错误:\n%v\n", combinedErr)
    } else {
        fmt.Println("\n所有并行任务执行成功!")
    }
}

运行上述代码示例,我们将得到:

任务 5 开始...
任务 4 开始...
任务 1 开始...
任务 2 开始...
任务 3 开始...
任务 1 完成。
任务 2 失败!
任务 3 完成。
任务 4 失败!
任务 5 完成。

并行任务执行完毕,发生以下错误:
任务 2 执行失败
任务 4 执行失败

defer 中的错误处理:确保信息不丢失

在函数中,defer 语句常用于执行清理操作,如关闭文件、释放锁等。这些清理操作本身也可能返回错误。如果函数主体也返回了错误,我们就面临如何处理这两个(或多个)错误的问题。简单地忽略 defer 中的错误或用它覆盖主体错误都可能导致重要信息的丢失。errors.Join() 提供了一种优雅的方式来合并它们。

//https://go.dev/play/p/ccKUkWXMbuN
package main

import (
    "errors"
    "fmt"
    "os"
)

func writeFileAndClose(filename string, data []byte) (err error) {
    f, err := os.Create(filename)
    if err != nil {
        return fmt.Errorf("创建文件失败: %w", err)
    }
    defer func() {
        // 在 defer 中调用 Close,并将其错误与函数可能已有的错误合并
        closeErr := f.Close()
        if closeErr != nil {
            fmt.Printf("关闭文件 %s 时发生错误: %v\n", filename, closeErr)
        }
        // 使用 errors.Join 合并主体错误和 defer 中的错误
        // 如果 err 为 nil,Join 的行为是返回 closeErr
        // 如果 closeErr 为 nil,Join 的行为是返回 err
        // 如果两者都非 nil,则合并
        err = errors.Join(err, closeErr)
    }()

    _, err = f.Write(data)
    if err != nil {
        // 为了能被 defer 中的 Join 合并,需要将错误赋值给命名返回值 err
        err = fmt.Errorf("写入文件失败: %w", err)
        return // defer 会在这里执行
    }

    // 模拟写入成功,但关闭失败的场景
    // 或者写入失败,关闭也失败的场景

    return nil // 如果写入成功,defer 仍会执行关闭并可能 Join 错误
}

func main() {
    // 场景1: 写入成功,关闭成功 (假设)
    // (为了演示,我们不实际创建文件,避免权限问题)
    fmt.Println("测试场景:写入和关闭都成功 (理想情况)")
    // err := writeFileAndClose("good.txt", []byte("hello"))
    // fmt.Printf("结果: %v\n\n", err) // 应为 nil

    // 场景2: 模拟写入失败 (err 非 nil),关闭也可能失败 (closeErr 非 nil)
    // 为了触发写入失败,我们可以尝试写入一个只读文件或无效路径
    // 为了触发关闭失败,这比较难模拟,但 errors.Join 能处理这种情况
    // 这里我们直接在函数逻辑中模拟这种情况
    badWriteFunc := func() (err error) { // 使用命名返回值
        fmt.Println("测试场景:写入失败,关闭也失败")
        // 模拟写入失败
        mainWriteErr := errors.New("模拟写入操作失败")
        err = mainWriteErr // 赋值给命名返回值

        defer func() {
            simulatedCloseErr := errors.New("模拟关闭操作也失败")
            fmt.Printf("关闭时发生错误: %v\n", simulatedCloseErr)
            err = errors.Join(err, simulatedCloseErr) // 合并
        }()
        return // 返回 mainWriteErr,然后 defer 执行
    }
    errCombined := badWriteFunc()
    if errCombined != nil {
        fmt.Printf("组合错误:\n%v\n", errCombined)
        // 我们可以检查这两个错误是否都存在
        if errors.Is(errCombined, errors.New("模拟写入操作失败")) {
            fmt.Println("包含:模拟写入操作失败")
        }
        if errors.Is(errCombined, errors.New("模拟关闭操作也失败")) {
            fmt.Println("包含:模拟关闭操作也失败")
        }
    }
}

运行该示例:

测试场景:写入和关闭都成功 (理想情况)
测试场景:写入失败,关闭也失败
关闭时发生错误: 模拟关闭操作也失败
组合错误:
模拟写入操作失败
模拟关闭操作也失败

“快速失败 (Fail Fast)”的黄金法则:为何它依然重要?

尽管 errors.Join() 在上述场景中表现出色,但我们不能忘记 Go 错误处理的一个核心原则——快速失败。 一些资深开发者在社区讨论中反复强调了这一点。

“快速失败”意味着:

  • 一旦发生错误,应尽快中止当前操作。
  • 将错误向上传播给调用者,由调用者决定如何处理。
  • 避免在错误状态下继续执行,这可能导致更严重的问题或产生难以追踪的“幽灵Bug”。

在绝大多数情况下,“快速失败”是更简单、更可预测、更易于调试的错误处理策略。它符合“最小惊奇原则”,让代码的行为更符合直觉。

API 边界与错误契约:思考在“Join”之前

有开发者还提出的另一个关键点是:“在假设你需要合并错误之前,先思考你的 API 边界及其错误契约。”

一个设计良好的 API 应该清晰地告知调用者:

  • 它可能返回哪些类型的错误?
  • 在什么情况下会返回错误?
  • 调用者应该如何响应这些错误?

如果一个 API 的职责是单一且明确的,那么通常情况下,它在遇到第一个无法自行处理的错误时就应该返回,而不是试图收集所有可能的内部错误再“打包”抛给调用者。过度使用 errors.Join() 向上层传递大量不相关的细粒度错误,可能会让调用者无所适从,造成信息噪音,反而违背了 Go 错误处理的明确性原则。

何时应该对 errors.Join() 说“不”?

结合上述讨论,以下是一些不建议或需要谨慎使用 errors.Join() 的场景:

  1. 错误之间存在明确的因果或依赖关系:此时应优先处理或报告最根本的错误。
  2. 简单的“快速失败”就能满足需求:不要为了“聚合”而聚合,增加不必要的复杂性。
  3. API 边界清晰,且期望调用者处理单一主要错误:向调用者返回一堆它不关心或无法有效处理的内部错误,通常不是好的 API 设计。
  4. 可能导致信息过载或掩盖核心问题:合并后的错误信息如果过于冗长或杂乱,反而不利于快速定位问题。

errors.Join() vs fmt.Errorf 包装多个错误:Go 1.20 的双重献礼

值得注意的是,在 Go 1.20 版本中,除了引入 errors.Join() 函数外,fmt.Errorf 的 %w 动词也得到了增强,现在它支持同时包装多个错误。这为我们组合错误信息提供了另一种选择。那么,这两者在使用和行为上有什么区别呢?

过滤 nil 错误的能力

  • errors.Join(errs…) 会自动忽略 errs 切片中的 nil 错误。如果所有传入的错误都是 nil,则 errors.Join 返回 nil。
  • fmt.Errorf 使用 %w 时,如果被包装的 err 是 nil,它仍然会生成一个非 nil 的错误(包含 nil 的字符串表示),除非所有 %w 对应的错误都是 nil 且格式化字符串本身在没有这些错误时会产生空错误。

我们来看一个例子:

// https://go.dev/play/p/X6aAjE0LdsY
package main

import (
    "errors"
    "fmt"
)

func main() {
    var err1 = errors.New("错误1")
    var err2 error // nil error
    var err3 = errors.New("错误3")

    // 使用 errors.Join
    joinedErr := errors.Join(err1, err2, err3)
    fmt.Printf("errors.Join 结果:\n%v\n\n", joinedErr)
    // 输出会包含 err1 和 err3,err2 (nil) 会被忽略

    // 使用 fmt.Errorf 包装多个错误
    // 注意:如果 err2 是 nil,"%w" 会输出 "<nil>"
    wrappedErr := fmt.Errorf("组合错误: 第一个: %w, 第二个(nil): %w, 第三个: %w", err1, err2, err3)
    fmt.Printf("fmt.Errorf 结果:\n%v\n\n", wrappedErr)

    // 演示 errors.Is 对两者的行为
    fmt.Printf("errors.Is(joinedErr, err1): %t\n", errors.Is(joinedErr, err1)) // true
    fmt.Printf("errors.Is(joinedErr, err2): %t\n", errors.Is(joinedErr, err2)) // false (因为 err2 是 nil 且被忽略)
    fmt.Printf("errors.Is(joinedErr, err3): %t\n", errors.Is(joinedErr, err3)) // true

    fmt.Printf("errors.Is(wrappedErr, err1): %t\n", errors.Is(wrappedErr, err1)) // true
    // 对于 fmt.Errorf,如果被包装的 err 是 nil,errors.Is 无法通过 %w 找到它
    fmt.Printf("errors.Is(wrappedErr, err2): %t\n", errors.Is(wrappedErr, err2)) // false
    fmt.Printf("errors.Is(wrappedErr, err3): %t\n", errors.Is(wrappedErr, err3)) // true

    // 如果所有错误都是 nil
    var nilErr1, nilErr2 error
    joinedNil := errors.Join(nilErr1, nilErr2)
    fmt.Printf("errors.Join(nil, nil) is nil: %t\n", joinedNil == nil) // true

    // fmt.Errorf 在所有 %w 都为 nil 时,如果格式化字符串本身为空,则可能返回 nil
    // 但通常会包含格式化字符串本身,所以不为 nil
    wrappedAllNil := fmt.Errorf("错误: %w, %w", nilErr1, nilErr2)
    fmt.Printf("fmt.Errorf(\"错误: %%w, %%w\", nil, nil) is nil: %t\n", wrappedAllNil == nil) // false
}

运行示例输出如下结果:

errors.Join 结果:
错误1
错误3

fmt.Errorf 结果:
组合错误: 第一个: 错误1, 第二个(nil): %!w(<nil>), 第三个: 错误3

errors.Is(joinedErr, err1): true
errors.Is(joinedErr, err2): false
errors.Is(joinedErr, err3): true
errors.Is(wrappedErr, err1): true
errors.Is(wrappedErr, err2): false
errors.Is(wrappedErr, err3): true
errors.Join(nil, nil) is nil: true
fmt.Errorf("错误: %w, %w", nil, nil) is nil: false

解包 (Unwrapping) 多个错误的能力

  • errors.Join 返回的错误类型(如果是非 nil 的)必然实现了 interface{ Unwrap() []error } 接口。这允许调用者获取一个包含所有被合并的非 nil 原始错误的切片,从而可以对每一个原始错误进行独立的检查。
  • fmt.Errorf 通过多个 %w 包装错误时,它仍然是构建一个错误链 (error chain)。这意味着错误是一层一层包装的,解包时需要多次调用 errors.Unwrap 来逐个访问。它不直接提供一次性获取所有被包装错误的方法。
// https://go.dev/play/p/8Zb2mvSFlFw
package main

import (
    "errors"
    "fmt"
)

type specialError struct {
    msg string
}

func (e *specialError) Error() string {
    return e.msg
}

func main() {
    errA := errors.New("错误A")
    errB := &specialError{"特殊错误B"}
    errC := errors.New("错误C")

    // 使用 errors.Join
    joined := errors.Join(errA, errB, errC)

    fmt.Println("使用 errors.Join 解包:")
    if unwrap, ok := joined.(interface{ Unwrap() []error }); ok {
        originalErrors := unwrap.Unwrap()
        for i, e := range originalErrors {
            fmt.Printf("  原始错误 %d: %v (类型: %T)\n", i+1, e, e)
            // 可以用 errors.As 检查特定类型
            var se *specialError
            if errors.As(e, &se) {
                fmt.Printf("    检测到 specialError: %s\n", se.msg)
            }
        }
    }
    fmt.Println()

    // 使用 fmt.Errorf 包装多个错误
    wrapped := fmt.Errorf("外层错误: (第一个: %w), (第二个: %w), (第三个: %w)", errA, errB, errC)
    // 实际的错误链结构取决于 %w 的顺序和格式化字符串
    // 例如,这里更像是 errA 被 wrapped 包裹,errB 被包裹 errA 的错误包裹,以此类推(具体取决于实现)
    // 或者,它们可能被视为并列地被一个包含描述文字的错误所包裹。
    // 为了清晰,我们假设一种简单的线性包裹(虽然内部实现可能更复杂,但 errors.Unwrap 行为类似)

    fmt.Println("使用 fmt.Errorf 解包 (逐层):")
    currentErr := wrapped
    i := 1
    for currentErr != nil {
        fmt.Printf("  解包层级 %d: %v (类型: %T)\n", i, currentErr, currentErr)
        var se *specialError
        if errors.As(currentErr, &se) { // 检查当前错误或其链中的错误
            fmt.Printf("    在链中检测到 specialError: %s\n", se.msg)
        }
        // errors.Is 也可以用于检查链中的特定错误实例
        if errors.Is(currentErr, errA) {
            fmt.Println("    在链中检测到 错误A")
        }

        unwrapped := errors.Unwrap(currentErr)
        if unwrapped == currentErr || i > 5 { // 防止无限循环或过多层级
            break
        }
        currentErr = unwrapped
        i++
    }
}

运行该示例,我们将得到预期的输出:

使用 errors.Join 解包:
  原始错误 1: 错误A (类型: *errors.errorString)
  原始错误 2: 特殊错误B (类型: *main.specialError)
    检测到 specialError: 特殊错误B
  原始错误 3: 错误C (类型: *errors.errorString)

使用 fmt.Errorf 解包 (逐层):
  解包层级 1: 外层错误: (第一个: 错误A), (第二个: 特殊错误B), (第三个: 错误C) (类型: *fmt.wrapErrors)
    在链中检测到 specialError: 特殊错误B
    在链中检测到 错误A

结合上述两个示例,我们可以看到:

  • 如果你需要将多个独立的错误视为一个集合,并希望轻松地忽略其中的 nil 值,同时方便地一次性访问所有非 nil 的原始错误,那么 errors.Join() 是更直接和语义化的选择。
  • 如果你更倾向于传统的错误链结构,通过错误包装来添加上下文信息,并且可以接受逐层解包,或者你的主要目的是在错误信息中包含多个原始错误的文本表示,那么 fmt.Errorf 配合多个 %w 也是可行的。

Go 1.20 同时提供这两种能力,让开发者在处理多个错误时有了更灵活的选择。理解它们的细微差别,有助于我们根据具体场景做出最合适的决策。

小结

Go 1.20 引入的 errors.Join() 无疑为 Go 语言的错误处理工具箱增添了一件强大的新工具。它在特定场景下——如输入验证、并行任务错误收集、defer 中的多错误处理——能够显著提升代码的表达力和用户体验,使得我们能够向调用者或用户提供更全面、更友好的错误信息。

然而,正如社区的讨论所揭示的,它并非“银弹”,更不应被滥用以取代“快速失败”这一久经考验的错误处理黄金法则。理解 errors.Join() 的适用边界,审慎评估其在具体场景下的收益与成本(如可能带来的信息过载或对 API 错误契约的破坏),是每一位 Gopher 都需要具备的判断力。

最终,优雅的错误处理,在于清晰、明确、以及在“最小惊奇”与“详尽信息”之间找到那个恰到好处的平衡点。errors.Join() 为我们实现这种平衡提供了一种新的可能性。

社区讨论帖:https://www.reddit.com/r/golang/comments/1ldyywj/use_errorsjoin/


聊一聊,也帮个忙:

  • 在你的 Go 项目中,你遇到过哪些适合使用 errors.Join() 的场景?或者,你认为哪些场景下应该坚决避免使用它?
  • 除了文中提到的,你对 Go 语言的错误处理机制还有哪些独到的见解或最佳实践?
  • 你认为“快速失败”和“错误聚合”这两种策略,在设计 API 时应该如何权衡?

欢迎在评论区留下你的经验、思考和问题。如果你觉得这篇文章对你有帮助,也请转发给你身边的 Gopher 朋友们,让更多人参与到关于 Go 错误处理的深度讨论中来!


精进有道,更上层楼

极客时间《Go语言进阶课》上架刚好一个月,受到了各位读者的热烈欢迎和反馈。在这里感谢大家的支持。目前我们已经完成了课程模块一『语法强化篇』的 13 讲,为你系统突破 Go 语言的语法认知瓶颈,打下坚实基础。

现在,我们已经进入模块二『设计先行篇』,这不仅包括 API 设计,更涵盖了项目布局、包设计、并发设计、接口设计、错误处理设计等构建高质量 Go 代码的关键要素。

这门进阶课程,是我多年 Go 实战经验和深度思考的结晶,旨在帮助你突破瓶颈,从“会用 Go”迈向“精通 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