标签 runtime 下的文章

体验Gemini Deep Research:以Go语言未来演进方向分析为例

本文永久链接 – https://tonybai.com/2025/03/16/gemini-deep-research-experience

基于大模型的AI已进入深度思考时代,以DeepSeek R1模型为代表的开源模型给主流AI厂商带来了巨大压力。其实早在2024年12月份,Google就在一篇名为“Try Deep Research and our new experimental model in Gemini, your AI assistant”中发布了自己的Deep Research产品:Gemini Deep Research

Gemini Deep Research不仅仅是一个简单的搜索引擎,而是一个智能研究助理。用户只需输入研究主题,Deep Research即可自动完成以下工作:

  • 自动制定研究计划:根据主题的复杂性,Deep Research会生成一个多步骤的研究计划。
  • 深度网络信息分析:Deep Research会像人类研究员一样,在网络上进行多轮搜索、分析、筛选,并根据已获取的信息不断调整搜索策略。
  • 生成综合报告:最终,Deep Research会生成一份结构化的报告,包含关键发现、主要观点以及原始资料链接。
  • 支持交互式提问:用户可以对报告内容进行追问,Deep Research会进一步解释或补充信息。

不过最初发布时,免费用户体验受到了限制。2025.3.13 Google更新了其AI产品gemini的功能特性,并宣布在Gemini 2.0 Flash Thinking等模型上增加Deep Research功能(并且相对于早期的功能又有了能力上的增强)。现在即便你是免费用户,只要打开Gemini应用的主页面,就能看到下面带有Deep Research功能选项的对话输入框:

并且,在Gemini app页面上免费用户可以使用的模型都支持Deep Research,虽然每月依然有使用次数限制:

作为Gemini AI助手的一项重要特性,基于大窗口增强后的Deep Research利用Gogle强大的信息搜索能力以及AI强大的信息处理能力,可为用户提供深度、全面的研究报告,大幅提高了研究效率。

在信息爆炸的时代,我们这些技术人员面临着持续学习和快速掌握新技术、新趋势的巨大挑战。传统的研究方法往往耗时费力,如何在海量信息中高效提取关键信息,已成为提升技术竞争力的关键要素。

本文将以”Go语言未来5-10年的演进方向及核心团队发力重点”这一主题为例,分享我对增强版Gemini Deep Research的抢先体验。

实战体验:Go语言未来演进方向研究

为了测试Deep Research的实际效果,我选择了一个对Go开发者非常关心的话题:

“Go语言未来5-10年的演进方向以及Go核心团队的发力重点会在哪里?”

研究过程

启动研究

在Gemini对话框中输入上述主题,并在左上角选择”Deep Research”模型,然后提交。

Gemini会首先会自动生成研究计划,如下图,并等待你的确认:

确认方案,并等待研究完成

你可以修改方案,也可以点击“开始研究”,一旦选择后者,Deep Research就会自动开始进行研究(包括反复的数据搜索、分析结果等)。在研究过程中,Gemini会显示当前的研究进度,例如”正在分析相关信息”、”正在生成报告”等,下面是研究过程的一些截图:






… …

整个过程大约持续了10-15分钟(具体时间取决于主题的复杂性)。

获取研究报告

研究完成后,Gemini生成了一份详细的报告,结构完整,内容丰富。Gemini支持将报告导出到Google Doc,之后你便可以基于Google Doc查看、编辑或下载这份研究报告了。Gemini为我生成的这份报告放在了这里。如果你访问不了,我在本文附录也放了一份报告结果,请参考。

下面我们再简单看一下报告质量。

研究报告内容与质量分析

这次Gemini针对我提出的题目生成的报告包含以下主要章节:

  • Go语言的持久相关性与未来轨迹
  • 近期重要进展分析(Go 1.24及未来)
  • 核心团队的优先事项解读:解读发力重点
  • 未来5-10年Go语言的演进方向
  • Go的应用:应对现代挑战
  • Go未来面临的挑战和考虑因素
  • 结论:规划Go未来十年的发展方向
  • 相关统计表格和参考文献

从全面性来看,该报告涵盖了Go语言发展的多个维度,从技术细节(如泛型、性能优化、WebAssembly支持)到宏观趋势(如云计算、边缘计算、AI/ML集成),再到社区和生态系统的发展,内容全面而不失重点。

该报告不仅是信息的简单堆砌,而是对信息进行了深入的分析和整合,不乏一定的深度。例如,报告准确地指出了Go核心团队在性能优化、并发、WebAssembly等方面的持续投入,并分析了这些投入背后的战略意图。

报告还给出了引用的信息的确切来源,包括Go官方博客、技术文章、社区讨论等,初步看了一眼,信息来源相关性强,且地址可靠。比如:报告中提到的Go 1.24的新特性、核心团队的优先事项等,都与官方信息保持一致。

报告也提出了一些有价值的洞察,例如Go在边缘计算和物联网领域的潜力、在AI/ML领域可能的发展方向等,为读者提供了前瞻性的视角。

报告结构非常清晰,语言流畅,易于理解。即使是对Go语言不太熟悉的读者,也能通过报告快速了解Go语言的未来发展趋势。

该报告的撰写质量估计已经超过了许多有多年Go开发经验的资深工程师所能提供的分析。如果一个技术人员亲自去调研和总结这些内容,没有3-5天的时间投入是很难完成的。

体验结论

通过此次体验,我们可以深刻地感受到Gemini Deep Research的强大功能和巨大潜力:

  • 效率提升:Deep Research将原本需要数小时甚至数天的研究工作缩短至几分钟,极大地提高了研究效率。
  • 信息全面性:Deep Research能够从多个来源获取信息,并进行综合分析,避免了人工研究可能存在的遗漏和偏见。
  • 深度洞察:Deep Research不仅是信息的搬运工,它能够对信息进行深入分析,提炼出有价值的洞察。
  • 持续学习:Deep Research处于不断进化中,未来将会变得越来越强大。

Gemini Deep Research等深度研究工具的出现与演进,标志着AI驱动的研究新时代的到来。它将改变我们获取信息、分析信息、利用信息的方式,为各行各业带来巨大的变革。对于技术团队来说,Deep Research无疑是一个强大的工具,可以帮助我们更快地学习、更深入地思考、更高效地工作。

附录

Go语言未来5-10年的演进方向及核心团队发力重点

1. 引言:Go的持久相关性与未来轨迹

自2009年公开宣布,并于2012年发布1.0版本以来,Go语言已在现代软件开发领域占据重要地位,尤其是在云基础设施和可扩展系统方面 1。其设计初衷是为了解决大规模软件开发的复杂性 6,强调简洁、高效和并发性 1。Go语言的用户群体显著增长,表明其采用率和相关性不断提高 10。这种增长凸显了理解其未来演进以及Go核心团队优先事项的必要性。本报告将分析近期发展、社区讨论以及Go项目关键人物的见解,以预测未来5到10年Go语言的发展轨迹,重点关注核心团队的努力方向。

Go语言最初的创建动机是为了解决Google在软件基础设施方面面临的实际问题,例如C++在构建现代服务器软件时遇到的构建缓慢、依赖管理失控和并发编程困难等挑战 1。这种以解决实际问题为导向的设计思路深深植根于Go语言的基因中,可以预见,未来Go核心团队将继续关注实际应用,并致力于满足开发人员的需求。

Go语言用户群体的持续增长以及主要科技公司的广泛采用,为Go语言的未来发展奠定了坚实的基础 10。来自各种调查的数据一致显示,越来越多的开发人员正在使用Go语言,并且有学习Go语言的意愿。诸如Google、Netflix、Uber和Dropbox等公司 3 在其关键基础设施中对Go语言的依赖,突显了Go语言的成熟性和适用于大规模项目的能力,这无疑将确保核心团队和社区对Go语言的持续投入和发展。

2. 近期重要进展分析:Go 1.24及未来

2025年2月发布的Go 1.24版本是一个重要的里程碑,它揭示了Go核心团队当前的优先事项 17。此版本的主要特性包括:

  • 完全支持泛型类型别名,增强了代码的灵活性并减少了冗余 17。这解决了社区长期以来的一个需求 8。
  • 运行时性能得到提升,在一系列代表性基准测试中,CPU开销平均降低了2-3%。这些改进包括基于Swiss Tables的新map实现、更高效的小对象内存分配以及新的内部互斥锁实现 10。
  • 通过go:wasmexport指令将Go函数导出到Wasm,并支持构建为WASI反应器/库,增强了WebAssembly (Wasm) 的功能 17。这标志着Go语言正日益关注将其应用范围扩展到传统的服务器端应用之外 21。
  • go.mod中新增了管理工具依赖的机制 18,并且go vet命令通过新的测试分析器得到了改进 18。这些变化旨在改善开发人员的体验和代码质量。
  • 标准库新增了FIPS 140-3合规性机制、用于目录限制文件系统访问的新os.Root类型以及比runtime.SetFinalizer更灵活的runtime.AddCleanup函数用于清理操作 1。这些新增功能增强了Go在安全性、系统编程和资源管理方面的能力。
  • 用于测试并发代码的实验性testing/synctest包 17。这突显了并发性在Go语言发展中的持续重要性。
  • bytes和strings包中新增了基于迭代器的新函数,提高了常见数据处理任务的效率 18。

