标签 go.mod 下的文章

体验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

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

深入GOCACHEPROG:Go构建缓存的自定义扩展

本文永久链接 – https://tonybai.com/2025/03/04/deep-dive-into-gocacheprog-custom-extensions-for-go-build-cache

1. 背景

众所周知,Go build cache是在Go 1.10版本加入到Go工具链中的,缓存的主要目标是避免重复编译相同的代码,从而加快构建速度。

默认情况下,Go构建缓存位于用户主目录下的一个特定目录中,例如,Linux上通常是\$HOME/.cache/go-build,Windows上是%LocalAppData%\go-build)。Mac上则是\$HOME/Library/Caches/go-build。当然,Go开发者也可以通过GOCACHE环境变量自定义缓存位置。构建缓存的目录布局结构如下:

除了Go build/install命令外,go test命令也会利用构建缓存(包括fuzzing test)。除了编译测试代码本身,go test还会缓存测试结果:如果测试代码和依赖项没有变化,并且之前的测试通过,go test会报告(cached),表示测试结果来自缓存,而无需重新运行测试。如果测试代码或依赖项发生变化,或者之前的测试失败,go test会重新编译和运行测试。

我们看到在GOCACHE目录下还有两个文件,一个是trim.txt,另外一个是testexpire.txt。trim.txt用于对Go构建缓存进行修剪(trim)(\$GOROOT/src/cmd/go/internal/cache/cache.go),删除不太可能被重复使用的旧缓存条目,避免因过时的缓存占用过多资源,以保持缓存的高效性和有效性,trim.txt中保存了上次进行修剪的时间。testexpire.txt则是用于go clean清理测试缓存(\$GOROOT/src/cmd/go/internal/clean/clean.go)。

默认的Go构建缓存机制取得了不错的构建和测试加速效果,可以满足了大多数需求。不过,也有一些接纳Go的开发者以及公司希望Go构建缓存支持自定义扩展。前Go核心成员、tailscale联创之一的Brad Fitzpatrick在2023年就提出了Go构建缓存自定义扩展的提案

在提案中,Bradfitz认为Go内置的构建缓存机制仅支持基于本地文件系统的缓存。在一些持续集成 (CI) 环境中,通常的做法是在每次运行时解和压缩\$GOCACHE目录,这种方法效率低下,甚至可能比CI操作本身还要慢(例如,GitHub Actions 中的缓存)。提案希望Go能够支持更灵活地自定义构建缓存机制,例如:

  • 直接利用GitHub的原生缓存系统(而不是低效的 tar/untar)。
  • 在公司内部的可信同事之间实现P2P缓存共享协议。

这些扩展的高级功能不太可能直接添加到Go工具本身中,因此Bradfitz希望Go命令可以支持指定一个特定的程序来扩展和管理缓存,这个特定的程序将作为Go命令启动的一个子进程的形式运行,go命令将内部的缓存接口转换为与子进程的通信协议,通过stdin/stdout与其通信。这样该特定的子程序就可以实现任意的缓存机制和策略了。Go与特定程序(比如my-cache-prog)的关系见下面示意图:

