<?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>Subdirectory on Tony Bai</title><link>https://tonybai.com/tags/subdirectory/</link><description>Recent content in Subdirectory on Tony Bai</description><generator>Hugo</generator><language>zh-cn</language><copyright>2004-2026 Tony Bai. 版权所有.</copyright><lastBuildDate>Wed, 08 Jul 2026 06:30:00 +0800</lastBuildDate><atom:link href="https://tonybai.com/tags/subdirectory/index.xml" rel="self" type="application/rss+xml"/><item><title>从手动 govanityurls + Nginx 迁移到 gvu：一次真实迁移记录</title><link>https://tonybai.com/2026/07/08/migrate-from-govanityurls-to-gvu/</link><pubDate>Wed, 08 Jul 2026 06:30:00 +0800</pubDate><guid>https://tonybai.com/2026/07/08/migrate-from-govanityurls-to-gvu/</guid><description>对于已经在生产环境中运行多年 govanityurls + Nginx 或其他私有模块拉取方案的 Go 开发团队而言，如何低成本地将庞大的私有模块映射迁移至更现代的工具是一大痛点。本文详细记录了一次将历史遗留体系真实迁移至命令行工具 gvu 的全过程。文章首先澄清了开发者常犯的认知误区，明确区分了 Vanity URL 解析（gvu 负责）与模块内容拉取（GOPROXY 负责）的底层边界。随后，通过详尽的步骤复盘，展示了如何盘点存量 vanity.yml 映射，并通过 gvu 的杀手级功能——Wildcard Routing（通配符路由），将原本需要逐条维护的十几个仓库映射，极简压缩为一条规则，从而彻底解放了团队配置和重载 Nginx 的运维压力。最终对比表明，迁移至 gvu 不仅实现了映射管理的零运维，更利用自定义域名和优先级路由机制，完美兼顾了历史老库的平滑过渡与未来Go代码仓库的高效扩张。</description><content:encoded><![CDATA[<p><img alt="题图" loading="lazy" src="/images/wp-content/uploads/2026/migrate-from-govanityurls-to-gvu-1.png"></p>
<p><a href="https://tonybai.com/2026/07/08/migrate-from-govanityurls-to-gvu">本文永久链接</a> – <a href="https://tonybai.com/2026/07/08/migrate-from-govanityurls-to-gvu">https://tonybai.com/2026/07/08/migrate-from-govanityurls-to-gvu</a></p>
<p>大家好，我是Tony Bai。</p>
<p>上一篇《<a href="https://tonybai.com/2026/07/06/get-started-with-gvu-in-5-minutes">5 分钟上手 gvu</a>》带大家跑通了一个全新项目从 0 到 1 接入 gvu 的过程。</p>
<p>但对于已经在生产环境跑了好几年 <code>govanityurls</code> 或其他私有Go模块拉取方案的团队（比如五年前的我们自己），更现实的问题是：已有的这套方案，要不要迁、怎么迁。</p>
<p>这篇文章就是要把这件事儿讲清楚，并且给出我们团队真实迁移的完整记录。</p>
<p><img loading="lazy" src="/images/wp-content/uploads/2025/paid/the-ultimate-guide-to-go-module-qr.png"></p>
<h2 id="先把一件事说清楚goproxy-和-gvu-不是一回事">先把一件事说清楚：goproxy 和 gvu 不是一回事</h2>
<p>这是我在写这篇文章之前，觉得最有必要单独拎出来讲的一点，因为这个误解我自己五年前也有过。</p>
<p><code>go get</code>、<code>go mod tidy</code> 背后其实有两层完全不同的机制在起作用：</p>
<h3 id="第一层vanity-import-path-解析govanityurls--gvu-解决的问题">第一层：vanity import path 解析（<code>govanityurls</code> / gvu 解决的问题）</h3>
<p>当你执行 <code>go get mycompany.com/go/common</code> 时，Go 工具链第一步要做的是搞清楚这个 import path 背后到底是哪个仓库、用什么版本控制系统。</p>
<p>它会去请求 <code>https://mycompany.com/go/common?go-get=1</code>，期待拿到一个包含 <code>go-import</code> meta 标签的 HTML 页面，告诉它&quot;真正的仓库在这里&quot;。</p>
<p><code>govanityurls</code> 和 gvu 干的就是这一件事—— 提供这个&quot;翻译层&quot;，把好看的、语义化的 import path 映射到真实仓库地址。它本身不存代码，不存版本，不存访问私有仓库的凭据，只是一次 HTTP 重定向式的元信息响应。</p>
<h3 id="第二层module-内容的拉取与缓存goproxy-解决的问题">第二层：module 内容的拉取与缓存（GOPROXY 解决的问题）</h3>
<p>搞清楚真实仓库地址之后，Go 工具链还需要实际拉取这个 module 的具体版本内容（代码、<code>go.sum</code> 校验信息）。</p>
<p>这一层默认走的是 <code>proxy.golang.org</code>，也可以是你自己搭的私有代理（Athens、GoProxy.io 私有化版本等），负责缓存、加速、以及对私有仓库做鉴权拉取。</p>
<p>两者的关系是：</p>
<p>vanity URL 解析发生在前，GOPROXY 拉取发生在后，一前一后，缺一不可，但解决的是完全不同的问题。</p>
<p>迁移到 gvu 之后，你的私有 module 依然需要一个 GOPROXY（或者用 <code>GOPRIVATE</code>/<code>GONOSUMCHECK</code> 让 Go 工具链直接绕过公共代理走直连）——gvu 只是替你省掉了手动维护 vanity 映射这一层，它不会、也不打算替代你现有的 GOPROXY。</p>
<p>五年前那几篇文章里，我们的架构其实一直是&quot;<code>govanityurls</code>（第一层）+ 内部 GOPROXY（第二层）&ldquo;的组合，这次迁移改动的只是第一层。</p>
<h2 id="迁移前五年前的架构长什么样">迁移前：五年前的架构长什么样</h2>
<p>回顾一下五年积累下来的这套东西，大致是这样：</p>
<ul>
<li>一台机器上跑着 <code>govanityurls</code> 进程，读取一份手写的 <code>vanity.yml</code>，每新增一个私有仓库就要手动加一条映射</li>
<li>前面挂一层 Nginx，做 HTTPS 终止、反代到 <code>govanityurls</code> 的端口</li>
<li>TLS 证书自己申请、自己续期</li>
<li>内部 GOPROXY 单独部署，负责真正的 module 内容拉取和缓存</li>
</ul>
<p>这套东西五年里总体稳定，但&quot;新增一个仓库 = 改配置 + 重载 govanityurls&quot;这个流程，一直是团队里被吐槽最多的一环，也是这次迁移最想解决的核心痛点。</p>
<h2 id="迁移步骤">迁移步骤</h2>
<h3 id="第一步盘点现有的-vanityyml">第一步：盘点现有的 vanity.yml</h3>
<p>把所有存量映射列出来。我们当时 <code>vanity.yml</code> 大概是这样的结构（简化版）：</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-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">paths</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">go/common</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">repo</span>: <span style="color:#ae81ff">https://internal-git.mycompany.com/go/common</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">vcs</span>: <span style="color:#ae81ff">git</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">go/authsdk</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">repo</span>: <span style="color:#ae81ff">https://internal-git.mycompany.com/go/authsdk</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">vcs</span>: <span style="color:#ae81ff">git</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># ... 还有几十条</span>
</span></span></code></pre></div><p>这一步的目的很简单：确认清楚现存映射一共有多少条，后面要在 gvu 里一一重建，同时顺手看一眼这些仓库的命名规律——这会决定下一步走哪条路径。</p>
<h3 id="第二步决定重建方式逐条添加还是用-wildcard-routing-批量覆盖">第二步：决定重建方式——逐条添加，还是用 wildcard routing 批量覆盖</h3>
<p>盘点完存量映射之后，我们发现几十条里大多都遵循同一个规律：仓库地址都是 <code>https://internal-git.mycompany.com/go/&lt;repo&gt;</code>，vanity path 也都是 <code>mycompany.com/go/&lt;repo&gt;</code>，只有仓库名不同。这种情况正好是 gvu 新上线的 <a href="https://gomodvanityurls.com/docs/wildcard-routing/">wildcard routing（通配符路由）</a>想解决的场景。</p>
<p>不过用之前要先说明两个前提：</p>
<ul>
<li>wildcard routing 是 Pro 账号的能力，免费账号（不管是匿名还是 Verified）都用不了，需要执行 <code>gvu subscribe</code> 升级；</li>
<li>wildcard routing 只能用在自定义域名下，默认的 <code>gomod.io</code> 不支持。也就是说，如果你打算用这个能力，第四步&quot;配置自定义域名&quot;要提到这一步之前先做完。</li>
</ul>
<p>确认这两个前提之后，wildcard routing 分两种：</p>
<p><strong>repo wildcard（覆盖同一个组织 / 路径前缀下的多个仓库）</strong></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-bash" data-lang="bash"><span style="display:flex;"><span>gvu add <span style="color:#e6db74">&#39;go.mycompany.com/go/*&#39;</span> <span style="color:#e6db74">&#39;https://internal-git.mycompany.com/go/*.git&#39;</span>
</span></span></code></pre></div><p>注意命令里的单引号不是可有可无的——<code>*</code> 在 shell 里会被当成通配符展开，必须用单引号把带 <code>*</code> 的参数包起来，避免被 shell 提前解释掉。这条规则建立之后，<code>go.mycompany.com/go/</code> 前缀下的任意仓库都会自动解析，不需要逐条添加：</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:#f92672">import</span> <span style="color:#e6db74">&#34;go.mycompany.com/go/common&#34;</span>    <span style="color:#75715e">// → internal-git.mycompany.com/go/common</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> <span style="color:#e6db74">&#34;go.mycompany.com/go/authsdk&#34;</span>   <span style="color:#75715e">// → internal-git.mycompany.com/go/authsdk</span>
</span></span></code></pre></div><p>以后团队新开一个仓库，只要命名遵循这个规律，直接就能用，不需要再执行任何 <code>gvu add</code>。</p>
<p><strong>subdir wildcard（覆盖 monorepo 里的多个子模块）</strong></p>
<p>如果你的私有代码是放在一个 monorepo 里、每个子目录一个独立 Go module（需要 Go 1.25+），可以用另一种通配符：</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-bash" data-lang="bash"><span style="display:flex;"><span>gvu add go.mycompany.com/go/mono https://internal-git.mycompany.com/go/mono.git <span style="color:#e6db74">&#39;--subdir=*&#39;</span>
</span></span></code></pre></div><p><code>--subdir=*</code> 里的 <code>*</code> 会被请求路径里对应的子目录名替换，一条规则覆盖 monorepo 里所有子模块，同样不需要逐条声明。</p>
<p><strong>两种通配符不能混用</strong>：一条路由要么是 repo wildcard，要么是 subdir wildcard，二选一；而且仓库 URL 里只允许出现一个 <code>*</code>。</p>
<p>剩下命名不规律的几条，用 exact route 单独声明，并且优先级更高。</p>
<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-bash" data-lang="bash"><span style="display:flex;"><span>gvu add go.mycompany.com/go/legacy https://internal-git.mycompany.com/legacy-repo/oldname.git
</span></span></code></pre></div><p>gvu 的路由匹配优先级是&quot;精确匹配 &gt; subdir wildcard &gt; repo wildcard&rdquo;，也就是说，即便这条 <code>legacy</code> 恰好也落在前面那条 repo wildcard 规则的覆盖范围内，精确路由也会优先生效，不用担心两条规则打架。</p>
<p>如果你的仓库命名不规整，或者分散在不同 Git 服务器 / 不同组织路径下，没办法用一条通配符规则覆盖，那就还是老老实实逐条添加：</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-bash" data-lang="bash"><span style="display:flex;"><span>gvu add gomod.io/common https://internal-git.mycompany.com/go/common
</span></span><span style="display:flex;"><span>gvu add gomod.io/authsdk https://internal-git.mycompany.com/go/authsdk
</span></span></code></pre></div><h3 id="第三步配额是怎么算的">第三步：配额是怎么算的</h3>
<p>值得一提的是配额规则：一条 wildcard 路由，不管它实际覆盖了多少个仓库，只占用 Pro 账号配额里的 1 个路由名额（Pro 账号总配额 50 条）。也就是说，前面那条覆盖了十个仓库的 repo wildcard 规则，只算 1 条，这也是这次迁移里省下配额最明显的一处。</p>
<h3 id="第四步如果你要保留自己的域名">第四步：如果你要保留自己的域名</h3>
<p>如果你不想让 <code>go.mod</code> 里的 module path 从 <code>mycompany.com/go/common</code> 变成 <code>gomod.io/common</code>（多数团队会希望保留自己的域名语义），或者像上一步那样打算用 wildcard routing，都需要先走自定义域名这条路：</p>
<ol>
<li>用邮箱把匿名账号升级成 Verified 账号（<code>gvu auth bind your@email.com</code>），这一步是免费的，解锁自定义域名能力</li>
<li>在你的 DNS 服务商那里，给 <code>go.mycompany.com</code> 加一条指向 gvu 的 CNAME 记录</li>
<li>在 gvu 里把域名和这条 CNAME 绑定验证通过</li>
<li>如果要用 wildcard routing，还需要额外执行 <code>gvu subscribe</code> 升级到 Pro 账号（自定义域名本身免费账号就能用，但 wildcard routing 是 Pro 专属能力）</li>
</ol>
<p>这一步本质上是把原来 Nginx 承担的&quot;域名 + TLS&quot;职责，转交给了 gvu。证书签发、续期这些事情，也就不用自己操心了。</p>
<h3 id="第五步验证">第五步：验证</h3>
<p>找几个存量项目，重新跑一遍 <code>go mod tidy</code> / <code>go build</code>，确认能正常解析到内部仓库。用了 wildcard routing 的话，记得确认一下 <code>GOPRIVATE</code> 里包含的是整个自定义域名前缀，而不是某一个具体仓库路径：</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-bash" data-lang="bash"><span style="display:flex;"><span>go env -w GOPRIVATE<span style="color:#f92672">=</span>go.mycompany.com
</span></span></code></pre></div><p>否则会出现&quot;路由规则本身没问题，但个别 sub-package 解析不出来&quot;这种容易让人摸不着头脑的情况。</p>
<p>另外建议多挑几个不同仓库名分别验证，而不是测一个就当全部通过——尤其是 repo wildcard，如果某个仓库名在目标 Git 服务器上其实不存在，<code>go get</code> 会报&quot;repository not found&quot;，这种情况下用 <code>git ls-remote</code> 把 <code>*</code> 换成实际仓库名单独测一下就能定位。</p>
<h3 id="第六步下线旧组件">第六步：下线旧组件</h3>
<p>确认所有存量 module 都迁移验证完毕之后，就可以把 <code>govanityurls</code> 进程和对应的 Nginx 配置下线了。注意，这里下线的只是&quot;第一层&quot;，你的内部 GOPROXY（第二层）该怎么跑还怎么跑，不要连它一起下线。</p>
<h2 id="迁移前后对比">迁移前后对比</h2>
<table>
	<thead>
			<tr>
					<th></th>
					<th>迁移前（手动 govanityurls + Nginx）</th>
					<th>迁移后（gvu）</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>需要自己运维的组件</td>
					<td><code>govanityurls</code> 进程、Nginx、TLS 证书</td>
					<td>0</td>
			</tr>
			<tr>
					<td>新增一个私有仓库的操作</td>
					<td>改 <code>vanity.yml</code> → 重载 Nginx</td>
					<td>命名规整的一批仓库：新增仓库时不需要任何操作；命名不规律的：一条 <code>gvu add</code></td>
			</tr>
			<tr>
					<td>覆盖 10 个命名规整仓库所需的路由数</td>
					<td>10 条 <code>vanity.yml</code> 条目</td>
					<td>1 条 repo wildcard 规则，只占 1 个配额名额（需要 Pro 账号 + 自定义域名）</td>
			</tr>
			<tr>
					<td>域名迁移代价（仓库从一个平台挪到另一个平台）</td>
					<td>改 <code>vanity.yml</code> 对应条目，无需下游改动</td>
					<td><code>gvu</code> 里改一下映射，无需下游改动（两者在这一点上其实是一样的，这是 vanity URL 机制本身的优势，不是 gvu 独有）</td>
			</tr>
			<tr>
					<td>TLS 证书</td>
					<td>自己签、自己续期</td>
					<td>gvu 托管</td>
			</tr>
			<tr>
					<td>GOPROXY（module 拉取层）</td>
					<td>不变</td>
					<td>不变，依然需要</td>
			</tr>
	</tbody>