Go 1.24中包含的诸如泛型等长期以来备受期待的功能,体现了核心团队对社区反馈的积极响应以及他们为满足现代编程需求而不断发展语言的意愿。Go社区对泛型的需求由来已久 8。Go 1.18开始引入泛型,并在1.24版本中进一步完善了对泛型类型别名的支持,这表明核心团队认真听取了开发者的意见,并准备在社区达成广泛共识且对生态系统有明显益处时,对语言进行重大改变。

Go 1.24中显著的性能改进,进一步巩固了Go语言在效率和速度方面的核心价值主张,预示着性能优化将继续成为核心团队未来的重点工作。关于使用Swiss Tables加速Go map以及其他运行时改进的详细博客文章 10 清晰地表明,核心团队正在持续努力使Go程序在现代硬件上运行得更快、更高效。这与Go最初为基础设施软件设定的设计目标相一致。

Go 1.24中对WebAssembly功能的增强,暗示着Go语言正在战略性地定位自己,使其成为一种能够在包括Web浏览器和基于云的Wasm运行时等多种环境中运行的多功能语言。go:wasmexport指令和WASI反应器支持的引入 17 不仅仅是增量式的变化,它们代表着核心团队有意使Go成为更具吸引力的WebAssembly开发选择。关于可扩展Wasm应用的博客文章 17 详细介绍了这些新增功能,表明核心团队期望Go在浏览器端和服务器端的Wasm应用中都发挥重要作用。

3. 核心团队的优先事项:解读发力重点

基于近期发布的版本、Go团队的博客文章 10 以及社区讨论,可以识别出Go核心团队的几个关键优先事项:

  • 持续强调性能和效率: 每个版本中持续的性能改进 10 表明,保持和提升Go的性能特性仍然是首要任务。这包括针对现代硬件优化运行时、标准库和编译器 10。对诸如新的map实现和内存分配改进等底层优化的关注,表明核心团队致力于从根本上提高Go的性能,从而使广泛的应用受益。关于Swiss Tables的博客文章 17 详细介绍了这些深层次的运行时修改,表明了对核心性能的长期投入。
  • 并发和并行方面的进步: Go在并发方面的优势 1 仍然是关键的关注点,实验性testing/synctest包的引入 17 表明,核心团队正在不断努力改进并发编程的工具和支持。关于未来可能增强并发模型的讨论 25 也表明了其持续的重要性。开发专门用于测试并发代码的工具(如实验性的testing/synctest包 17)突显了核心团队致力于确保并发Go程序的可靠性和正确性,这对于许多目标用例(如云基础设施和分布式系统)至关重要。并发是Go语言的一个核心差异化优势,而对更好的测试框架的投入则体现了对其健壮性的承诺。介绍testing/synctest的博客文章 17 证实了这一重点。
  • 对WebAssembly能力的战略投资: Go 1.24中对Wasm支持的显著增强 17 以及社区持续的兴趣 21 表明,使Go成为一种可行的WebAssembly语言是核心团队的战略重点。这为Go在前端开发和其他基于Wasm的环境中开辟了新的可能性 18。通过go:wasmexport将Go函数导出到Wasm宿主,并构建WASI反应器的双重关注,表明了核心团队对Wasm支持采取了全面的方法,旨在实现与各种Wasm生态系统(包括浏览器和服务器端环境)的互操作性。关于可扩展Wasm应用的博客文章 17 详细介绍了这种双重方法,表明核心团队设想Go在浏览器端和服务器端的Wasm应用中都将发挥重要作用。
  • 加强语言和标准库的安全性: Go 1.24中包含的FIPS 140-3合规性机制 17 以及Go生态系统中关于安全性的持续讨论 8 突显了核心团队致力于使Go成为构建关键应用的安全语言。对内存安全的关注 1 也与这一优先事项相符。通过简单的环境变量 18 提供对FIPS认证加密的内置支持,体现了核心团队对安全性的积极态度,使得开发人员更容易构建符合安全规范的应用,而无需依赖外部库或复杂的配置。此功能直接解决了软件开发中日益增长的安全性重要性,尤其适用于需要遵守FIPS标准的企业和政府应用。
  • 持续优化云原生架构: Go在云原生开发领域的强大影响力 2 是显而易见的,预计核心团队将继续为该领域优化语言和标准库。这包括与微服务、容器化 9 以及与云平台的集成 38 相关的改进。Docker和Kubernetes等主要的云基础设施工具都是用Go语言构建的 9,这使得Go的未来与云原生技术的演进紧密相连。这表明核心团队可能会优先考虑那些能够使该生态系统中的开发人员受益的功能和改进。Go在云生态系统中的基础性作用为核心团队提供了强大的动力,以确保它仍然非常适合这些工作负载,并保持其在该领域相对于其他语言的竞争优势。
  • 探索Go在新兴领域的潜力(AI/ML,边缘计算): 尽管Go在AI/ML领域尚未占据主导地位 8,但在该领域的使用潜力正在增长,尤其是在部署模型和构建基础设施方面 10。同样,Go的高效性和小巧的体积使其成为边缘计算和IoT应用的有力候选者 8。核心团队对支持这些领域的努力可能会在未来增加,正如关于Go在AI系统中的作用的讨论所表明的那样 10。Go在处理大型数据集方面的高效率及其在高性能AI应用开发方面的潜力 8 表明,即使Go的目标不是取代Python成为主要的模型开发语言,核心团队也可能正在探索增强Go在某些AI/ML工作负载(如高性能推理或构建AI基础设施)方面的适用性的方法。Go的性能优势可以在速度和效率至关重要的AI/ML领域(如推理或边缘部署,其中低延迟至关重要)得到利用。Go的轻量级特性和内置的并发性 26 与边缘计算和IoT的需求非常契合,在这些环境中,资源受限和需要处理大量并发连接是很常见的。这种天然的契合性表明核心团队可能会继续优化Go以适应这些环境。
  • 提升开发者体验:工具和生态系统: 核心团队始终致力于通过增强工具 8(包括go命令、go vet和IDE集成 38)来改善开发者体验。错误处理 8 和包管理 5 的改进也是持续的优先事项。Go生态系统的健康发展 8 对于语言未来的成功至关重要。Go 1.24中引入的用于管理工具依赖的工具(使用go get -tool和go tool 18)直接解决了Go开发人员常见的workflow挑战,简化了开发所需的外部实用程序的管理,体现了对实用性和改善Go程序员日常体验的关注。简化开发工具的依赖管理可以改善整体开发者体验,并减少Go项目中的摩擦。诸如go vet(带有新的测试分析器)等现有工具的持续改进以及对新工具和功能的不断探索 8 表明,核心团队致力于为Go程序员提供一个健壮高效的开发环境,帮助他们编写更好更可靠的代码。强大的工具链对于开发者生产力至关重要,核心团队对这方面的投入反映了其对于Go语言长期成功的意义。

4. 不断演进的格局:未来5-10年的Go语言

展望未来,可以预见Go语言的几个趋势和潜在发展方向:

  • 预期的语言演进和潜在的新特性: 尽管Go 1.x一直秉持着对向后兼容性的坚定承诺 1,但泛型的引入 8 表明,Go愿意为了解决关键的局限性和满足社区的需求而进行演进。未来的演进可能包括进一步完善泛型、潜在地改进错误处理 8,以及基于社区反馈和不断发展的技术格局,谨慎地引入其他特性。关于“Go 2.0”的讨论 8 表明了对更重大变革的长期愿景,但核心团队强调将采取循序渐进的方式 35。正如Russ Cox 48 所阐述的,以及Go语言缓慢但稳步的发展历程 35 所反映的那样,核心团队对语言的改变采取谨慎的态度。这表明,虽然核心团队对演进持开放态度,但他们将继续优先考虑稳定性和向后兼容性,以避免破坏庞大的现有Go代码生态系统。这种谨慎的做法一直是Go语言发展的标志,并且很可能会继续下去,从而确保Go语言对于长期项目来说仍然是一个可靠的选择。
  • 标准库的增长和成熟: 标准库是Go语言的一大优势 1,提供了广泛的开箱即用功能。预计未来的增长将包括新的包以及对现有包的改进,可能涉及网络、数据处理和对新兴技术的支持等领域。math/rand/v2包的引入 10 为未来的库演进和现代化提供了一个范例。正如Go语言15周年纪念 10 中提到的那样,引入带有版本控制的新标准库包(如math/rand/v2)表明了一种具有前瞻性的库演进方法。这使得在不破坏与旧版本兼容性的情况下实现重大改进和新功能成为可能,为在遵守Go 1兼容性承诺的同时实现现代化提供了一条途径。
  • Go Modules和依赖管理的作用: Go Modules 5 已成为Go语言依赖管理的标准,未来的发展可能会侧重于进一步简化和增强该系统。go.mod中工具指令的引入 18 是这种演进的最新例证。对Go Modules的持续改进,例如跟踪工具依赖的能力 18,表明核心团队致力于提供一个健壮且用户友好的依赖管理系统。这对于大型复杂的Go项目的可扩展性和可维护性至关重要,并反映了持续改进开发者体验的努力。
  • 社区影响和开源贡献: Go的开源特性 1 意味着社区通过提案 49、贡献和反馈 16 在其发展中发挥着重要作用。核心团队通过调查 17 和讨论积极与社区互动,使得社区的意见成为塑造Go未来发展方向的关键因素。提案流程本身 56 确保了任何重大变更在被采纳之前都会在社区内得到仔细考虑和讨论。Go开发者调查 17 是核心团队收集广泛反馈并了解Go社区的使用模式、挑战和期望改进的关键机制。这种数据驱动的方法确保了语言的演进能够满足用户的实际需求。