Bradfitz也对比了使用FUSE(用户空间文件系统)的方案(比如使用juicefs将基于S3的共享文件系统挂载到每个开发人员以及ci节点上,但Bradfitz认为FUSE文件系统在linux之外的平台上不稳定,在很多CI环境下无法工作。因此,一个Go原生支持的用户自定义构建缓存机制是非常有必要的,可以解决Go内置缓存的局限性,特别是在CI环境和团队协作场景中。它通过提供一个外部程序接口来实现灵活性,避免了直接修改Go命令本身。

在《Go 1.24中值得关注的几个变化》以及《Go 1.24新特性前瞻:工具链和标准库》我们也提及了Go 1.24新增的实验特性:通过GOCACHEPROG实现Go构建缓存(go build cache)的自定义扩展。并提到了Bradfitz给出的GOCACHEPROG的参考实现go-tool-cache。不过Go 1.24正式版发布后,我使用Go 1.24.0验证了一下go-tool-cache,发现go-tool-cache似乎已经无法与Go 1.24.0正常协作了

$go version
go version go1.24.0 linux/amd64
$GOCACHEPROG="./go-cacher --verbose --cache-dir /tmp/go-cache" go install fmt
2025/03/03 17:00:30 put(action b8310cbc256f74a5f615df68a3a97753d42e1665adc309e78f20fc13259dec98, obj , 902 bytes): failed to write file to disk with right size: disk=1275; wanted=902
2025/03/03 17:00:30 put(action bc54b2b00ab97b34ef769b66fbe4afd5998f46f843cf2beddcd41974a2564bb1, obj , 1650 bytes): failed to write file to disk with right size: disk=116838; wanted=1650
2025/03/03 17:00:30 put(action 9c4f13b659995a6010f99d4427a18cf2e77919d251ef15e0f751bfdc2dff1806, obj , 1473 bytes): failed to write file to disk with right size: disk=273; wanted=1473
2025/03/03 17:00:30 put(action 6600d21f6b5d283315d789f13e681eed1c51d3ddde835b0f14817ecd144a667e, obj , 566 bytes): failed to write file to disk with right size: disk=565; wanted=566
/root/.bin/go1.24.0/src/internal/runtime/maps/runtime_swiss.go:11:2: package internal/asan is not in std (/root/.bin/go1.24.0/src/internal/asan)
/root/.bin/go1.24.0/src/internal/runtime/maps/group.go:10:2: package internal/runtime/sys is not in std (/root/.bin/go1.24.0/src/internal/runtime/sys)
/root/.bin/go1.24.0/src/fmt/print.go:8:2: package internal/fmtsort is not in std (/root/.bin/go1.24.0/src/internal/fmtsort)
/root/.bin/go1.24.0/src/sync/hashtriemap.go:10:2: package internal/sync is not in std (/root/.bin/go1.24.0/src/internal/sync)

修正这个问题还是新实现一个GOCACHEPROG的扩展程序呢?我们选择后者,这样可以让我们更好地从头理解GOCACHEPROG。在这篇文章中,我们会从理解GOCACHEPROG protocol开始,逐步深入到实现自定义缓存管理的具体步骤,包括代码示例。后续基于这个基础,大家可以自己动手,实现满足你的个人/组织需求的Go构建缓存的管理程序。

我们首先来看看Go命令与GOCACHEPROG扩展程序间的协议,这是实现自定义缓存扩展程序的核心。

2. 协议

cmd/go/internal/cacheprog包的文档中,有关于Go命令与GOCACHEPROG扩展程序间的协议的详细说明。下面基于该文档,我们对这个协议做一些说明,并作为后续实现的参考。

前面说过,GOCACHEPROG是Go 1.24引入的新实验特性(很大可能在Go 1.25版本转正),允许使用外部程序实现Go构建缓存。其间的通信协议基于JSON消息通过stdin/stdout进行交换。Go命令将GOCACHEPROG指定的程序(以下称为my-cache-prog)以child process的形式启动,之后my-cache-prog与go命令之间的通信过程大致如下:

  • 初始化: my-cache-prog启动后立即发送一个包含自身支持命令的Response消息(也称为init response,对应的ID=0)给Go命令。
  • 请求-响应模型: Go命令收到init response后,根据其支持的命令,发送Request,缓存程序my-cache-prog收到请求后进行处理,并回复Response

目前协议支持的命令类型包括如下三种:

  • put: 将对象存储到缓存中。
  • get: 从缓存中检索对象。
  • close: 请求缓存程序优雅退出。

显然,通过KnownCommands机制,Go命令可以支持未来协议的扩展。

文档中还给出了协议请求响应模型中Request和Response的定义,这个我们在Go命令的实现中也能找到:

// $GOROOT/src/cmd/go/internal/cacheprog/cacheprog.go

// Cmd is a command that can be issued to a child process.
//
// If the interface needs to grow, the go command can add new commands or new
// versioned commands like "get2" in the future. The initial [Response] from
// the child process indicates which commands it supports.
type Cmd string

const (
    // CmdPut tells the cache program to store an object in the cache.
    //
    // [Request.ActionID] is the cache key of this object. The cache should
    // store [Request.OutputID] and [Request.Body] under this key for a
    // later "get" request. It must also store the Body in a file in the local
    // file system and return the path to that file in [Response.DiskPath],
    // which must exist at least until a "close" request.
    CmdPut = Cmd("put")

    // CmdGet tells the cache program to retrieve an object from the cache.
    //
    // [Request.ActionID] specifies the key of the object to get. If the
    // cache does not contain this object, it should set [Response.Miss] to
    // true. Otherwise, it should populate the fields of [Response],
    // including setting [Response.OutputID] to the OutputID of the original
    // "put" request and [Response.DiskPath] to the path of a local file
    // containing the Body of the original "put" request. That file must
    // continue to exist at least until a "close" request.
    CmdGet = Cmd("get")

    // CmdClose requests that the cache program exit gracefully.
    //
    // The cache program should reply to this request and then exit
    // (thus closing its stdout).
    CmdClose = Cmd("close")
)

// Request is the JSON-encoded message that's sent from the go command to
// the GOCACHEPROG child process over stdin. Each JSON object is on its own
// line. A ProgRequest of Type "put" with BodySize > 0 will be followed by a
// line containing a base64-encoded JSON string literal of the body.
type Request struct {
    // ID is a unique number per process across all requests.
    // It must be echoed in the Response from the child.
    ID int64

    // Command is the type of request.
    // The go command will only send commands that were declared
    // as supported by the child.
    Command Cmd

    // ActionID is the cache key for "put" and "get" requests.
    ActionID []byte `json:",omitempty"` // or nil if not used

    // OutputID is stored with the body for "put" requests.
    //
    // Prior to Go 1.24, when GOCACHEPROG was still an experiment, this was
    // accidentally named ObjectID. It was renamed to OutputID in Go 1.24.
    OutputID []byte `json:",omitempty"` // or nil if not used

    // Body is the body for "put" requests. It's sent after the JSON object
    // as a base64-encoded JSON string when BodySize is non-zero.
    // It's sent as a separate JSON value instead of being a struct field
    // send in this JSON object so large values can be streamed in both directions.
    // The base64 string body of a Request will always be written
    // immediately after the JSON object and a newline.
    Body io.Reader `json:"-"`

    // BodySize is the number of bytes of Body. If zero, the body isn't written.
    BodySize int64 `json:",omitempty"`

    // ObjectID is the accidental spelling of OutputID that was used prior to Go
    // 1.24.
    //
    // Deprecated: use OutputID. This field is only populated temporarily for
    // backwards compatibility with Go 1.23 and earlier when
    // GOEXPERIMENT=gocacheprog is set. It will be removed in Go 1.25.
    ObjectID []byte `json:",omitempty"`
}

// Response is the JSON response from the child process to the go command.
//
// With the exception of the first protocol message that the child writes to its
// stdout with ID==0 and KnownCommands populated, these are only sent in
// response to a Request from the go command.
//
// Responses can be sent in any order. The ID must match the request they're
// replying to.
type Response struct {
    ID  int64  // that corresponds to Request; they can be answered out of order
    Err string `json:",omitempty"` // if non-empty, the error

    // KnownCommands is included in the first message that cache helper program
    // writes to stdout on startup (with ID==0). It includes the
    // Request.Command types that are supported by the program.
    //
    // This lets the go command extend the protocol gracefully over time (adding
    // "get2", etc), or fail gracefully when needed. It also lets the go command
    // verify the program wants to be a cache helper.
    KnownCommands []Cmd `json:",omitempty"`

    // For "get" requests.

    Miss     bool       `json:",omitempty"` // cache miss
    OutputID []byte     `json:",omitempty"` // the ObjectID stored with the body
    Size     int64      `json:",omitempty"` // body size in bytes
    Time     *time.Time `json:",omitempty"` // when the object was put in the cache (optional; used for cache expiration)

    // For "get" and "put" requests.

    // DiskPath is the absolute path on disk of the body corresponding to a
    // "get" (on cache hit) or "put" request's ActionID.
    DiskPath string `json:",omitempty"`
}

Request是由Go命令发送的请求,它包含的几个字段的含义如下:

  • ID: 每个进程中所有请求的唯一编号
  • Command: 请求类型(put/get/close)
  • ActionID: 缓存键
  • OutputID: 存储在缓存中的对象ID,实际也是Body数据的Sha256的值。
  • Body: “put”请求的主体数据,”get”和”close”请求没有Body。
  • BodySize: Body的字节数

Response则是由缓存程序回复给Go命令的结构,它的定义中的几个字段的含义如下:

  • ID: 对应请求的ID
  • Err: 错误信息(如有)
  • KnownCommands: 支持的命令列表(用于初始Response)
  • Miss: 缓存未命中标志
  • OutputID: 存储在缓存中的对象ID
  • Size: 主体大小(字节)
  • Time: 对象放入缓存的时间
  • DiskPath: 对应缓存项在磁盘上的绝对路径

这里要注意几点:

  • 除了init Response,其他Response可以乱序返回,Go命令会通过Response中的ID来匹配对应的Request。
  • 不论缓存数据存储在哪里,最终提供给Go命令的都应该在本地文件系统中,并通过Response中的DiskPath来指示该数据对应的绝对路径。

为了能更好地理解这个协议的交互,我这里画了一幅Go命令与my-cache-prog之间的交互示意图:

到这里,还有一个地方尚未清楚,那就是put请求与put/get请求之间以及put请求内部body的编码格式并未说清楚。在文档中,这部分也不是那么清晰,但这却决定了后续实现的正确性。为了给后面的实现做好铺垫,我们可以通过查看Go命令的对put请求的编码实现来确认这部分内容。在

// $GOROOT/src/cmd/go/internal/cache/prog.go

func (c *ProgCache) writeToChild(req *cacheprog.Request, resc chan<- *cacheprog.Response) (err error) {
    c.mu.Lock()
    if c.inFlight == nil {
        return errCacheprogClosed
    }
    c.nextID++
    req.ID = c.nextID
    c.inFlight[req.ID] = resc
    c.mu.Unlock()

    defer func() {
        if err != nil {
            c.mu.Lock()
            if c.inFlight != nil {
                delete(c.inFlight, req.ID)
            }
            c.mu.Unlock()
        }
    }()

    c.writeMu.Lock()
    defer c.writeMu.Unlock()

    if err := c.jenc.Encode(req); err != nil {
        return err
    }
    if err := c.bw.WriteByte('\n'); err != nil {
        return err
    }
    if req.Body != nil && req.BodySize > 0 {
        if err := c.bw.WriteByte('"'); err != nil {
            return err
        }
        e := base64.NewEncoder(base64.StdEncoding, c.bw)
        wrote, err := io.Copy(e, req.Body)
        if err != nil {
            return err
        }
        if err := e.Close(); err != nil {
            return nil
        }
        if wrote != req.BodySize {
            return fmt.Errorf("short write writing body to GOCACHEPROG for action %x, output %x: wrote %v; expected %v",
                req.ActionID, req.OutputID, wrote, req.BodySize)
        }
        if _, err := c.bw.WriteString("\"\n"); err != nil {
            return err
        }
    }
    if err := c.bw.Flush(); err != nil {
        return err
    }
    return nil
}

通过上述代码,我们可以总结出下面put请求的编码格式:

解释一下这张图。

  • 顶部(蓝色区域): JSON编码的请求元数据

包含ID、ActionID、OutputID和BodySize等字段。这部分使用标准JSON格式。

  • 中间(黄色条): 换行符分隔符(‘\n’)

JSON元数据后的第一个换行符。

  • 中部(绿色区域): Base64编码的请求体(可选)

这部分以双引号(“)开始,紧接着是Base64编码的二进制数据,最后以双引号(“)结束。

  • 底部(黄色条): 最终换行符(‘\n’)

整个请求的结束标记。

总的来说,Go命令的put请求使用了JSON+Base64的组合编码方式:请求的元数据以JSON格式编码,请求体以Base64编码(base64编码前后各有一个双引号),它们之间用换行符分隔,整个请求最后以换行符结束。这种格式便于解析,同时也能处理二进制数据。

注意:根据json.Encoder.Encode的文档,编码后的json文本也会跟着一个换行符(newline)。

不过代码中还有一点非常值得注意,那就是Put请求的BodySize的值为base64编码之前的Body长度!这一点如果不看源码,很容易使用BodySize去读取Body体的内容,从而导致解码出错!

好了,详细了解了上述协议后,我们就来尝试实现一个my-cache-prog程序。程序开源到github.com/bigwhite/go-cache-prog项目中了,大家可以结合项目代码来继续阅读下面的内容。

3. 实现

3.1 整体设计

go-cache-prog的实现采用了模块化设计,将不同的功能划分到独立的包中,以提高代码的可维护性和可扩展性。整体结构如下:

go-cache-prog/
├── cmd/
│   └── go-cache-prog/
│       └── main.go      (可执行程序入口)
├── protocol/
│   └── protocol.go  (请求/响应定义和解析)
├── storage/
│   ├── storage.go   (存储后端接口)
│   └── filesystem/
│       └── filesystem.go (基于本地文件系统的存储实现)
└── cache/
    └── cache.go     (内存缓存逻辑)
  • cmd/go-cache-prog/main.go: 这是可执行程序的入口点。

它负责解析命令行参数、设置日志输出、确定缓存目录、初始化存储和缓存、发送初始能力响应、启动请求处理循环。

// cmd/go-cache-prog/main.go  (部分)
func main() {
    // ... (参数解析、日志设置、缓存目录确定) ...

    store, err := filesystem.NewFileSystemStorage(cacheDir, verbose)
    if err != nil {
        log.Fatalf("Failed to initialize filesystem storage: %v", err)
    }
    cacheInstance := cache.NewCache(store)

    // ... (发送初始响应) ...
    requestHandler := protocol.NewRequestHandler(reader, os.Stdout, cacheInstance, verbose)

    if err := requestHandler.HandleRequests(); err != nil {
        log.Fatalf("Error handling requests: %v", err)
    }
}
  • protocol: 此包处理与go命令的通信协议,定义请求/响应结构,处理请求。
// protocol/protocol.go (部分)
type RequestHandler struct {
    reader        *bufio.Reader
    writer        io.Writer
    cache         *cache.Cache
    verbose       bool
    gets          int //statistics
    getMiss       int
}

func (rh *RequestHandler) HandleRequests() error {
    for {
        req, err := rh.readRequest()
        // ... (错误处理、请求处理) ...
    }
}
  • storage: 此包定义了存储后端的抽象接口。
// storage/storage.go
type Storage interface {
    Put(actionID, outputID []byte, data []byte, size int64) (string, error)
    Get(actionID []byte) (outputID []byte, size int64, modTime time.Time, diskPath string, found bool, err error)
    // ... (可选方法) ...
}
  • storage/filesystem: 此包提供了storage.Storage接口的一个具体实现,使用本地文件系统。
// storage/filesystem/filesystem.go (部分)
type FileSystemStorage struct {
    baseDir string
    verbose bool
}

func NewFileSystemStorage(baseDir string, verbose bool) (*FileSystemStorage, error) {
    // ... (创建目录) ...
}
  • cache: 此包实现了内存缓存层, 位于存储接口之上。
// cache/cache.go (部分)
type Cache struct {
    entries map[string]CacheEntry
    mu      sync.RWMutex
    store   storage.Storage
}

func NewCache(store storage.Storage) *Cache {
    // ... (初始化 map) ...
}

3.2 协议解析

protocol包负责处理go-cache-prog与go命令之间的基于JSON的通信协议。

  • 请求 (Request):
// protocol/protocol.go
type Request struct {
    ID       int64
    Command  Cmd
    ActionID []byte `json:",omitempty"`
    OutputID []byte `json:",omitempty"`
    Body     io.Reader `json:"-"`
    BodySize int64   `json:",omitempty"`
    ObjectID []byte `json:",omitempty"` // Deprecated
}
  • 响应 (Response):
// protocol/protocol.go
type Response struct {
    ID            int64      `json:",omitempty"`
    Err           string     `json:",omitempty"`
    KnownCommands []Cmd      `json:",omitempty"`
    Miss          bool       `json:",omitempty"`
    OutputID      []byte     `json:",omitempty"`
    Size          int64      `json:",omitempty"`
    Time          *time.Time `json:",omitempty"`
    DiskPath      string     `json:",omitempty"`
}

RequestHandler的readRequest方法负责读取和解析请求:

// protocol/protocol.go (部分)
func (rh *RequestHandler) readRequest() (*Request, error) {
    line, err := rh.reader.ReadBytes('\n')
    if err != nil {
        return nil, err
    }
    // ... (处理空行) ...
    var req Request
    if err := json.Unmarshal(line, &req); err != nil {
        // 检查base64
        if len(line) >= 2 && line[0] == '"' && line[len(line)-1] == '"'{
            // ...
        }
        return nil, fmt.Errorf("failed to unmarshal request: %w", err)
    }
    return &req, nil
}

对于put请求, 如果BodySize大于0, 需要读取并解码Base64数据:

// protocol/protocol.go (部分)
func (rh *RequestHandler) handlePut(req *Request) {
    var bodyData []byte
    if req.BodySize > 0 {
        bodyLine, err := rh.reader.ReadBytes('\n')
        // ... (跳过空行)...
        bodyLine, err = rh.reader.ReadBytes('\n')
        // ... (错误处理) ...

        bodyLine = bytes.TrimSpace(bodyLine)
        if len(bodyLine) < 2 || bodyLine[0] != '"' || bodyLine[len(bodyLine)-1] != '"' {
            // ... (格式错误) ...
        }
        base64Body := bodyLine[1 : len(bodyLine)-1]
        bodyData, err = base64.StdEncoding.DecodeString(string(base64Body))
        // ... (解码错误、大小不匹配处理) ...
    }
    // ... (调用 cache.Put) ...
}

3.3 缓存管理

cache包实现了内存缓存层,减少对底层存储的访问。

  • CacheEntry结构体:
// cache/cache.go
type CacheEntry struct {
    OutputID []byte
    Size     int64
    Time     time.Time
    DiskPath string
}
  • Cache结构体和NewCache:
// cache/cache.go
type Cache struct {
    entries map[string]CacheEntry
    mu      sync.RWMutex
    store   storage.Storage
}

func NewCache(store storage.Storage) *Cache {
    return &Cache{
        entries: make(map[string]CacheEntry),
        store:   store,
    }
}
  • Put方法:
// cache/cache.go
func (c *Cache) Put(actionID, outputID []byte, data []byte, size int64) (string, error) {
    diskPath, err := c.store.Put(actionID, outputID, data, size)
    if err != nil {
        return "", err
    }

    entry := CacheEntry{ /* ... */ }

    actionIDHex := fmt.Sprintf("%x", actionID)
    c.mu.Lock()
    c.entries[actionIDHex] = entry
    c.mu.Unlock()

    return diskPath, nil
}
  • Get方法:
// cache/cache.go
func (c *Cache) Get(actionID []byte) (*CacheEntry, bool, error) {
    actionIDHex := fmt.Sprintf("%x", actionID)

    c.mu.RLock()
    entry, exists := c.entries[actionIDHex]
    c.mu.RUnlock()

    if exists {
        return &entry, true, nil // 优先从内存缓存读取
    }

    // ... (从存储中读取, 并更新内存缓存) ...
}

3.4 抽象存储接口与本地文件系统实现

storage.Storage接口定义了存储后端的抽象,目的是为了支持更多的实现扩展,比如支持在S3上存储等。

// storage/storage.go
type Storage interface {
    Put(actionID, outputID []byte, data []byte, size int64) (string, error)
    Get(actionID []byte) (outputID []byte, size int64, modTime time.Time, diskPath string, found bool, err error)
}

storage/filesystem包提供了一种基于本地文件系统的实现。

  • FileSystemStorage和NewFileSystemStorage:
// storage/filesystem/filesystem.go
type FileSystemStorage struct {
    baseDir string
    verbose bool
}

func NewFileSystemStorage(baseDir string, verbose bool) (*FileSystemStorage, error) {
    if err := os.MkdirAll(baseDir, 0755); err != nil {
        return nil, err
    }
    return &FileSystemStorage{baseDir: baseDir, verbose: verbose}, nil
}
  • Put方法:
// storage/filesystem/filesystem.go
func (fss *FileSystemStorage) Put(actionID, outputID []byte, data []byte, size int64) (string, error) {
    actionIDHex := fmt.Sprintf("%x", actionID)
    //outputIDHex := fmt.Sprintf("%x", outputID) //Might not need

    actionFile := filepath.Join(fss.baseDir, fmt.Sprintf("a-%s", actionIDHex))
    diskPath := filepath.Join(fss.baseDir, fmt.Sprintf("o-%s", actionIDHex))
    absPath, _ := filepath.Abs(diskPath)

    // Write metadata
    now := time.Now()
    ie, err := json.Marshal(indexEntry{
        Version:  1,
        OutputID: outputID,
        Size:     size,
        Time:     &now,
    })
    // ... (错误处理, 写入元数据文件) ...
    if size > 0{
        // 写入数据文件
        if err := os.WriteFile(diskPath, data, 0644); err != nil {
            return "", fmt.Errorf("failed to write cache file: %w", err)
        }
    } else {
        //创建空文件
        zf, err := os.OpenFile(diskPath, os.O_CREATE|os.O_RDWR, 0644)
        if err != nil {
            return "", fmt.Errorf("failed to create empty file: %w", err)
        }
        zf.Close()
    }

    return absPath, nil
}
  • Get方法:
// storage/filesystem/filesystem.go
func (fss *FileSystemStorage) Get(actionID []byte) (outputID []byte, size int64, modTime time.Time, diskPath string, found bool, err error) {
    actionIDHex := fmt.Sprintf("%x", actionID)
    actionFile := filepath.Join(fss.baseDir, fmt.Sprintf("a-%s", actionIDHex))

    // Read metadata
    af, err := os.ReadFile(actionFile)
    // ... (文件不存在处理) ...
    var ie indexEntry
    if err := json.Unmarshal(af, &ie); err != nil {
        return nil, 0, time.Time{}, "", false, fmt.Errorf("failed to unmarshal index entry: %w", err)
    }

    objectFile := filepath.Join(fss.baseDir, fmt.Sprintf("o-%s", actionIDHex))
    info, err := os.Stat(objectFile)
    // ... (对象文件不存在、或其他错误处理) ...
    diskPath, _ = filepath.Abs(objectFile)

    return ie.OutputID, info.Size(), info.ModTime(), diskPath, true, nil
}

storage/filesystem使用了两种类型的文件来分别存储缓存数据和元数据:

  • a-{actionID} (Action File): 元数据文件

这个文件存储了关于缓存条目的元数据,使用JSON格式。actionID是缓存键的十六进制表示。

  • o-{actionID} (Object File): 对象文件。

这个文件存储了实际的缓存数据(即Request.Body的内容)。actionID 与对应的元数据文件中的actionID 相同。

对于一些Put请求(with BodySize=0)的,同样会创建元数据文件和对象文件,只是对象文件的size为0。

这么设计便于快速查找:在执行Get操作时,go-cache-prog首先读取a-{actionID}文件。这个文件很小,因为它只包含元数据。通过读取这个文件,go-cache-prog可以快速确定:缓存条目是否存在(如果 a-{actionID} 文件不存在,则肯定不存在)。 如果存在,可以获取到OutputID、数据大小(Size)和最后修改时间(Time),并放入内存缓存中,而无需读取可能很大的o-{actionID}文件,便可以知道对象文件(o-{actionID})是否存在。

4. 验证

下载go-cache-prog源码并编译:

$git clone https://github.com/bigwhite/go-cache-prog.git
$make

注意:go-cache-prog需要与Go 1.24及以上版本配合使用。

接下来,我们将fmt包首次编译安装到go-cache-prog的默认缓存目录下(~/.gocacheprog):

$GOCACHEPROG="./go-cache-prog --verbose" go install fmt
2025/03/04 10:47:59 Using cache directory: /Users/tonybai/.gocacheprog
2025/03/04 10:47:59 Received request: ID=1, Command=get, ActionID=90c776cb58a3c3a99b5622344df5bc959fd2b90f299b40ae21ec6ccf16c77a23, OutputID=, BodySize=0
2025/03/04 10:47:59 Received request: ID=2, Command=put, ActionID=90c776cb58a3c3a99b5622344df5bc959fd2b90f299b40ae21ec6ccf16c77a23, OutputID=4e67091862cdc5ff3d44d51adaf9f5a3f5e993dcbc0b6aad884d00d929f3f4d3, BodySize=3037
2025/03/04 10:47:59 Put request: ID=2, Actual BodyLen=4055
2025/03/04 10:47:59 Received request: ID=3, Command=get, ActionID=b2d3027bda366ae198f991d65f62b5be25aa7fe41092bb81218ba24363923b69, OutputID=, BodySize=0
2025/03/04 10:47:59 Received request: ID=4, Command=get, ActionID=c48dafcc394ccfed5c334ef2e21ba8b5bd09a883956f17601cf8a3123f8afd2b, OutputID=, BodySize=0
2025/03/04 10:47:59 Received request: ID=5, Command=get, ActionID=b16400d94b83897b0e7a54ee4223208ff85b4926808bcae66e488d2dbab85054, OutputID=, BodySize=0
2025/03/04 10:47:59 Received request: ID=6, Command=get, ActionID=789f5b8e5b2390e56d26ac916b6f082bfb3e807ee34302f8aa0310e6e225ac77, OutputID=, BodySize=0

... ...
2025/03/04 10:48:03 Received request: ID=321, Command=close, ActionID=, OutputID=, BodySize=0
2025/03/04 10:48:03 Gets: 107, GetMiss: 107

由于初始情况下,默认缓存目录下(~/.gocacheprog)没有构建缓存的文件,因此上面的所有get都miss了,go命令会发送put请求,go-cache-prog会构建初始cache。在默认缓存目录下(~/.gocacheprog)下,我们可以看到类似这样的文件列表:

$ls ~/.gocacheprog
a-01fae6e8773991089b07eef70a209ee3e99e229231b4956689d7c914a84c70de
a-030b82281d0fae81d44e96b140c276fa232abe46ae92b7fe1d4b7213bc58eef1
a-046d1381c7f1061967c50c5ba2a112486374c6682e80b154f26f17302eb623a4
... ...
o-fc0a0cf26b5a438834ee47a7166286bfb4266c93b667a66e5630502db7651507
o-fc5364bf6b2b714e6a90e8b57652827666b93366f0e322875eefd21b4cc58b3f
o-fde27b35692f9efeae945f00ab029fe156cbfa961bf6149ab9767e1efd057545
o-ff141dd2b1c95d4cba6c3cda5792d8863e428824565ecb5765018710199a2f69

接下来,我们再次执行同样的命令,看看cache是否起到了作用:

$GOCACHEPROG="./go-cache-prog --verbose" go install fmt
2025/03/04 10:50:14 Using cache directory: /Users/tonybai/.gocacheprog
2025/03/04 10:50:14 Received request: ID=1, Command=get, ActionID=90c776cb58a3c3a99b5622344df5bc959fd2b90f299b40ae21ec6ccf16c77a23, OutputID=, BodySize=0
2025/03/04 10:50:14 Received request: ID=2, Command=get, ActionID=c48dafcc394ccfed5c334ef2e21ba8b5bd09a883956f17601cf8a3123f8afd2b, OutputID=, BodySize=0
2025/03/04 10:50:14 Received request: ID=3, Command=get, ActionID=b16400d94b83897b0e7a54ee4223208ff85b4926808bcae66e488d2dbab85054, OutputID=, BodySize=0
2025/03/04 10:50:14 Received request: ID=4, Command=get, ActionID=789f5b8e5b2390e56d26ac916b6f082bfb3e807ee34302f8aa0310e6e225ac77, OutputID=, BodySize=0
2025/03/04 10:50:14 Received request: ID=5, Command=get, ActionID=c6e6427a15f95d70621df48cc68ab039075d66c1087427eb9a04bcf729c5b491, OutputID=, BodySize=0
... ...
2025/03/04 10:50:14 Received request: ID=161, Command=close, ActionID=, OutputID=, BodySize=0
2025/03/04 10:50:14 Gets: 160, GetMiss: 0

我们看到所有的Get请求都命中了缓存(GetMiss: 0),此次执行也肉眼可见的快!

我们再来用一个可执行程序验证一下利用build cache的构建。在go-cache-prog项目下有一个examples/helloworld示例,在该目录下执行make,我们就能看到构建的输出:

$cd examples/helloworld
$make
GOCACHEPROG="../../go-cache-prog --verbose" go build
2025/03/04 10:54:35 Using cache directory: /Users/tonybai/.gocacheprog
2025/03/04 10:54:35 Received request: ID=1, Command=get, ActionID=7c1950a92d55fae91254e8923f7ea4cdfd2ce34953bcf2348ba851be3e2402a1, OutputID=, BodySize=0
2025/03/04 10:54:35 Received request: ID=2, Command=put, ActionID=7c1950a92d55fae91254e8923f7ea4cdfd2ce34953bcf2348ba851be3e2402a1, OutputID=43b1c1a308784cd610fda967d781d3c5ccfd4950263df98d18a2ddb2dd218f5a, BodySize=251
2025/03/04 10:54:35 Put request: ID=2, Actual BodyLen=339
2025/03/04 10:54:35 Received request: ID=3, Command=get, ActionID=90c776cb58a3c3a99b5622344df5bc959fd2b90f299b40ae21ec6ccf16c77a23, OutputID=, BodySize=0

... ...
2025/03/04 10:54:35 Received request: ID=165, Command=close, ActionID=, OutputID=, BodySize=0
2025/03/04 10:54:35 Gets: 163, GetMiss: 1

我们看到绝大部分都是命中缓存的。

执行构建出的helloworld,程序也会正常输出内容:

$./helloworld
hello, world!

5. 小结

本文深入探讨了Go 1.24引入的GOCACHEPROG这一实验性特性,它为Go构建缓存带来了前所未有的灵活性。通过允许开发者使用自定义程序来管理构建缓存,GOCACHEPROG解决了Go内置缓存机制在特定场景下的局限性,特别是CI环境和团队协作中的痛点。

文中,我们基于对协议的理解,逐步构建了一个名为go-cache-prog的自定义缓存程序。go-cache-prog采用了模块化设计,将协议解析、缓存管理和存储抽象分离到不同的包中,提高了代码的可维护性和可扩展性。

最后,我们通过实际的编译和安装示例,验证了go-cache-prog的功能,展示了它如何与Go命令协同工作,实现自定义的构建缓存管理。

go-cache-prog项目提供了一个坚实的基础,开发者可以在此基础上进行扩展,实现更高级的功能,例如:

  • 不同的存储后端:实现storage.Storage接口,支持将缓存数据存储到云存储(如 AWS S3、Google Cloud Storage)、分布式缓存(如 Redis、Memcached)或其他存储系统中。
  • 缓存失效策略:实现更复杂的缓存失效策略,例如基于 LRU(最近最少使用)或 TTL(生存时间)的过期机制。
  • 分布式缓存:构建一个分布式的缓存系统,支持在多个开发机器或 CI 节点之间共享构建缓存。
  • 监控和统计:添加监控和统计功能,跟踪缓存命中率、缓存大小、性能指标等。

此外,目前的go-cache-prog是顺序处理go命令的请求的,大家也可以自行将其改造为并发处理请求,不过务必注意并发处理的同步。


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语言第一课 Go语言进阶课 AI原生开发工作流实战 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