<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>公共依赖 on Tony Bai</title><link>https://tonybai.com/tags/%E5%85%AC%E5%85%B1%E4%BE%9D%E8%B5%96/</link><description>Recent content in 公共依赖 on Tony Bai</description><generator>Hugo</generator><language>zh-cn</language><copyright>2004-2026 Tony Bai. 版权所有.</copyright><lastBuildDate>Sun, 05 Jul 2026 07:00:00 +0800</lastBuildDate><atom:link href="https://tonybai.com/tags/%E5%85%AC%E5%85%B1%E4%BE%9D%E8%B5%96/index.xml" rel="self" type="application/rss+xml"/><item><title>五年，三篇文章，一个我一直没真正解决的问题</title><link>https://tonybai.com/2026/07/05/go-private-modules-lessons-learned/</link><pubDate>Sun, 05 Jul 2026 07:00:00 +0800</pubDate><guid>https://tonybai.com/2026/07/05/go-private-modules-lessons-learned/</guid><description>大约5年前，我写过两篇关于“如何在小厂内部搭建私有 Go module 拉取方案”的文章。这套方案在公司内部用了五年，基本没出过大问题。直到今年，我们第一次把 Go 私有库以白盒方式交付给客户，才发现真正的麻烦从来都不在“内部”——而在于代码一旦离开你的组织边界，所有关于私有 module 的假设都会失效。这篇文章记录了这五年的认知升级，以及为什么我最终把这套认知做成了一个名为gvu的命令行工具。</description><content:encoded><![CDATA[<p><img alt="题图" loading="lazy" src="/images/wp-content/uploads/2026/go-private-modules-lessons-learned-1.png"></p>
<p><a href="https://tonybai.com/2026/07/05/go-private-modules-lessons-learned">本文永久链接</a> – <a href="https://tonybai.com/2026/07/05/go-private-modules-lessons-learned">https://tonybai.com/2026/07/05/go-private-modules-lessons-learned</a></p>
<p>大家好，我是Tony Bai。</p>
<p>五年前，我写过两篇文章，讲的是怎么在公司内部搭一套私有 Go module 拉取方案。三年前，我又补了第三篇，说这套东西“跑了两年，基本没什么大问题”。</p>
<p>三篇文章，五年时间，我一直以为这件事已经被我解决了。</p>
<p>直到上个月，我们把一个 Go 私有库以白盒方式交付给一家客户。第二天一早，对方工程师就甩来一段报错:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">$</span> <span style="color:#66d9ef">go</span> <span style="color:#a6e22e">mod</span> <span style="color:#a6e22e">tidy</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">go</span>: <span style="color:#a6e22e">finding</span> <span style="color:#a6e22e">module</span> <span style="color:#66d9ef">for</span> <span style="color:#f92672">package</span> <span style="color:#a6e22e">mycompany</span>.<span style="color:#a6e22e">com</span><span style="color:#f92672">/</span><span style="color:#66d9ef">go</span><span style="color:#f92672">/</span><span style="color:#a6e22e">common</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">go</span>: <span style="color:#a6e22e">mycompany</span>.<span style="color:#a6e22e">com</span><span style="color:#f92672">/</span><span style="color:#66d9ef">go</span><span style="color:#f92672">/</span><span style="color:#a6e22e">common</span>: <span style="color:#a6e22e">unrecognized</span> <span style="color:#f92672">import</span> <span style="color:#a6e22e">path</span> <span style="color:#e6db74">&#34;mycompany.com/go/common&#34;</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">https</span> <span style="color:#a6e22e">fetch</span>: <span style="color:#a6e22e">Get</span> <span style="color:#e6db74">&#34;https://mycompany.com/go/common?go-get=1&#34;</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">dial</span> <span style="color:#a6e22e">tcp</span>: <span style="color:#a6e22e">lookup</span> <span style="color:#a6e22e">mycompany</span>.<span style="color:#a6e22e">com</span>: <span style="color:#a6e22e">no</span> <span style="color:#a6e22e">such</span> <span style="color:#a6e22e">host</span>
</span></span></code></pre></div><p>我盯着这段报错愣了几秒，随即哑然失笑——这不就是五年前我们内部第一次遇到的那个问题吗？只不过这一次，“内部”变成了别人的“内部”。</p>
<p>五年里，我一直在同一个边界<em>内部</em>不断打磨这套方案，却从没想过，如果有一天代码要<em>跨出</em>这个边界呢?</p>
<p>客户的开发机、CI 机器，统统访问不到我们组织内部的私有 GOPROXY 服务器，也访问不到我们内部的 VCS。代码本身没有任何问题，问题出在一个我们几乎从未认真思考过的地方：这个 Go module 的 import path 本身，是一个只在我们自己的网络边界里才成立的“承诺”。</p>
<p><img loading="lazy" src="/images/wp-content/uploads/2025/paid/the-ultimate-guide-to-go-module-qr.png"></p>
<h2 id="五年前这个问题第一次找上我">五年前，这个问题第一次找上我</h2>
<p>这不是我第一次跟这类问题交手。</p>
<p>2021 年，我写过一篇《<a href="https://tonybai.com/2021/09/03/the-approach-to-go-get-private-go-module-in-house">小厂内部私有Go module拉取方案</a>》。当时的背景很简单：随着公司里 Go 项目变多，&ldquo;代码重复&quot;问题冒了出来，我们把公共代码抽成了内部私有仓库，于是就有了&quot;如何让 <code>go get</code> 拉到私有Go代码库&quot;这个需求。</p>
<p>大厂的答案很简单——因为大厂内部的 VCS 天然就挂在一个内部域名下（比如 <code>git.company.com</code>），import path 直接指向真实仓库地址即可，<code>go get</code> 该怎么工作就怎么工作。</p>
<p>但小厂或个人开发者通常没有这个条件：我们希望私有 module 的 import path 是干净的、带有自己域名语义的（比如 <code>mycompany.com/go/common</code>），但这个域名背后并没有真的托管代码，代码实际存放在内部的 Git 服务器上。这中间，需要一层&quot;翻译&rdquo;。</p>
<p>我们当时选择的方案是 <code>govanityurls</code> + Nginx + 内部 GOPROXY 的组合：<code>govanityurls</code> 负责把 <code>mycompany.com/go/common</code> 这样的 vanity import path 翻译成真实的仓库地址，内部 GOPROXY 负责统一代理公共 module 和私有 module 的拉取。开发者只需要把 <code>GOPROXY</code> 指向内部代理，剩下的事情对他们透明。</p>
<p>这套方案上线之后，确实解决了当时的问题。</p>
<h2 id="方案在演进但边界的假设没有变">方案在演进，但边界的假设没有变</h2>
<p>2022 年，业务线决定把代码托管从 Gerrit 迁移到 GitLab，我又写了篇《<a href="https://tonybai.com/2022/06/18/the-approach-to-go-get-private-go-module-in-house-part2">小厂内部私有Go module拉取方案（续）</a>》，记录了适配过程中踩的坑——SSH 密钥交换算法不匹配、<code>vanity.yaml</code> 里仓库地址的调整，等等。本质上，这次只是把&quot;翻译层&quot;背后指向的仓库换了个地方，原理没有变。</p>
<p>2023 年，我又写了第三篇《<a href="https://tonybai.com/2023/03/03/the-approach-to-go-get-private-go-module-in-house-part3/">小厂内部私有Go module拉取方案3</a>》。那时候这套基础设施已经稳定运行了两年，&ldquo;总体感觉不错，没什么大问题&rdquo;。文章里我提到了一个一直没有优雅解决的小麻烦：每次新增一个用作私有依赖的仓库，<code>vanity.yml</code> 都得手动改一次，开发者对这种运维琐事天然是抗拒的。同一篇文章里，我还顺手研究了一下 <code>go mod replace</code> 能不能绕开 vanity 层直接指向内部仓库——最后发现 Go 官方早就支持了这种用法，只是没多少人知道。</p>
<p>回头看这三篇文章，我当时关注的都是&quot;怎么让方案更稳、更省心&quot;，但有一个前提我从来没有认真质疑过：</p>
<blockquote>
<p>所有这些方案，都默认&quot;拉代码的人&quot;和&quot;存代码的服务器&quot;处在同一个网络边界、同一个信任边界里。</p>
</blockquote>
<p>五年里，我们不断优化这个边界<em>内部</em>的体验，却从来没想过，如果有一天代码需要<em>跨出</em>这个边界呢？</p>
<h2 id="今年边界本身松动了">今年，边界本身松动了</h2>
<p>今年我们开始以白盒方式向客户交付 Go 私有库的源码。这是一种新的交付模式：不再是&quot;我们运营一个服务，客户调用接口&quot;，而是代码本身要跑到客户自己组织的机房里，由客户自己的工程师编译、构建和维护。</p>
<p>这一下子把五年来被小心维护的假设全部打破了：</p>
<ul>
<li>网络边界变了：客户访问不到我们内部的私有 GOPROXY，也访问不到我们内部的 Git 服务器。</li>
<li>信任边界变了：我们不可能把内部 VCS 的访问凭证交给客户，那意味着客户能拿到我们所有其他私有代码。</li>
<li>组织边界变了：<code>mycompany.com/go/common</code> 这样的 import path，在客户的网络里就是个死链接，<code>govanityurls</code> 部署在我们这边，客户那边根本连不到。</li>
</ul>
<p>于是我们不得不在客户组织内部，重新搭建一套简化版的私有 module 拉取方案——申请域名或者用内网 DNS 顶一下、配一遍 vanity 映射、协调 GOPROXY 或 GOPRIVATE、把仓库权限单独开一份给客户。整个过程和五年前第一次搭建时几乎一样繁琐，唯一的区别是，这次要在一个我们不熟悉、不受控的环境里，重新做一遍。</p>
<p>那一刻我意识到，这从来都不是一个&quot;公司内部基础设施&quot;问题，而是一个更普遍的问题：</p>
<blockquote>
<p><strong>只要代码需要跨越一个信任边界——从团队到团队，从公司到客户，从内网到公网——import path 和它背后真实仓库地址之间的这层&quot;翻译&quot;，就会重新变成一个需要人工搭建的脆弱环节。</strong></p>
</blockquote>
<p>这个问题不是 Go 独有的，几乎任何一种需要&quot;私有代码分发&quot;的语言生态都会遇到类似的处境。只是 Go 因为 <code>go get</code> 直接依赖 import path 做域名解析这个设计，把这个问题暴露得格外明显。</p>
<h2 id="命令行正在重新变成控制面">命令行，正在重新变成“控制面”</h2>
<p>过去这类&quot;边界重新搭建&quot;的问题，标准答案往往是造一个管理后台：一个 Web UI，让你去配置域名映射、TLS 证书、路由规则——本质上是把运维动作图形化。</p>
<p>但对开发者来说，这从来都不是最自然的心智模型。真正高频的运维动作，最后总会被收敛成一行命令：<code>gh repo create</code>、<code>wrangler deploy</code>、<code>supabase db push</code>——这几年你会发现，越来越多原本需要打开一个 Web 控制台才能完成的事情，正在被重新压缩进终端里的一行命令。命令行不再只是&quot;极客的偏好&quot;，它正在重新成为很多产品事实上的控制面，AI 时代尤甚。</p>
<p>这和我这五年反复折腾的事情，其实是同一个道理：<code>vanity.yml</code> 手动改一次、Nginx 配一次、GOPROXY 调一次——这些散落在不同工具、不同配置文件里的动作，本质上都应该是<strong>一个命令的事</strong>。不管这个&quot;边界&quot;是公司内部，还是客户组织内部。</p>
<h2 id="顺手把这五年的认知封装了一下">顺手把这五年的认知封装了一下</h2>
<p>所以这次白盒交付踩完坑之后，我索性把这五年反复重复的这套操作——vanity import path 的映射、私有 module 的拉取配置——抽象成了一个通用的命令行工具，取名 <strong>gvu</strong>（gomodvanityurls 的缩写）。它现在正在做公测，官网在 <a href="https://gomodvanityurls.com">gomodvanityurls.com</a>。</p>
<p>它解决的问题很朴素：不管你是要在公司内部搭一套私有 module 拉取方案，还是像我们这次一样要在客户组织内部临时重建一套，都不用再手写 <code>vanity.yml</code>、配 Nginx、协调 GOPROXY 这些琐事了，一行命令搞定映射和分发。</p>
<p>这篇不是产品说明书，就不展开细节了，后面我会单独写几篇上手向的文章。感兴趣的话可以先去<a href="https://gomodvanityurls.com">gvu官网</a>看看，花五分钟体验一下。</p>
<h2 id="这次go-build-一次成功">这次，<code>go build</code> 一次成功</h2>
<p>方案在客户组织内部重新搭好之后，我又收到了那位工程师的消息，这次贴的是：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-gdscript3" data-lang="gdscript3"><span style="display:flex;"><span><span style="color:#f92672">$</span> go mod tidy
</span></span><span style="display:flex;"><span>go: downloading mycompany<span style="color:#f92672">.</span>com<span style="color:#f92672">/</span>go<span style="color:#f92672">/</span>common v1<span style="color:#f92672">.</span><span style="color:#ae81ff">2.3</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">$</span> go build <span style="color:#f92672">./...</span>
</span></span></code></pre></div><p>没有报错，干干净净地结束。</p>
<p>五年前那篇文章的开头，我写过&quot;Go module 拉取公共依赖不再是痛点&quot;。五年后我想补一句：<strong>私有依赖的拉取，只要代码不跨越边界，也早就不是痛点了。真正的痛点，永远出现在边界被打破的那一刻。</strong></p>
<h2 id="小结">小结</h2>
<p>这五年，我们的方案从“能用”到“稳定”，看起来是在不断进步，但本质上一直是在同一个边界内部打磨细节。</p>
<p>今年白盒交付撞到的这堵墙，才让我第一次看清问题的全貌：Go 私有 module 的拉取困境，本质上是一个&quot;代码如何优雅地跨越信任边界&quot;的问题，而不是一个&quot;公司内部该怎么配置 GOPROXY&quot;的问题。</p>
<p>五年三篇文章，记录的都是同一个问题在不同场景下的重复出现。这一次，我把重复出现的部分做成了一个工具。</p>
<p>接下来，我会写几篇 <a href="https://gomodvanityurls.com">gvu</a> 的上手教程和一些进阶用法，感兴趣的朋友可以留意后续文章。</p>
<p>如果你也被&quot;私有 Go module 怎么优雅分发和拉去&quot;这个问题困扰过，欢迎在评论区聊聊你的方案，说不定又是一次有意思的讨论。</p>
<hr>
<p>还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 <strong>《<a href="http://gk.link/a/12IzL">从0 开始构建 Agent Harness</a>》</strong> 将带你：</p>
<ul>
<li>抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理</li>
<li>用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw</li>
<li>构建坚不可摧的 Safety Middleware 与飞书人工审批防线</li>
<li>在底层实现 Token 成本审计、链路追踪与自动化跑分评估</li>
<li>从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师”</li>
</ul>
<p>扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。</p>
<p><img loading="lazy" src="/images/wp-content/uploads/2026/build-agent-harness-from-scratch-qr.png"></p>
<hr>
<p><strong>原「Gopher部落」已重装升级为「Go &amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！</strong></p>
<p>我们致力于打造一个高品质的 <strong>Go 语言深度学习</strong> 与 <strong>AI 应用探索</strong> 平台。在这里，你将获得：</p>
<ul>
<li><strong>体系化 Go 核心进阶内容:</strong> 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。</li>
<li><strong>前沿 Go+AI 实战赋能:</strong> 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。</li>
<li><strong>星主 Tony Bai 亲自答疑:</strong> 遇到难题？星主第一时间为你深度解析，扫清学习障碍。</li>
<li><strong>高活跃 Gopher 交流圈:</strong> 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。</li>
<li><strong>独家资源与内容首发:</strong> 技术文章、课程更新、精选资源，第一时间触达。</li>
</ul>
<p>衷心希望「Go &amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！</p>
<p><img loading="lazy" src="http://image.tonybai.com/img/tonybai/gopher-and-ai-tribe-zsxq-small-card.jpg"></p>
<hr>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img loading="lazy" src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png"></p>
<!-- 
公众号地址：
-->
]]></content:encoded></item></channel></rss>