5. Go的应用:应对现代挑战

Go语言的设计和近期发展使其能够很好地应对软件开发中的几个现代挑战:

  • 云计算和微服务:巩固Go的地位: Go的高效性、并发性和小巧的二进制文件使其非常适合构建云原生应用和微服务 3。其持续的演进,包括性能的提升和并发测试工具的改进,可能会进一步加强其在该领域的地位。Go语言通过goroutine和channel实现的内置并发模型 1 为构建需要高效处理大量并发请求的分布式系统和微服务提供了显著的优势。与依赖外部库实现并发的语言相比,这种内置的并发模型简化了可扩展和响应迅速的云应用的开发。
  • 边缘计算和物联网:发挥Go的效率优势: Go的性能和较小的资源占用使其成为边缘计算和物联网应用的绝佳选择 8。随着这些领域的持续增长,Go的作用预计将进一步扩大,尤其是在针对资源受限环境进行优化方面。Go语言生成的小巧且自包含的二进制文件 1 对于资源受限(如内存和处理能力)的边缘设备和物联网环境尤其有益。这使得Go应用能够在更广泛的硬件上高效运行。
  • WebAssembly:将Go的触角延伸到前端: 凭借Go 1.24中增强的Wasm支持和持续的开发 17,Go正成为构建高性能前端Web应用的可行选择,可能在某些领域挑战JavaScript的主导地位,尤其是在计算密集型任务或需要浏览器中实现类似原生性能的应用方面。即使编译为WebAssembly 24,Go的性能特性也为Web应用带来了相比传统基于JavaScript的解决方案的显著性能提升潜力,尤其是在涉及复杂计算或需要与系统资源紧密交互的应用方面。
  • 人工智能和机器学习:探索新的领域: 尽管在库的可用性方面仍然存在挑战 15,但Go的性能和效率使其成为部署和提供AI/ML模型的有希望的语言 8。未来的发展可能会看到对基于Go的AI/ML库和框架的更多投入,可能侧重于Go的优势(如用于并行处理的并发性)特别有益的领域。Go强大的性能和并发能力使其非常适合构建支持AI/ML工作负载的基础设施,例如数据处理管道、模型服务平台和分布式训练系统,即使它不会成为所有AI/ML开发阶段的主要语言。

6. Go未来面临的挑战和考虑因素

尽管Go语言的发展前景良好,但也面临着一些挑战和需要考虑的因素:

  • 在简洁性与特性扩展之间取得平衡: Go的简洁性是其核心优势之一 1,但诸如泛型等特性的加入也引入了复杂性。核心团队必须在对新特性的渴望与保持语言的简洁性和可读性之间仔细权衡 8。泛型的引入虽然解决了社区的一个主要需求,但也代表着Go最初极简主义设计理念的一次偏离。核心团队需要继续仔细评估未来的特性提案,以确保它们在提供实质性好处的同时,不会过度损害语言的可读性和易于理解的核心原则。
  • 回应社区反馈和不断变化的需求: Go社区对某些限制和期望的特性提出了很多意见 8,核心团队需要继续与这些反馈互动,并在坚守其核心原则的同时,使语言适应不断变化的需求 10。核心团队通过调查、博客文章和提案流程 17 与Go社区的积极互动对于确保语言的演进符合用户的实际需求和更广泛的软件开发趋势至关重要。维持这种开放的沟通和反馈循环对于Go语言的长期健康和相关性至关重要。
  • 来自其他编程语言的竞争: Go面临着来自其他现代编程语言(如Rust 5)以及其他也针对类似领域(如云原生开发和高性能计算)的语言的竞争。Go未来的成功将取决于其维持独特优势并继续响应竞争格局而发展自身的能力。尽管Go和Rust经常在相似的领域展开竞争,但它们提供了不同的权衡(例如,Go的简洁性与Rust对不使用垃圾回收的内存安全的关注)。Go的持续成功可能取决于强调其优势并解决其相对于竞争对手的劣势,例如错误处理的冗长 59 或其他语言中存在的某些高级语言特性的缺乏。

7. 结论:规划Go未来十年的发展方向

Go语言有望在未来5到10年内继续保持增长和发展。正如近期发布的版本和社区互动所表明的那样,核心团队的优先事项侧重于持续的性能改进、并发方面的进步、对WebAssembly的战略投资、加强安全性、持续优化云原生架构以及探索AI/ML和边缘计算等新兴领域。

尽管在简洁性与特性扩展之间取得平衡以及应对竞争格局将是关键的挑战,但Go语言强大的基础、活跃的社区以及核心团队致力于满足开发者需求的承诺,都预示着Go语言拥有光明的未来。其适应现代挑战的能力以及对实用解决方案的持续关注,可能会在未来几年内巩固其作为构建可靠、可扩展和高效软件系统的关键语言的地位。

有价值的表格:

  1. 表格:近期Go版本(Go 1.23和Go 1.24)的关键特性和关注领域
版本 关键语言特性 显著性能提升 工具增强 标准库新增/变更 版本体现的关注领域
Go 1.23 slices/maps中的迭代器函数 配置文件引导优化 (PGO) 改进的go命令 新增iter包 性能,泛型集成
Go 1.24 泛型类型别名 更快的map (Swiss Tables), 内存分配, 互斥锁 新的测试分析器,工具依赖管理 FIPS 140-3, os.Root, runtime.AddCleanup, 弱指针 性能,泛型,Wasm,安全,开发者体验
  1. 表格:Go语言的采用统计数据和趋势
年份 统计来源 指标 主要发现/趋势
2024 Stack Overflow 开发者调查 最受喜爱的编程语言之一 表明开发者满意度高。
2024 Talent.com 美国Go开发者平均年薪约为$132,823 显示出对Go开发者的强烈需求和高价值。
2023 Go开发者调查 H2 >90% 开发者满意度 突显了Go社区内的积极体验。
2021 Stack Overflow 调查 约9.55% 的开发者使用Go 显示出相当一部分开发者正在积极使用Go。
2020 JetBrains 开发者生态系统 约110万主要Go开发者,约270万包括第二语言 表明全球拥有庞大且不断增长的Go开发者社区。
2019 Stack Overflow 调查 Go是第三大最想学习的语言 表明随着更多开发者希望获得Go技能,其采用率将持续增长。
2024 Okoone.com Go的用户群在过去五年内增长了两倍 表明Go的受欢迎程度和采用率迅速增长。
2024 Developer Nation 调查 11% 的后端开发者目前使用Go 提供了Go在关键目标人群中的具体采用率。

Works cited

// 数量太多,这里省略。


Gopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且,2025年将在星球首发“Go陷阱与缺陷”和“Go原理课”专栏!此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格6$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily
  • Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

Go 1.24新特性前瞻:工具链和标准库

本文永久链接 – https://tonybai.com/2024/12/17/go-1-24-foresight-part2

上一篇文章中,我们介绍了即将于2025年2月发布的Go 1.24版本在语法、编译器和运行时方面的主要变化。本文将继续承接上文,重点介绍Go 1.24在工具链和标准库方面的重要更新,供大家参考。

1. 工具链