</table>
<p>最后一行故意留在这里，是想再强调一次：这次迁移动的只是&quot;翻译层&quot;，&ldquo;拉取层&quot;从头到尾没有变过，这也是我在文章开头要先把两者区分清楚的原因——如果团队里有人指望&quot;上了 gvu 就不用管 GOPROXY 了&rdquo;，迁移过程中大概率会踩这个认知上的坑。</p>
<h2 id="小结">小结</h2>
<p>这次迁移前后花了不到半个小时，大部分时间花在盘点存量映射上，实际的重建操作本身很快——尤其是这次用上了 wildcard routing 之后，原本要逐条敲的十几条 <code>gvu add</code>，命名规整的十条合并成了一条 repo wildcard 规则就解决了，剩下一条历史遗留的老库单独用精确路由声明，优先级还能保证不会被 wildcard 规则误覆盖。</p>
<p>需要提醒一句：wildcard routing 是 Pro 账号才有的能力，而且必须搭配自定义域名使用，如果你的团队规模不大、存量仓库也没几个，逐条 <code>gvu add</code> 完全够用，不必为了这个能力专门升级付费账号——这个功能真正划算的场景，是仓库数量比较多、且命名规律统一的团队。</p>
<p>更重要的是想清楚了一件事：私有 module 拉取这件事，从来不是一个单一问题，而是&quot;vanity 解析&quot;和&quot;内容拉取&quot;两层问题叠在一起。gvu 解决的是前一层，这一层解决好之后，剩下的路由和证书维护成本基本归零。</p>
<p>下一篇，我会回到起源那篇文章埋的那个坑——白盒交付给客户时，怎么用 gvu 在客户组织内部 10 分钟重建一套私有 module 拉取方案。</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>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img loading="lazy" src="/images/wp-content/uploads/course-card/iamtonybai-banner-2.gif"></p>
<hr>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img loading="lazy" src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png"></p>
<!-- 
公众号地址：https://mp.weixin.qq.com/s/6KjGgCwQCF1j8-OLMheqnQ
-->
]]></content:encoded></item><item><title>Go 私有模块拉取全解：凭据配置 + Vanity URL，个人与组织全覆盖</title><link>https://tonybai.com/2026/07/08/go-private-modules-comprehensive-guide/</link><pubDate>Wed, 08 Jul 2026 06:00:00 +0800</pubDate><guid>https://tonybai.com/2026/07/08/go-private-modules-comprehensive-guide/</guid><description>Go 语言在公共依赖管理上已足够成熟，但私有模块的拉取依然是许多团队和个人开发者频繁踩坑的痛点。本文是一份全景式的实战指南，旨在一次性讲透 Go 私有模块拉取的底层机制与最佳落地实践。</description><content:encoded><![CDATA[<p><img alt="题图" loading="lazy" src="/images/wp-content/uploads/2026/go-private-modules-comprehensive-guide-1.png"></p>
<p><a href="https://tonybai.com/2026/07/08/go-private-modules-comprehensive-guide">本文永久链接</a> – <a href="https://tonybai.com/2026/07/08/go-private-modules-comprehensive-guide">https://tonybai.com/2026/07/08/go-private-modules-comprehensive-guide</a></p>
<p>大家好，我是Tony Bai。</p>
<p>Go 语言的依赖管理从 <code>GOPATH</code> 时代走到 Go Modules，公共依赖的拉取体验已经足够顺滑，但只要涉及“私有模块”，绝大多数团队和个人开发者都会在某个时刻撞上同一堵墙。</p>
<p>具体来说，大概率是下面这几个痛点里的一个或几个：</p>
<ul>
<li>凭据配置散落各处：<code>.netrc</code>、<code>git config</code>、CI 环境变量里都可能藏着一份 token，换一台机器、换一个 CI 平台，就要重新配一遍，稍不注意还容易把明文 token 提交进仓库；</li>
<li>import path 和代码托管平台强绑定：<code>go.mod</code> 里写死了 <code>github.com/yourname/xxx</code>，哪天想把仓库从 GitHub 挪到自建 GitLab 或者 Codeberg，所有依赖这个模块的代码都要跟着改 import path；</li>
<li>想要有自己域名语义的 import path，又不想为此单独运维一套服务：自己写静态页面维护成本高，自建 <code>govanityurls</code> + Nginx 又要操心证书续期、单点故障；</li>
<li>组织规模变大之后，新增一个私有模块的映射变成一件琐事：改配置、重载服务，团队人数越多，这类重复劳动的隐性成本越明显；</li>
<li>代码需要跨组织边界分发时（比如交付给客户），原有的私有拉取方案在对方网络环境里直接失效，需要临时重新搭一遍。</li>
</ul>
<p>这篇文章尝试把这件事一次性讲透，分三部分：</p>
<ul>
<li>第一部分是不分场景都要搞清楚的凭据配置底层机制；</li>
<li>第二部分是个人开发者的几种典型场景；</li>
<li>第三部分是组织 / 团队场景。</li>
</ul>
<p>每种场景我都会给出对应的落地方案，上面这几个痛点，也会在对应的小节里逐一给出解法，供你按需查阅、按需收藏。</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>
<h3 id="两种主流的凭据配置方式">两种主流的凭据配置方式</h3>
<p><strong>方式一：<code>~/.netrc</code></strong></p>
<p>这是最传统的方式，<code>git</code>、<code>go</code> 命令本身都会读取这个文件：</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-fallback" data-lang="fallback"><span style="display:flex;"><span>machine github.com
</span></span><span style="display:flex;"><span>login your-username
</span></span><span style="display:flex;"><span>password ghp_your_personal_access_token
</span></span></code></pre></div><p>以github.com为例，其中的password你可以在“Developer Settings”的“Personal access tokens”中申请，推荐使用“Fine-grained personal access tokens”，你可以更好地控制token的权限粒度。</p>
<p><strong>方式二：<code>git config</code> 的 <code>insteadOf</code> 重写</strong></p>
<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-bash" data-lang="bash"><span style="display:flex;"><span>git config --global url.<span style="color:#e6db74">&#34;https://oauth2:</span><span style="color:#e6db74">${</span>GITHUB_TOKEN<span style="color:#e6db74">}</span><span style="color:#e6db74">@github.com/&#34;</span>.insteadOf <span style="color:#e6db74">&#34;https://github.com/&#34;</span>
</span></span></code></pre></div><p>如果你更习惯 SSH 方式（配好 deploy key 之后），可以让 HTTPS 的拉取请求自动重写成 SSH：</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-bash" data-lang="bash"><span style="display:flex;"><span>git config --global url.<span style="color:#e6db74">&#34;ssh://git@github.com/&#34;</span>.insteadOf <span style="color:#e6db74">&#34;https://github.com/&#34;</span>
</span></span></code></pre></div><p>CI 环境里，建议把 token 放进 CI 平台自带的 secret 变量，而不是明文写进仓库里的脚本文件，这是最基本的安全底线。</p>
<h3 id="四个环境变量职责边界要分清楚">四个环境变量，职责边界要分清楚</h3>
<p>这是整篇文章最重要的一个知识点，后面所有场景都建立在这个区分之上：</p>
<ul>
<li><code>GOPRIVATE</code>：告诉 Go 工具链，哪些 module 路径前缀属于私有模块，遇到这些路径时跳过公共 <code>GOPROXY</code> 和 <code>GOSUMDB</code>，直接走版本控制工具（<code>git</code>）直连拉取。例如：</li>
</ul>
<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-bash" data-lang="bash"><span style="display:flex;"><span>export GOPRIVATE<span style="color:#f92672">=</span>github.com/yourname/*,go.mycompany.com/*
</span></span></code></pre></div><ul>
<li>
<p><code>GOPROXY</code>：控制 module 内容本身怎么拉取、怎么缓存，默认是 <code>https://proxy.golang.org,direct</code>。如果你在公司内部有自己的私有代理（Athens、goproxy/goproxy.cn等），一般会配成类似 <code>https://your-internal-proxy,direct</code> 这样的组合。</p>
</li>
<li>
<p><code>GOSUMDB</code>：控制是否要向 <code>sum.golang.org</code> 做校验和验证，私有模块通常需要连同 <code>GOPRIVATE</code> 一起把这一层也关掉，否则公共校验服务器根本没有你私有模块的记录，会导致拉取失败。</p>
</li>
<li>
<p><code>GOVCS</code>：Go 1.16 引入的安全限制，控制哪些路径允许用哪种版本控制工具拉取，默认只允许公共托管平台走 <code>git</code>/<code>hg</code> 等，私有域名如果不在白名单里，可能需要显式配置，例如：</p>
</li>
</ul>
<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-bash" data-lang="bash"><span style="display:flex;"><span>export GOVCS<span style="color:#f92672">=</span>go.mycompany.com:git
</span></span></code></pre></div><p>简单总结一下这四者的关系：<code>GOPRIVATE</code> 划定“哪些是私有的”，<code>GOPROXY</code>/<code>GOSUMDB</code> 决定“公共部分怎么拉、怎么校验”，<code>GOVCS</code> 决定“私有部分允许用什么工具拉”。</p>
<p>理解了这一层，后面无论是个人场景还是组织场景，本质上都只是在“私有仓库放在哪里”和“import path 要不要自定义”这两个维度上做选择，凭据配置这一层的原理是完全一致的。</p>
<h2 id="个人开发者的私有模块拉取">个人开发者的私有模块拉取</h2>
<h3 id="直接使用-github--gitlab-等平台的私有仓库">直接使用 GitHub / GitLab 等平台的私有仓库</h3>
<p>这是最简单、成本最低的场景。假设你在 GitHub 上有一个私有仓库 <code>github.com/tonybai/privatemodule</code>，配置如下：</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-bash" data-lang="bash"><span style="display:flex;"><span>export GOPRIVATE<span style="color:#f92672">=</span>github.com/tonybai/*
</span></span><span style="display:flex;"><span>git config --global url.<span style="color:#e6db74">&#34;https://oauth2:</span><span style="color:#e6db74">${</span>GITHUB_TOKEN<span style="color:#e6db74">}</span><span style="color:#e6db74">@github.com/&#34;</span>.insteadOf <span style="color:#e6db74">&#34;https://github.com/&#34;</span>
</span></span></code></pre></div><p><code>go get github.com/tonybai/privatemodule</code> 即可正常工作，<code>go.mod</code> 里的 module path 直接就是 <code>github.com/tonybai/privatemodule</code>，不需要任何额外的“翻译层”。</p>
<h3 id="想用个人域名做-import-path代码仍托管在-github--gitlab--codeberg-等平台">想用个人域名做 import path，代码仍托管在 GitHub / GitLab / Codeberg 等平台</h3>
<p>如果你不满足于 <code>github.com/tonybai/privatemodule</code> 这种带着托管平台印记的 import path，希望换成 <code>example.me/go/privatemodule</code>，或者希望以后即便把仓库从 GitHub 挪到 GitLab、Codeberg，import path 也保持不变，这就需要引入 vanity import URL 机制。</p>
<p>原理很简单：Go 工具链请求 <code>https://example.me/go/privatemodule?go-get=1</code> 时，期望拿到一个包含 <code>go-import</code> meta 标签的 HTML 页面，告诉它真实仓库地址在哪里：</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-html" data-lang="html"><span style="display:flex;"><span>&lt;<span style="color:#f92672">meta</span> <span style="color:#a6e22e">name</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;go-import&#34;</span> <span style="color:#a6e22e">content</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;example.me/go/privatemodule git https://github.com/tonybai/privatemodule&#34;</span>&gt;
</span></span></code></pre></div><p>按运维成本从低到高，有几种落地方式：</p>
<p><strong>方案一：手写静态 HTML，挂到 GitHub Pages / Cloudflare Pages 上</strong></p>
<p>几乎零成本，只要给每个 module 路径建一个静态页面即可，缺点是每新增一个 module 都要手动加一个页面，仓库多了之后维护起来比较琐碎。</p>
<p><strong>方案二：自己部署 <code>govanityurls</code></strong></p>
<p><code>govanityurls</code> 是官方 <code>golang.org/x</code> 下的一个开源工具，读取一份 YAML 映射配置，自动生成对应的 <code>go-import</code> 页面。需要自己找一台机器（哪怕是最便宜的 VPS）跑这个服务，前面配一层 Nginx 做 TLS 终止。可控性最高，但要自己维护这套服务的可用性和证书续期。</p>
<p><strong>方案三：用 gvu 这类托管服务</strong></p>
<p>如果不想自己运维一套 <code>govanityurls</code> + Nginx，也可以用 gvu（<a href="https://gomodvanityurls.com">gomodvanityurls.com</a>）这类托管化的 vanity URL 服务，一条命令创建映射：</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-bash" data-lang="bash"><span style="display:flex;"><span>gvu add example.me/go/privatemodule https://github.com/tonybai/privatemodule.git
</span></span></code></pre></div><p>不需要自己起服务、配证书，适合不想为了一个 vanity 层单独维护基础设施的个人开发者。这只是三种方案里的一种，具体选哪种，取决于你是否愿意为了完全的可控性，多维护一套自己的服务。</p>
<h3 id="自己在-vps-上搭-git-server同时想用个人域名做-import-path">自己在 VPS 上搭 Git Server，同时想用个人域名做 import path</h3>
<p>如果你更进一步，连代码托管也不想依赖第三方平台，自己在 VPS 上跑了 Gitea、GitLab CE 或者 Codeberg 式的自建 Git Server，这里有个关键认知：<strong>vanity 解析层和实际代码托管，完全可以是两个互相独立的服务</strong>，用的甚至可以是不同的域名、不同的机器。</p>
<p>这种场景下，前面三种方案依然都成立，只是 <code>go-import</code> 里指向的真实仓库地址，从 <code>github.com/...</code> 换成了你自己 VPS 上的 Git Server 地址，例如：</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-html" data-lang="html"><span style="display:flex;"><span>&lt;<span style="color:#f92672">meta</span> <span style="color:#a6e22e">name</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;go-import&#34;</span> <span style="color:#a6e22e">content</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;example.me/go/privatemodule git https://git.example.me/tonybai/privatemodule&#34;</span>&gt;
</span></span></code></pre></div><p>对应到 gvu 的用法上，就是把目标仓库地址换成你自建 Git Server 的地址：</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-bash" data-lang="bash"><span style="display:flex;"><span>gvu add example.me/go/privatemodule https://git.example.me/tonybai/privatemodule.git
</span></span></code></pre></div><p>同样，手写静态页面或者自建 <code>govanityurls</code> 这两种方式在这个场景下也完全适用，选择哪种取决于你愿意为“完全自主可控”付出多少运维成本。</p>
<h2 id="组织--团队的私有模块拉取">组织 / 团队的私有模块拉取</h2>
<p>组织场景相比个人场景，多出的核心考量是：<strong>仓库数量更多、参与人员更多、可用性要求更高</strong>——vanity 解析服务一旦故障，可能意味着全公司的 CI 流水线都构建失败，这是个人场景基本不需要考虑的风险等级。</p>
<h3 id="组织统一使用-github--gitlab-等平台的私有仓库">组织统一使用 GitHub / GitLab 等平台的私有仓库</h3>
<p>这是最常见的组织场景，凭据管理上比个人场景多几个需要注意的点：</p>
<ul>
<li>建议用机器账号（bot account）而不是某个具体成员的个人账号去生成访问令牌，避免因为人员变动导致 CI 突然失效；</li>
<li>令牌尽量按仓库或按项目限权，而不是签发一个能访问组织下所有仓库的超级令牌；</li>
<li>令牌统一放在 CI 平台的 secret 管理里，避免散落在各个项目的配置文件中。</li>
</ul>
<p>其余配置和个人场景完全一致，<code>GOPRIVATE</code> 换成组织域名前缀即可：</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-bash" data-lang="bash"><span style="display:flex;"><span>export GOPRIVATE<span style="color:#f92672">=</span>github.com/mycorp/*
</span></span></code></pre></div><h3 id="组织想用组织域名做-vanity-import-path">组织想用组织域名做 vanity import path</h3>
<p>这是很多团队迟早会遇到的需求：不希望 <code>go.mod</code> 里到处是 <code>github.com/mycorp/xxx</code>，而是想统一成 <code>go.mycompany.com/xxx</code>，一方面是品牌语义，另一方面是为未来可能的仓库迁移（比如从 GitHub 换到自建 GitLab）留出余地——只要 vanity 层的映射改一下，所有下游使用方的 import path 完全不用变。</p>
<p>五年前我在公司内部第一次搭建这类方案时，写过几篇实践记录（《<a href="https://tonybai.com/2021/09/03/the-approach-to-go-get-private-go-module-in-house">小厂内部私有Go module拉取方案</a>》、《<a href="https://tonybai.com/2022/06/18/the-approach-to-go-get-private-go-module-in-house-part2">小厂内部私有Go module拉取方案（续）</a>》和《<a href="https://tonybai.com/2023/03/03/the-approach-to-go-get-private-go-module-in-house-part3/">小厂内部私有Go module拉取方案3</a>》），走的就是自建 <code>govanityurls</code> + Nginx 这条路。这套方案跑了五年，整体稳定，最大的优势是完全自主可控，不依赖任何第三方服务；代价是每新增一个仓库都要手动改一次映射配置、重载一次 Nginx，团队规模大了之后，这个日常维护成本会逐渐显现出来。</p>
<p>如果团队不想自己承担这部分运维成本，用 gvu 这类托管方案可以把“新增映射”这个动作压缩成一条命令，而且天然具备高可用（不用担心自己那台跑 <code>govanityurls</code> 的机器挂了导致全公司构建失败）：</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-bash" data-lang="bash"><span style="display:flex;"><span>gvu add go.mycompany.com/privatemodule https://github.com/mycorp/privatemodule.git
</span></span></code></pre></div><p>两种路线各有取舍：</p>
<ul>
<li>自建方案胜在完全自主、不依赖外部服务的可用性；</li>
<li>托管方案胜在免运维、天然高可用。</li>
</ul>
<p>具体选哪个，取决于团队愿意在这件事上投入多少运维精力。</p>
<h3 id="组织在内部数据中心--vps-自建-git-server同时要-vanity-import-path">组织在内部数据中心 / VPS 自建 Git Server，同时要 vanity import path</h3>
<p>这是组织场景里最复杂、也最容易被低估的一种情况：私有代码托管在内部数据中心自建的 GitLab / Gitea 上，同时又希望有一层语义化的 vanity import path。原理和 前面个人场景自建git server完全一致，只是仓库地址换成了内部数据中心的地址（通常是内网 IP 或内网域名）。</p>
<p>这里额外提一句：如果你的团队还涉及把这类私有代码以“白盒交付”的方式给到外部客户，会遇到一个更棘手的问题——客户的网络环境访问不到你内部的 vanity 服务和内部仓库，这又是另一套需要专门讨论的方案了，我在上一篇《<a href="https://tonybai.com/2026/07/05/go-private-modules-lessons-learned/">五年，三篇文章，一个我一直没真正解决的问题</a>》里记录过我的具体做法，这里不展开。</p>
<h2 id="小结怎么选">小结：怎么选</h2>
<p>把上面讨论的几个维度整理成一份简单的参考：</p>
<ul>
<li><strong>只是想让私有依赖能被 <code>go get</code> 拉到，不在乎 import path 好不好看</strong>：直接用托管平台的私有仓库 + 对应凭据配置即可，不需要引入 vanity 层。</li>
<li><strong>想要自定义 import path，且不介意自己运维一套服务</strong>：手写静态页面（仓库少）或自建 <code>govanityurls</code>（仓库多、要求可控）都是合理选择。</li>
<li><strong>想要自定义 import path，但不想自己运维 vanity 服务</strong>：可以看看 gvu <a href="https://gomodvanityurls.com">gomodvanityurls.com</a>这类托管方案，一条命令搞定映射。</li>
<li><strong>组织规模较大、涉及高可用要求，或者需要频繁给不同客户 / 项目分配独立的 import path</strong>：优先考虑免运维的托管方案，或者在自建方案上补齐高可用保障，两者都是可行路径，关键是提前想清楚团队愿意为“完全自主可控”付出多少运维成本。</li>
</ul>
<p>无论最终选哪条路，先把第一部分那几个环境变量的职责边界搞清楚，后面遇到的大多数报错，基本都能顺着这条线索排查出来。</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>你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？</p>
<ul>
<li>想写出更地道、更健壮的Go代码，却总在细节上踩坑？</li>
<li>渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？</li>
<li>想打造生产级的Go服务，却在工程化实践中屡屡受挫？</li>
</ul>
<p>继《<a href="http://gk.link/a/10AVZ">Go语言第一课</a>》后，我的《<a href="http://gk.link/a/12yGY">Go语言进阶课</a>》终于在极客时间与大家见面了！</p>
<p>我的全新极客时间专栏 《<a href="http://gk.link/a/12yGY">Tony Bai·Go语言进阶课</a>》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。</p>
<p>目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！</p>
<p><img loading="lazy" src="/images/wp-content/uploads/course-card/iamtonybai-banner-2.gif"></p>
<hr>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img loading="lazy" src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png"></p>
<!-- 
公众号地址：https://mp.weixin.qq.com/s/4LMgDpHaX2Q4oc3U4DtStQ
-->
]]></content:encoded></item></channel></rss>