1.1 go.mod新增tool指示符,支持对tool的依赖管理(#48429)

我们日常编写Go项目代码时常常会依赖一些使用Go编写的工具,比如golang.org/x/tools/cmd/stringer或github.com/kyleconroy/sqlc。我们希望所有项目合作者都使用相同版本的工具,以避免在不同时间、不同环境中的输出不同的结果。因此,Go社区希望通过go.mod将工具的版本以及依赖管理起来。

在Go 1.24版本之前,Go Wiki推荐tools.go的一种来自社区的最佳实践,阐述这种实践的最好的一个示例来自Go modules by example中的一个文档:”Tools as dependencies“,其大致思路是将项目依赖的Go工具以“项目依赖”的方式存放到tools.go文件(放到go module根目录下)中,以golang.org/x/tools/cmd/stringer为例,tools.go的内容大致如下:

//go:build tools

package tools

import (
    _ "golang.org/x/tools/cmd/stringer"
)

然后在同一目录下安装stringer或直接go run:

$go install golang.org/x/tools/cmd/stringer

在安装stringer时,go.mod会记录下对stringer的依赖以及对应的版本,后续go.mod提交到项目repo中,所有项目成员就都可以使用相同版本的Stringer了。

tools.go实践虽然能解决问题,但这种方式还是存在一些不便:

  • 配置繁琐:需要手动创建 tools.go 文件,并添加特定的构建标签来排除它;
  • 使用不便:运行工具时可能需要额外的脚本或配置(每次手敲go run golang.org/x/tools/cmd/stringer的确有些不便)。

Go开发者期望工具依赖也能够无缝地与其他项目依赖(包依赖)统一管理,并纳入go.mod的版本控制体系。

为此,该提案设计并实现了下面几点以满足开发者的上述述求:

  • go.mod引入tool directive,用于显式声明项目所需的工具。
  • tool directive与其他依赖项统一纳入go.mod文件,方便管理和版本控制。
  • 扩展go install和go get命令,支持安装、更新和卸载工具。

我们来看一个示例,首先我们初始化一个module:

$ gotip mod init demo
go: creating new go.mod: module demo
$ cat go.mod
module demo

go 1.24

编辑go.mod,加入下面内容:

$ cat go.mod
module demo

go 1.24

tool golang.org/x/tools/cmd/stringer

安装tool前需要go get它的依赖,否则go install会报错:

$gotip install tool
no required module provides package golang.org/x/tools/cmd/stringer; to add it:
    go get golang.org/x/tools/cmd/stringer

$gotip get golang.org/x/tools/cmd/stringer
go: downloading golang.org/x/tools v0.28.0
go: downloading golang.org/x/sync v0.10.0
go: downloading golang.org/x/mod v0.22.0
go: added golang.org/x/mod v0.22.0
go: added golang.org/x/sync v0.10.0
go: added golang.org/x/tools v0.28.0

$ cat go.mod
module demo

go 1.24

tool golang.org/x/tools/cmd/stringer

require (
    golang.org/x/mod v0.22.0 // indirect
    golang.org/x/sync v0.10.0 // indirect
    golang.org/x/tools v0.28.0 // indirect
)

我们看到:go.mod中require了stringer的依赖。

接下来,我们便可以用go install安装stringer了:

$ ls -l `which stringer` // old版本的stringer
-rwxr-xr-x 1 root root 6500561 1月  23 2024 /root/go/bin/stringer

$ gotip install tool
$ ls -l `which stringer`
-rwxr-xr-x 1 root root 7303970 12月  9 21:41 /root/go/bin/stringer

后续要更新stringer版本,可以直接使用go get -u:

$gotip get -u golang.org/x/tools/cmd/stringer

此外,除了手工编辑go.mod,添加依赖的tool外,我们也可以直接使用go get -tool像go.mod中添加依赖的tool,它们在效果上是等价的:

// 重置go.mod到最初状态
# cat go.mod
module demo

go 1.24

// 执行go get -tool
$gotip get -tool golang.org/x/tools/cmd/stringer
go: added golang.org/x/mod v0.22.0
go: added golang.org/x/sync v0.10.0
go: added golang.org/x/tools v0.28.0

$ cat go.mod
module demo

go 1.24

tool golang.org/x/tools/cmd/stringer

require (
    golang.org/x/mod v0.22.0 // indirect
    golang.org/x/sync v0.10.0 // indirect
    golang.org/x/tools v0.28.0 // indirect
)

使用stringer时也无需手工敲入那么长的命令(go run golang.org/x/tools/cmd/stringer),只需使用gotip tool stringer即可:

$ gotip tool stringer
Usage of stringer:
    stringer [flags] -type T [directory]
    stringer [flags] -type T files... # Must be a single package
For more information, see:

https://pkg.go.dev/golang.org/x/tools/cmd/stringer

Flags:
  -linecomment
        use line comment text as printed text when present
  -output string
        output file name; default srcdir/<type>_string.go
  -tags string
        comma-separated list of build tags to apply
  -trimprefix prefix
        trim the prefix from the generated constant names
  -type string
        comma-separated list of type names; must be set

go tool stringer就相当于go run golang.org/x/tools/cmd/stringer@v0.28.0了(注:v0.28.0是当前golang.org/x/tools的版本)。

tool directive和go工具链做了很好的融合,除了上面的命令外,还支持:

  • go build tool构建module依赖的tool,并将构建出可执行文件放在当前目录下;
  • go build -o bin/ tool将构建module依赖的tool,并将构建出可执行文件放在项目自己的bin目录下。

到这里,屏幕前的你可能会问一个问题:如果本地多个项目依赖同一个工具的不同版本,比如golangci-lint的v1.62.2和v1.62.0时,那么两个项目安装的golangci-lint是否会相互覆盖和影响呢?我们来验证一下,下面建立两个项目:tool-directive1和tool-directive2。

.
├── tool-directive1/
│   ├── go.mod
│   └── go.sum
└── tool-directive2/
    ├── go.mod
    └── go.sum

我们先在tool-directive1下面执行下面命令添加对golangci-lint的依赖:

$gotip get -tool github.com/golangci/golangci-lint/cmd/golangci-lint
go: downloading github.com/golangci/golangci-lint v1.62.2
go: downloading github.com/gofrs/flock v0.12.1
go: downloading github.com/fatih/color v1.18.0
... ...

然后在同一个目录下,使用gotip tool golangci-lint执行该工具,查看其版本:

$ gotip tool golangci-lint --version
golangci-lint has version v1.62.2 built with devel go1.24-c8fb6ae6 Sun Dec 8 15:34:47 2024 +0000 from (unknown, modified: ?, mod sum: "h1:b8K5K9PN+rZN1+mKLtsZHz2XXS9aYKzQ9i25x3Qnxxw=") on (unknown)

我们看到tool-directive1依赖了v1.62.2版本的golangci-lint。不过你在执行上述命令时可能会注意到,这个命令的执行非常耗时,可能需要10~20s才能出结果。如果你再执行一次,它就可以瞬间输出结果,为什么会这样的?稍后我们给出答案。

现在我们切换到tool-directive2目录下,执行下面命令添加对golangci-lint v1.62.0版本的依赖:

$gotip get -tool github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.0

然后在同一个目录下,使用gotip tool golangci-lint执行该工具,查看其版本:

$gotip tool golangci-lint --version
golangci-lint has version v1.62.0 built with devel go1.24-c8fb6ae6 Sun Dec 8 15:34:47 2024 +0000 from (unknown, modified: ?, mod sum: "h1:/G0g+bi1BhmGJqLdNQkKBWjcim8HjOPc4tsKuHDOhcI=") on (unknown)

我们看到tool-directive2下得到的是v1.62.0版本的golangci-lint。并且我们会遇到同样的现象:第一次执行很慢,第二次执行就会瞬间出结果。

再回到tool-directive1下,看看它依赖的golangci-lint是否被覆盖了:

$gotip tool golangci-lint --version
golangci-lint has version v1.62.2 built with devel go1.24-c8fb6ae6 Sun Dec 8 15:34:47 2024 +0000 from (unknown, modified: ?, mod sum: "h1:b8K5K9PN+rZN1+mKLtsZHz2XXS9aYKzQ9i25x3Qnxxw=") on (unknown)

我们发现:两个项目下依赖的版本各自独立,并不会相互覆盖。

这其中的缘由又是什么呢?为什么使用go tool golangci-lint第一次执行会慢,而后续的执行就会飞快呢?下面的issue将回答这个问题。

1.2 Go run生成的可执行文件支持缓存(#69290)

Go 1.24 之前,cmd/go仅缓存编译后的包文件(build actions),而不缓存链接后的二进制文件(link actions)。不缓存二进制文件很大原因在于二进制文件比单个包对象文件大得多,并且它们不像包文件那样被经常重用。

不过上述1.1中,让go支持对依赖工具的管理以及让go tool支持自定义工具执行的issue让这个issue最终被纳入Go 1.24。该issue实现后,go run以及像上面那种go tool golangci-lint(本质上也是go run github.com/golangci/golangci-lint/cmd/golangci-lint@vx.y.z)的编译链接的结果会被缓存到go build cache中。这也是上面不同项目依赖同一工具不同版本时不会相互覆盖以及首次使用go tool执行依赖工具较慢的原因,第一次go tool执行会执行编译链接过程,之后的运行就会从缓存中直接找到缓存的文件并执行了。

由于这个issue会显著增大go build cache的磁盘空间占用,该issue也规定了,在缓存执行定期清理的时候,可执行文件缓存会优先于包缓存被优先清理掉

1.3 Go build支持生成伪版本号(#50603)

在Go 1.18及之后的版本中,cmd/go工具链在构建二进制文件时会嵌入依赖版本信息和VCS(版本控制系统)信息,这使得开发者可以更容易地追踪二进制文件的来源。然而,当使用go build命令构建主模块时,主模块的版本信息并不会被记录,而是显示为(devel),这导致开发者需要使用外部构建脚本或-ldflags来手动设置版本信息。相比之下,go install命令会正确记录主模块的版本信息。

该issue就旨在让go build命令也能像go install一样,自动嵌入主模块的版本信息,从而避免开发者依赖外部构建脚本。

落地后,Go 1.24的go build命令会在编译后的二进制文件中包含版本信息。如果本地VCS(版本控制系统)标签可用,主模块的版本将从该标签中设置。如果没有本地VCS标签可用,则会生成一个伪版本(pseudo-version),通常包含时间戳和提交哈希。 此外,为了避免与已发布的版本混淆,go build还会在伪版本中添加一些特殊的标识符,例如devel,以表明这是一个本地构建的版本。如果有未提交的VCS更改,则会附加一个+dirty后缀。

使用-buildvcs=false标志可以省略二进制文件中的版本控制信息。

下面对比一下Go 1.24版本之前与Go 1.24版本在go build时生成的版本信息的差异:

以Go 1.23为例,其构建和安装的stringer的版本信息如下:

$go version  -m `which stringer`
/root/go/bin/stringer: go1.23.0
... ...

而使用go1.24的build构建的stringer的版本信息如下:

$go version  -m tool-directive1/bin/stringer
tool-directive1/bin/stringer: devel go1.24-c8fb6ae6 Sun Dec 8 15:34:47 2024 +0000
... ...

1.4 默认使能GOCACHEPROG以支持外部缓存

估计Go社区很少有人用过GOCACHEPROG,即便在Go 1.21版本之后,它是以实验特性的形式提供的,通过GOEXPERIMENT=cacheprog启用。这个特性是由Go语言元老Brad Fitzpatrick提出的,其主issue编号是59719

我们知道:Go语言的cmd/go工具已经具备了强大的缓存支持,但其缓存机制仅限于基于文件系统的缓存。这种缓存方式在某些场景下效率不高,尤其是在CI(持续集成)环境中,用户通常需要将GOCACHE目录打包和解压缩,这往往比CI操作本身还要慢。此外,用户可能希望利用位于网络上的共享缓存(比如S3)或公司内部的P2P缓存协议来提高缓存效率,但这些功能并不适合直接集成到cmd/go工具中。

为了解决上述问题,Brad Fitzpatrick提出了一个新的环境变量GOCACHEPROG,类似于现有的GOCACHE变量。通过设置GOCACHEPROG,用户可以指定一个外部程序,该程序将作为子进程运行,并通过标准输入/输出来与cmd/go工具进行通信。cmd/go工具将通过这个接口与外部缓存程序交互,外部程序可以根据需要实现任意的缓存机制和策略。

为此,Bradfitz在issue 59719中给出了交互的协议设计。cmd/go工具与外部缓存程序之间的通信基于JSON格式的消息。消息分为请求(ProgRequest)和响应(ProgResponse)。请求包括命令类型、操作ID(ActionID)、对象ID(ObjectID)等。响应则包括缓存命中与否、对象的磁盘路径等信息。

其中请求的命令类型有如下几种:

  • get:从缓存中获取对象。
  • put:将对象存入缓存。
  • close:关闭缓存连接。

对于put请求,cmd/go工具会将对象的二进制数据通过base64编码后发送给外部程序。对于get请求,外部程序返回对象的磁盘路径。

在\$GOROOT/src/cmd/go/internal/cache/prog.go文件中可以看到具体协议相关的结构。

Bradfitz还给出了一个外部cache的样例程序go-tool-cache,还有开发者fork了该样例程序,将它改造为以S3为后端cache的外部缓存程序。感兴趣的童鞋,可以按照这些样例程序的说明试验一下外部缓存功能。

1.5 go工具链支持HTTP扩展认证:GOAUTH(#26232)

在Go语言中,go get命令用于从远程代码仓库获取依赖包。通常,这些依赖包的导入路径是通过HTTP请求获取的,服务器会返回一个包含元标签(meta tag)的HTML页面,指示如何获取该包的源代码。然而,对于需要身份验证的私有仓库,go get无法直接工作,因为go get使用的是net/http.DefaultClient,它不知道如何处理需要身份验证的URL。具体来说,当go get尝试获取一个私有仓库的URL时,由于没有提供身份验证信息,服务器会返回401或403错误,导致go get无法继续执行。这个问题在企业环境中尤为常见,因为许多公司使用私有代码托管服务,而这些服务通常需要身份验证。

issue 26232为上述情况提供了一种方案,让go get能够支持需要身份验证的私有仓库,使得用户可以通过go get命令获取私有仓库中的代码:

$go get git.mycompany.com/private-repo

即使https://git.mycompany.com/private-repo需要身份验证,go get也能够正常工作。

方案采用了一种类似于Git凭证助手的机制,并通过新增的Go环境变量GOAUTH来指定一个或多个认证命令。go get在执行时会调用这些命令,获取身份验证信息,并在后续的HTTP请求中使用这些信息。

GOAUTH环境变量可以包含一个或多个认证命令,每个命令由空格分隔的参数列表组成,命令之间用分号分隔。go get会在每次需要进行HTTP请求时,首先检查缓存中的认证信息,如果没有匹配的认证信息,则会调用GOAUTH命令来获取新的认证信息。

通过go help goauth可以查看GOAUTH的详细用法,在Go 1.24中它支持如下认证命令:

  • off:禁用GOAUTH功能
  • netrc:从NETRC或用户主目录中的.netrc文件中获取访问凭证,这也是GOAUTH的默认值
  • git dir:在指定目录dir中运行git credential fill并使用其凭证。go命令将运行git credential approve/reject来更新凭证助手的缓存。
  • command:执行给定的命令(以空格分隔的参数列表),并将提供的头信息附加到 HTTPS 请求中。该命令必须按照以下格式生成输出:
Response      = { CredentialSet } .
CredentialSet = URLLine { URLLine } BlankLine { HeaderLine } BlankLine .
URLLine       = /* URL that starts with "https://" */ '\n' .
HeaderLine    = /* HTTP Request header */ '\n' .
BlankLine     = '\n' .

1.6 go build支持-json(#62067)

Go 1.24版本之前,Go已经支持了go test -json命令,旨在为测试过程提供结构化的JSON输出,便于工具解析和处理测试结果。然而,当测试或导入的包在构建过程中失败时,构建错误信息会与测试的JSON输出交织在一起,导致工具难以准确地将构建错误与受影响的测试包关联起来。这增加了工具处理go test -json输出的复杂性。

为了解决这个问题,issue 62067提出了为go build命令(包括go install)添加-json标志的建议,以便生成与go test -json兼容的结构化JSON输出。go test -json也得到了优化,现在在test时出现构建错误时,go test -json也会以json格式输出构建错误信息,与test结果的json内容可以很好的融合在一起。当然,你也可以通过GODEBUG=gotestjsonbuildtext=1继续让go test -json输出文本格式的构建错误信息,以保持与Go 1.24之前的情况一致。

2. 标准库

Go标准库向来是添加新特性的大户,不过鉴于变化太多,下面我们仅列举一些主要的变化点。

2.1 json包支持omitzero选项

关于这个变化点,我在《JSON包新提案:用“omitzero”解决编码中的空值困局》一文中有详细说明,请移步阅读,这里不赘述了。

2.2 新增weak包和weak指针

weak包和weak指针是Go团队在设计和实现unique包时的“副产物”,Go团队认为weak指针可以给大家带来更灵活的内存管理机制,于是将其从internal中提到标准库中。我之前的《Go weak包前瞻:弱指针为内存管理带来新选择》一文对weak包有详细说明,请移步阅读。

2.3 crypto: FIPS 140-3认证

在Go 1.24开发周期中,Go密码学小组与Russ Cox根据开发者日益增多的密码学合规性(满足FIPS 140)的需求反馈,决定对Go的加密库进行改造,以符合申请进行FIPS 140标准认证的要求。有关这个认证的issue和改动点(cl)都很多,大家可以阅读我的《走向合规:Go加密库对FIPS 140的支持》一文了解详情。

2.4 crypto:增加hkdf、pbkdf2、sha3等密码学包

读过我的《Go开发者的密码学导航:crypto库使用指南》一文的读者都知道:Go密码学团队维护的密码学包分布在Go标准库crypto目录和golang.org/x/crypto下面。Go密码学小组负责人Roland Shoemaker认为当前这种”分割”的状态会带来一些问题

  • 用户困惑:用户经常对为什么某些加密库在x/crypto模块中,而另一些在标准库中感到困惑。这种困惑可能导致用户不愿意依赖x/crypto模块中的代码,因为他们误以为x/crypto中的代码是“实验性”的,质量或API稳定性不如标准库。
  • 复杂的安全补丁流程:标准库依赖于x/crypto模块中的多个包(目前有7个),这些包需要被vendored。这种依赖关系增加了安全补丁的复杂性,因为需要一个特殊的第三方流程来处理这些包的补丁,而不是像标准库或x/crypto模块那样直接处理。
  • 开发周期不一致:理论上,x/crypto模块是一个可以快速开发新加密算法或协议的地方,因为这些算法或协议的规范可能还在变化中。然而,实际上,x/crypto模块并没有被这样使用。如果开始这样做,反而会强化用户对x/crypto模块的误解。
  • 特定包的快速开发需求:例如x/crypto/ssh包最近经历了非常快速的开发,许多用户希望立即使用新引入的功能和修复。如果将这个包移入标准库,可能会因为标准库的发布周期较慢而产生摩擦。

为此Shoemaker提议了一个将x/crypto下的包到标准库crypto目录下的方案,以简化Go语言加密库的管理和维护,提高用户对这些库的信任和使用率,方案的大致思路和步骤如下:

  • 将x/crypto模块中的大部分包直接迁移到标准库的crypto/目录下,迁移过程应在单个标准库发布周期内完成,尽量接近发布周期的末尾,以避免需要同步两个版本的包。
  • 迁移后,冻结x/crypto模块和标准库中的对应包,直到标准库重新开放,只接受标准库版本的更改。
  • 使用构建标签(build tags)来区分迁移前后的版本,允许用户在不更新到最新Go版本的情况下继续使用x/crypto模块。
  • 在迁移后的两个主要版本中(例如,假设在Go 1.24中完成迁移,则在Go 1.26中),移除旧的构建标签实现,只保留转发到标准库版本的包装器。
  • 一些包由于其更新周期与标准库不一致,或者已经冻结/弃用,将不会迁移到标准库中。例如,x/crypto/x509roots包需要根据任意时间表进行更新,因此应移至独立的模块golang.org/x/x509roots。
  • 一些已经弃用或冻结的包(如twofish、cast5、tea等)将保留在x/crypto模块中,并在v1版本中标记为冻结。
  • x/crypto/ssh包由于其快速的开发周期,可能会在迁移时带来一些麻烦。虽然可以考虑将其推迟迁移,但最终仍建议将其移入标准库。

基于上述方案,Go 1.24版本中,Go密码学团队完成了hkdf、pbkdf2、sha3和mlkem等包的迁移。当然这次迁移与Go密码学包要进行FIPS 140-3认证也有着直接的联系。

这里面值得一提的是mklem包,它实现了NIST FIPS 203中指定的抗量子密钥封装方法ML-KEM(以前称为Kyber),也是Go密码学包中第一个后量子密码学包。

2.5 支持限制目录的文件系统访问(#67002)

目录遍历漏洞(Directory Traversal Vulnerabilities)和符号链接遍历漏洞(Symlink Traversal Vulnerabilities)是常见的安全漏洞。攻击者通过提供相对路径(如”../../../etc/passwd”)或创建符号链接,诱使程序访问其本不应访问的文件,从而导致安全问题。例如,CVE-2024-3400 是一个最近的真实案例,展示了目录遍历漏洞如何导致远程代码执行。

在Go中,虽然可以通过 filepath.IsLocal等函数来验证文件名,但防御符号链接遍历攻击较为困难。现有的os.Open和os.Create等函数在处理不受信任的文件名时,容易受到这些攻击的影响。

为了解决这些问题,issue 67002提出了在os包中添加几个新的函数和方法,以安全地打开文件并防止目录遍历和符号链接遍历攻击。

最初该提案提出新增一些安全访问文件系统的API函数,在讨论过程中,Russ Cox 提出了一个更为简洁的方案,避免了引入大量新的 API,而是通过引入一个新的类型 Dir 来表示受限的文件系统根目录。这个方案最终奠定了该提案的最终实现。

最终Go在os包中引入了一个新的Root类型,并基于该类型提供了在特定目录内执行文件系统操作的能力。os.OpenRoot函数打开一个目录并返回一个os.Root。os.Root上的方法仅限于在该目录内操作,并且不允许路径引用目录外的位置,包括跟随符号链接指向目录外的路径。下面是一些Root类型的常用方法:

  • os.Root.Open 打开一个文件以供读取。
  • os.Root.Create 创建一个文件。
  • os.Root.OpenFile 是通用的打开调用。
  • os.Root.Mkdir 创建一个目录。

下面我们用一个示例对比一下通过os.Root进行的文件系统操作与传统文件系统操作的差异:

// go1.24-foresight/stdlib/osroot/main.go
package main

import (
    "fmt"
    "os"
)

func main() {
    // 使用 os.Root 访问相对路径
    root, err := os.OpenRoot(".") // 打开当前目录作为根目录
    if err != nil {
        fmt.Println("Error opening root:", err)
        return
    }
    defer root.Close()

    // 尝试访问相对路径 "../passwd"
    file, err := root.Open("../passwd")
    if err != nil {
        fmt.Println("Error opening file with os.Root:", err)
    } else {
        fmt.Println("Successfully opened file with os.Root")
        file.Close()
    }

    // 传统的 os.OpenFile 方式
    // 尝试访问相对路径 "../passwd"
    file2, err := os.OpenFile("../passwd", os.O_RDONLY, 0644)
    if err != nil {
        fmt.Println("Error opening file with os.OpenFile:", err)
    } else {
        fmt.Println("Successfully opened file with os.OpenFile")
        file2.Close()
    }
}

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

$gotip run main.go
Error opening file with os.Root: openat ../passwd: path escapes from parent
Successfully opened file with os.OpenFile

我们看到:当代码通过os.Root返回的目录来尝试访问相对路径”../passwd”时,由于os.Root限制了操作仅限于根目录内,因此会返回错误。

从安全角度来看,Go 1.24之后,建议搭建多多使用这种安全操作文件系统的方式,如果你的文件操作都局限在一个目录下。

2.6 使用runtime.AddCleanup替代SetFinalizer(#67535)

Go 1.24版本之前,Go提供了runtime.SetFinalizer函数用于对象的终结处理。然而,SetFinalizer的使用存在许多问题和限制,Michael Knyszek总结了下面几点:

  • 必须引用分配的第一个字:SetFinalizer必须引用分配的第一个字,这要求程序员了解什么是“分配”,而这一概念在语言中通常不暴露。
  • 每个对象只能有一个终结器:不能为同一个对象设置多个终结器。
  • 引用循环问题:如果对象参与了引用循环,且该对象有终结器,那么该对象将不会被释放,终结器也不会运行。
  • GC周期问题:有终结器的对象至少需要两个GC周期才能被释放。

后面两个问题主要源于SetFinalizer允许对象复活(object resurrection),这使得对象的清理变得复杂且不可靠。

为了解决上述问题,,Michael Knyszek提出了一个新的API runtime.AddCleanup,并建议正式弃用runtime.SetFinalizer。AddCleanup的设计目标是解决SetFinalizer的诸多问题,特别是避免对象复活,从而允许对象的及时清理,并支持对象的循环清理。

AddCleanup函数的原型如下:

func AddCleanup[T, S any](ptr *T, cleanup func(S), arg S) Cleanup

AddCleanup函数将一个清理函数附加到ptr。当ptr不再可达时,运行时会在一个单独的goroutine中调用 cleanup(arg)。

AddCleanup的一个典型的用法如下:

f, _ := Open(...)
runtime.AddCleanup(f, func(fd uintptr) { syscall.Close(fd) }, f.Fd())

通常,ptr是一个包装底层资源的对象(例如上面典型用法中的那个包装操作系统文件描述符的File对象),arg是底层资源(例如操作系统文件描述符),而清理函数释放底层资源(例如,通过调用close系统调用)。

AddCleanup对ptr的约束很少,支持为同一个指针附加多个清理函数。不过,如果ptr可以从cleanup或arg中可达,ptr将永远不会被回收,清理函数也永远不会运行。作为一种简单的保护措施,如果arg等于ptr,AddCleanup会引发panic。清理函数的运行顺序没有指定。特别是,如果几个对象相互指向并且同时变得不可达,它们的清理函数都可以运行,并且可以以任何顺序运行。即使对象形成一个循环也是如此。

cleanup(arg)调用并不总是保证运行,特别是它不保证在程序退出之前能运行。

清理函数可能在对象变得不可达时立即运行。为了正确使用清理函数,程序必须确保对象在清理函数安全运行之前保持可达。存储在全局变量中的对象,或者可以通过从全局变量跟踪指针找到的对象,是可达的。函数参数或方法接收者可能在函数最后一次提到它的地方变得不可达。为了确保清理函数不会过早调用,我们可以将对象传递给KeepAlive函数,以保证对象在保持可达的最后一个点之后依然可达。

到这里,也许一些读者想到了RAII(Resource Acquisition Is Initialization),RAII的核心思想是将资源的获取和释放与对象的生命周期绑定在一起,从而确保资源在对象不再使用时能够被正确释放。似乎AddCleanup可以用于实现Go版本的RAII,下面是一个示例:

// go1.24-foresight/stdlib/addcleanup/main.go

package main

import (
    "fmt"
    "os"
    "runtime"
    "syscall"
    "time"
)

type FileResource struct {
    file *os.File
}

func NewFileResource(filename string) (*FileResource, error) {
    file, err := os.Open(filename)
    if err != nil {
        return nil, err
    }

    // 使用 AddCleanup 注册清理函数
    fd := file.Fd()
    runtime.AddCleanup(file, func(fd uintptr) {
        fmt.Println("Closing file descriptor:", fd)
        syscall.Close(int(fd))
    }, fd)

    return &FileResource{file: file}, nil
}

func main() {
    fileResource, err := NewFileResource("example.txt")
    if err != nil {
        fmt.Println("Error opening file:", err)
        return
    }

    // 模拟使用 fileResource
    _ = fileResource
    fmt.Println("File opened successfully")

    // 当 fileResource 不再被引用时,AddCleanup 会自动关闭文件
    fileResource = nil
    runtime.GC() // 强制触发 GC,以便清理 fileResource
    time.Sleep(time.Second * 5)
}

运行上述代码得到如下结果:

$gotip run main.go
File opened successfully
Closing file descriptor: 3

的确,在Go中,runtime.AddCleanup可以用来模拟RAII机制,但与传统的RAII有一些不同,在Go中,资源获取通常是通过显式的函数调用来完成的,例如打开文件等,而不是像C++那样在构造函数中隐式完成。并且,资源的释放由Go GC回收对象时触发。如果要实现C++那样的RAII,需要我们自行做一些封装。

2.7 不易出错的新Benchmark函数(#61515)

在Go语言中,基准测试(benchmarking)是通过testing.B类型的b.N来实现的。b.N表示基准测试需要执行的迭代次数。然而,这种设计存在一些问题:

  • 容易忘记使用b.N:在某些情况下,开发者可能会忘记使用b.N,导致基准测试无法正确执行。
  • 误用b.N:开发者可能会错误地将b.N用于其他目的,例如调整算法输入的大小,而不是作为迭代次数。
  • 复杂的计时器管理:基准测试框架无法知道b.N循环何时开始,因此如果基准测试有复杂的设置(setup),开发者需要手动调用ResetTimer来重置计时器,这提高了开发人员使用benchmark函数的门槛,还非常容易出错。

为了解决上述问题,Austin Clements提议在testing.B中添加一个新的方法Loop,并鼓励开发者使用Loop而不是b.N:

func (b *B) Loop() bool

func Benchmark(b *testing.B) {
    ...(setup)
    for b.Loop() {
        // … benchmark body …
    }
    ...(cleanup)
}

显然新Loop方法以及基于新Loopfang方法的“新Benchmark”函数有如下优点:

  • 避免误用b.N:Loop方法明确地用于基准测试的迭代,开发者无法将其用于其他目的。
  • 自动计时器管理:基准测试框架可以仅记录发生在基准测试操作期间(即for循环内部)的时间和其他指标,因此开发者不再需要手动调用ResetTimer或担心setup的复杂性了。
  • 减少重复设置:Loop方法可以在内部处理迭代启动(ramp-up),这意味着基准测试之前的setup只会执行一次,而不是在每次启动步骤中重复执行。这对于具有复杂设置的基准测试来说,可以节省大量时间。
  • 防止编译器优化:对go编译器来说,Loop方法本身就是一个的明显信号,可阻止某些优化(如内联),以确保基准测试结果的有效性。
  • 支持更丰富的统计分析:将来,Loop方法可以收集值分布而不是仅仅平均值,从而提供更深入的基准测试结果分析。

这里也强烈建议大家在Go 1.24及以后版本中,使用基于B.Loop的新基准测试函数

2.8 增加实验包testing/synctest(#69687)

在Go语言中,测试并发代码一直是一个具有挑战性的任务。传统的测试方法通常依赖于真实的系统时钟和同步机制,这会导致测试变得缓慢且容易出现不确定性(即“flaky”测试)。例如,测试一个带有超时机制的并发缓存时,测试代码可能需要等待几秒钟来验证缓存条目是否在预期时间内过期。这种等待不仅增加了测试的执行时间,还可能导致测试在某些情况下失败,尤其是在CI系统负载较高或执行环境不稳定的情况下。

为了解决这些问题,Go社区提出了一个新的testing/synctest包,旨在简化并发代码的测试。该包的核心思想是通过使用虚拟时钟和goroutine组(也称为气泡(bubble)来控制并发代码的执行,从而使测试既快速又可靠。下面是synctest包的API:

func Run(f func()) {
    synctest.Run(f)
}

func Wait() {
    synctest.Wait()
}

我们看到synctest包对外仅暴露两个公开函数。

Run函数在一个新的goroutine中执行f函数,并创建一个独立的goroutine组(气泡),确保所有相关的goroutine都在虚拟时钟的控制下执行。气泡内的goroutine不能与气泡外的goroutine直接交互,否则会引发panic。如果所有goroutine都被阻塞且没有定时器被调度,Run会引发panic。Run 会在气泡中的所有goroutine退出后返回。

Wait函数调用后将阻塞,直到当前气泡中的所有其他goroutine都处于持久阻塞状态。该函数用于确保在虚拟时间推进后,所有相关的goroutine都已经完成其工作。即确保在测试继续之前所有后台goroutine都已空闲或退出。如果从非气泡的goroutine调用Wait,或者同一气泡中的两个goroutine同时调用Wait,会引发panic。阻塞在系统调用或外部事件(如网络操作)的goroutine不是持久阻塞的,Wait不会等待这些goroutine。

这里再明确一下上面API说明中提到的各种概念:

  • goroutine组(气泡)

Run函数创建的goroutine及其间接启动的所有goroutine形成一个独立的“气泡”。气泡内的goroutine使用虚拟时钟,并且气泡内的所有操作(如通道、定时器等)都与该气泡关联。气泡内的goroutine不能与气泡外的goroutine直接交互。

  • 虚拟时钟

虚拟时钟的初始时间为2000-01-01 00:00:00 UTC。每个气泡有一个虚拟时钟,它只有在所有goroutine都处于阻塞状态时才会推进。这意味着测试代码可以精确控制时间的流逝,而不会受到真实系统时钟的限制。

  • 持久阻塞

一个goroutine如果只能被气泡内的另一个goroutine解除阻塞,则称其为持久阻塞。以下操作会使goroutine持久阻塞:

- 在气泡内向通道发送或接收数据
- 在select语句中,每个case都是气泡内的通道
- sync.Cond.Wait
- time.Sleep

下面是一个使用testing/synctest进行测试的简单示例,我们有一个Cache结构:

// go1.24-foresight/stdlib/synctest/cache.go

package main

import (
    "sync"
    "time"
)

// Cache 是一个泛型并发缓存,支持任意类型的键和值。
type Cache[K comparable, V any] struct {
    mu      sync.Mutex
    items   map[K]cacheItem[V]
    expiry  time.Duration
    creator func(K) V
}

// cacheItem 是缓存中的单个条目,包含值和过期时间。
type cacheItem[V any] struct {
    value     V
    expiresAt time.Time
}

// NewCache 创建一个新的缓存,带有指定的过期时间和创建新条目的函数。
func NewCache[K comparable, V any](expiry time.Duration, f func(K) V) *Cache[K, V] {
    return &Cache[K, V]{
        items:   make(map[K]cacheItem[V]),
        expiry:  expiry,
        creator: f,
    }
}

// Get 返回缓存中指定键的值,如果键不存在或已过期,则创建新条目。
func (c *Cache[K, V]) Get(key K) V {
    c.mu.Lock()
    defer c.mu.Unlock()

    // 检查缓存中是否存在该键
    item, exists := c.items[key]

    // 如果键存在且未过期,返回缓存的值
    if exists && time.Now().Before(item.expiresAt) {
        return item.value
    }

    // 如果键不存在或已过期,创建新条目
    value := c.creator(key)
    c.items[key] = cacheItem[V]{
        value:     value,
        expiresAt: time.Now().Add(c.expiry),
    }

    return value
}

上述代码实现了一个简单的并发缓存,支持泛型键和值,并且具有过期机制。通过使用sync.Mutex来保护对缓存条目的并发访问,确保了线程安全。Get方法在键不存在或已过期时,会调用creator函数创建新条目,并更新缓存。

下面是对上面Cache结构进行并发测试的代码:

// go1.24-foresight/stdlib/synctest/cache_test.go

package main

import (
    "testing"
    "testing/synctest"
    "time"
)

func TestCacheEntryExpires(t *testing.T) {
    synctest.Run(func() {
        count := 0
        c := NewCache(2*time.Second, func(key string) int {
            count++
            return count
        })

        // Get an entry from the cache.
        if got, want := c.Get("k"), 1; got != want {
            t.Errorf("c.Get(k) = %v, want %v", got, want)
        }

        // Verify that we get the same entry when accessing it before the expiry.
        time.Sleep(1 * time.Second)
        synctest.Wait()
        if got, want := c.Get("k"), 1; got != want {
            t.Errorf("c.Get(k) = %v, want %v", got, want)
        }

        // Wait for the entry to expire and verify that we now get a new one.
        time.Sleep(3 * time.Second)
        synctest.Wait()
        if got, want := c.Get("k"), 2; got != want {
            t.Errorf("c.Get(k) = %v, want %v", got, want)
        }
    })
}

通过使用synctest.Run和synctest.Wait,上述测试代码能够在虚拟时钟的控制下验证Cache的过期机制。synctest.Run创建了一个独立的goroutine组,确保所有相关的goroutine都在虚拟时钟的控制下执行。synctest.Wait确保在虚拟时间推进后,所有相关的goroutine都已经完成其工作。

使用gotip执行该测试:

$GOEXPERIMENT=synctest  gotip test -v
=== RUN   TestCacheEntryExpires
--- PASS: TestCacheEntryExpires (0.00s)
PASS
ok      demo    0.002s

我们可以瞬间得到结果,而无需等待代码中的Sleep秒数

2.9 其他一些变化

slog包添加包级变量slog.DiscardHandler (类型为slog.Handler ),它将丢弃所有日志输出。

下面是五个返回迭代器的新增函数,以strings包为例:

- func Lines(s string) iter.Seq[string] 

返回一个迭代器,遍历字符串s中以换行符结尾的行。

- func SplitSeq(s, sep string) iter.Seq[string] 

返回一个迭代器,遍历s中由sep分隔的所有子字符串。  

- func SplitAfterSeq(s, sep string) iter.Seq[string] 

返回一个迭代器,遍历s中在每个sep实例之后分割的子字符串。 

- func FieldsSeq(s string) iter.Seq[string] 

返回一个迭代器,遍历s中由空白字符(由unicode.IsSpace定义)分隔的子字符串。

- func FieldsFuncSeq(s string, f func(rune) bool) iter.Seq[string] 

返回一个迭代器,遍历s中由满足f(c)的Unicode码点分隔的子字符串。

和weak包一样,HashTrieMap同样是实现unique包的副产品,但它的性能很好,在很多情况下都要比sync.Map快很多。于是Michael Knyszek使用HashTrieMap替换了sync.Map的底层实现。

当然,如果你不满意HashTrieMap的表现,你也可以使用GOEXPERIMENT=nosynchashtriemap恢复到sync.Map之前的实现。

在Go语言的net/http包中,HTTP/2的支持默认是通过TLS加密的连接来实现的,通常称为”h2″。然而,HTTP/2也可以在不加密的TCP连接上运行,这种模式被称为”h2c”(HTTP/2 Clear Text)。尽管golang.org/x/net/http2/h2c包提供了对h2c的支持,但这种支持并不直接集成到net/http包中,导致用户在使用h2c时需要进行复杂的配置和处理。因此,社区提出了将h2c支持直接集成到net/http包中的issue,以简化用户的使用体验。

直接集成h2c支持后,将使得Go语言的HTTP/2功能更加完整,用户可以更方便地在未加密的连接上使用HTTP/2。

3. 其它

3.1 支持go:wasmexport指示符(#65199)

Go语言在WebAssembly(Wasm)的支持方面已经有了一定的进展,特别是在Go 1.21版本引入了go:wasmimport指示符,使得Go代码可以调用Wasm宿主定义的函数。然而,目前仍然无法从Wasm宿主调用Go代码。这对于一些需要扩展功能的应用来说是一个限制,例如Envoy、Istio、VS Code等应用,它们允许通过调用Wasm编译的代码来扩展功能。但Go目前无法支持这些应用,因为Go编译的Wasm模块中唯一导出的函数是_start,对应于main包中的main函数。

但Go社区对导出Go函数为wasm有着迫切的需求,同时,导出函数到Wasm宿主也是实现GOOS=wasip2的必要条件(wasip2是WASI规范的预览2版本)。

于是issue 65199给出了导出Go函数到Wasm的落地方案。该issue提议在库模式下(即导出的Go函数供其他基于wasm运行时库开发的应用使用),重用-buildmode构建标志值c-shared,用于wasip1。它现在向编译器发出信号,要求用_initialize函数替换_start函数,该函数执行运行时和包的初始化:

$gotip help buildmode
... ...
    -buildmode=c-shared
        Build the listed main package, plus all packages it imports,
        into a C shared library. The only callable symbols will
        be those functions exported using a cgo //export comment.
        On wasip1, this mode builds it to a WASI reactor/library,
        of which the callable symbols are those functions exported
        using a //go:wasmexport directive. Requires exactly one
        main package to be listed.
... ...

新增一个编译器指示符go:wasmexport,用于向编译器发出信号,表明某个函数应该使用Wasm导出(Wasm export),在生成的Wasm二进制文件中导出。该指示符只能在GOOS=wasip1时使用,否则会导致编译失败。

//go:wasmexport name

其中name是导出函数的名称,该参数是必需的。该指示符只能用于函数,不能用于方法

该issue由Johan Brandhorst提出,但最终是由CherryMui给出了最终实现,并且CherryMui还给出了一个应用go:wasmexport的example,这个example演示了go:wasmexport在库模式下的应用方法。例子代码较多,这里我做了一个裁剪,下面是裁剪后的代码和使用方法,大家可以参考一下。

示例的结构如下:

$tree -F ./wasmtest
./wasmtest
├── Makefile
├── go.mod
├── go.sum
├── testprog/
│   └── x.go
└── w.go

其中testprog/x.go中导出了一个Add函数:

// go1.24-foresight/wasmtest/testprog/x.go

package main

func init() {
    println("init function called")
}

//go:wasmexport Add
func Add(a, b int64) int64 {
    return a+b
}

func main() {
        println("hello")
}

我们将x.go编译为x.wasm文件:

$GOARCH=wasm GOOS=wasip1 gotip build -buildmode=c-shared -o x.wasm ./testprog

然后在w.go中使用x.wasm中的Add函数:

// go1.24-foresight/wasmtest/w.go

package main
import (
    "context"
    "fmt"
    "os"
    "github.com/tetratelabs/wazero"
    "github.com/tetratelabs/wazero/api"
    "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
)

func main() {
    ctx := context.Background()
    r := wazero.NewRuntime(ctx)
    defer r.Close(ctx)
    buf, err := os.ReadFile(os.Args[1])
    if err != nil {
        panic(err)
    }
    config := wazero.NewModuleConfig().
        WithStdout(os.Stdout).WithStderr(os.Stderr).
        WithStartFunctions() // don't call _start
    wasi_snapshot_preview1.MustInstantiate(ctx, r)
    m, err := r.InstantiateWithConfig(ctx, buf, config)
    if err != nil {
        panic(err)
    }

    // get export functions from the module
    F := func(a int64, b int64) int64 {
        exp := m.ExportedFunction("Add")
        r, err := exp.Call(ctx, api.EncodeI64(a), api.EncodeI64(b))
        if err != nil {
            panic(err)
        }
            rr := int64(r[0])
                fmt.Printf("host: Add %d + %d = %d\n", a,b,rr)
                return rr
    }

    // Library mode.
    entry := m.ExportedFunction("_initialize")
    fmt.Println("Library mode: initialize")
    _, err = entry.Call(ctx)
    if err != nil {
        panic(err)
    }
    fmt.Println("\nLibrary mode: call export functions")
    println(F(5,6))
}

运行上述w.go,我们将得到以下预期结果:

$gotip run w.go ./x.wasm
Library mode: initialize
init function called

Library mode: call export functions
host: Add 5 + 6 = 11
11

3.2 移植(porting)

  • Linux:要求内核版本不低于3.2。
  • macOS:Go 1.24是支持macOS 11 Big Sur的最后一个版本。
  • Windows:提升对Nano Server和内置服务帐户的支持,并修复域环境中的性能问题。
  • 支持的Unicode版本升级到15.1.0。

4. 小结

本文详细介绍了即将发布的Go 1.24版本在工具链和标准库方面的重要新特性。这些新特性不仅简化了工具的使用,提升了开发体验,还增强了标准库的功能和安全性,特别是在加密、并发测试等方面。通过这些改进,Go语言将继续朝着更高效、更安全、更易用的方向发展。

本文涉及的源码可以在这里下载。

5. 参考资料


Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时,我们也会加强代码质量和最佳实践的分享,包括如何编写简洁、可读、可测试的Go代码。此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily
  • Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 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

文章

评论

  • 正在加载...

分类

标签

归档



Statcounter View My Stats