<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>第 17 章 模块与依赖 on Go 语言原本</title><link>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch17modules/</link><description>Recent content in 第 17 章 模块与依赖 on Go 语言原本</description><generator>Hugo</generator><language>zh-cn</language><atom:link href="http://golang.design/under-the-hood/zh-cn/part5toolchain/ch17modules/index.xml" rel="self" type="application/rss+xml"/><item><title>17.1 依赖管理的难点</title><link>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch17modules/challenges/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch17modules/challenges/</guid><description>&lt;h1 id="171-依赖管理的难点"&gt;17.1 依赖管理的难点&lt;/h1&gt;
&lt;p&gt;现代软件几乎不再从零写起。一个寻常的服务端程序，&lt;code&gt;import&lt;/code&gt; 列表往下走一层是日志库、HTTP
路由、序列化器，再往下是它们各自依赖的加密、压缩、网络原语,真正属于你自己的代码，可能不到
整张依赖图的百分之一。这带来了复用的红利，也带来了一个本不属于你、却必须由你回答的问题：
&lt;strong&gt;这张图上每个包，到底该用哪个版本？&lt;/strong&gt; 这一节不急着给 Go 的答案，先把问题本身的难讲清楚。
难点有两条主线：一是&lt;strong&gt;版本如何选&lt;/strong&gt;（钻石依赖与约束求解），二是&lt;strong&gt;选定之后如何不漂移&lt;/strong&gt;
（可重现构建）。理解了这两条，才能看懂 &lt;a href=".././minimum"&gt;17.3&lt;/a&gt; 那套「最小版本选择」为什么值得
专门设计，以及 Go 为走到那一步付出过什么代价。&lt;/p&gt;
&lt;h2 id="1711-钻石依赖一个约束求解问题"&gt;17.1.1 钻石依赖：一个约束求解问题&lt;/h2&gt;
&lt;p&gt;依赖管理的核心难题，集中体现在一种叫&lt;strong&gt;钻石依赖&lt;/strong&gt;（diamond dependency）的拓扑上。你的程序同时
依赖 A 和 B，而 A 和 B 又都依赖同一个 C,依赖图从顶点出发分成两支，又在 C 处重新汇合，画出来
正是一颗钻石。问题出在两支对 C 提出了&lt;strong&gt;不同的版本要求&lt;/strong&gt;：&lt;/p&gt;

&lt;script src="http://golang.design/under-the-hood/mermaid.min.js"&gt;&lt;/script&gt;
&lt;script&gt;
 window.addEventListener("DOMContentLoaded", function () {
 mermaid.initialize({
 startOnLoad: false,
 theme: "neutral",
 securityLevel: "strict",
 fontFamily: "inherit",
 });
 mermaid.run({ querySelector: ".mermaid" });
 });
&lt;/script&gt;


&lt;pre class="mermaid"&gt;flowchart TD
 APP[&amp;#34;你的程序 main&amp;#34;] --&amp;gt; A[&amp;#34;A v1.0&amp;#34;]
 APP --&amp;gt; B[&amp;#34;B v1.0&amp;#34;]
 A --&amp;gt;|&amp;#34;require C v1.2&amp;#34;| C12[&amp;#34;C v1.2&amp;#34;]
 B --&amp;gt;|&amp;#34;require C v1.5&amp;#34;| C15[&amp;#34;C v1.5&amp;#34;]
 C12 -.-&amp;gt;|&amp;#34;二者只能合一&amp;#34;| PICK{&amp;#34;链进哪个 C?&amp;#34;}
 C15 -.-&amp;gt; PICK&lt;/pre&gt;
&lt;p&gt;为什么不能让 A 用 C v1.2、B 用 C v1.5，各取所需？多数语言做不到。C 的两个版本通常导出同一个
包路径、同一组类型名，把两份链进同一个程序，链接器会撞上重复符号；即便链接器允许，C v1.2 的
&lt;code&gt;Token&lt;/code&gt; 和 C v1.5 的 &lt;code&gt;Token&lt;/code&gt; 在类型系统里也是两个互不兼容的类型,一旦 A 把自己拿到的 &lt;code&gt;Token&lt;/code&gt;
传给 B，就会在边界上炸开。这条裂缝用一段示意代码看得更清楚：&lt;/p&gt;</description></item><item><title>17.2 语义化版本管理</title><link>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch17modules/semantics/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch17modules/semantics/</guid><description>&lt;h1 id="172-语义化版本管理"&gt;17.2 语义化版本管理&lt;/h1&gt;
&lt;p&gt;要管理版本，先得让版本号&lt;strong&gt;有意义&lt;/strong&gt;。&lt;a href=".././challenges"&gt;17.1&lt;/a&gt; 把依赖管理的难处归到两点：钻石依赖
里的版本冲突，与跨机器、跨时间的可重现构建。这两点能不能解，取决于一个更底层的前提，&lt;strong&gt;一个版本
号到底承诺了什么&lt;/strong&gt;。如果 &lt;code&gt;v1.5&lt;/code&gt; 与 &lt;code&gt;v1.4&lt;/code&gt; 之间的关系完全无从预料，那么任何选择算法都无从下手，
只能退回到「把所有版本两两试一遍」的求解。Go 模块的做法，是先把版本号变成一份&lt;strong&gt;可被机器信任的
契约&lt;/strong&gt;，再在这份契约之上提出一条独特而强硬的规则,&lt;strong&gt;语义化导入版本&lt;/strong&gt;。这一节讲清这两者，它们是
&lt;a href=".././minimum"&gt;17.3&lt;/a&gt; 那套版本选择算法能够成立、且能简单到「一遍图遍历」的前提。&lt;/p&gt;
&lt;h2 id="1721-语义化版本把兼容性写进数字"&gt;17.2.1 语义化版本：把兼容性写进数字&lt;/h2&gt;
&lt;p&gt;语义化版本（Semantic Versioning，semver）由 Tom Preston-Werner 在 2013 年定形为 2.0.0
规范，Go 模块直接采用了它，并在版本号前统一冠以 &lt;code&gt;v&lt;/code&gt;。版本号形如 &lt;strong&gt;&lt;code&gt;vMAJOR.MINOR.PATCH&lt;/code&gt;&lt;/strong&gt;
（如 &lt;code&gt;v1.4.2&lt;/code&gt;），三段都是非负整数，且每一段的递增都&lt;strong&gt;有规范约定的含义&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PATCH&lt;/strong&gt;（补丁）递增：「只引入向后兼容的 bug 修复」。&lt;code&gt;v1.4.1 → v1.4.2&lt;/code&gt;，行为不变，只是更正了
错误。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MINOR&lt;/strong&gt;（次版本）递增：「以向后兼容的方式向公开 API 增加了功能」。&lt;code&gt;v1.4 → v1.5&lt;/code&gt;，老代码
原封不动仍能编译、仍能运行，只是多了可用的东西。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MAJOR&lt;/strong&gt;（主版本）递增：「引入了向后不兼容的 API 变更」。&lt;code&gt;v1.x → v2.0&lt;/code&gt;，老代码可能编译不过，
或行为悄然改变。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;把这套约定落到一个具体改动上，递增哪一段是被改动的性质唯一决定的，并无自由裁量：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;现有版本 v1.4.2，下一次发布该打什么版本号？
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 修了一个 panic，不动任何签名 → v1.4.3 （PATCH：兼容的修复）
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 新增一个导出函数 Encode，旧 API 不变 → v1.5.0 （MINOR：兼容的新功能）
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 删掉 Decode、或改了它的参数类型 → v2.0.0 （MAJOR：破坏兼容）
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;注意每升一段，更低的段都归零：&lt;code&gt;v1.4.2&lt;/code&gt; 的次版本一升就是 &lt;code&gt;v1.5.0&lt;/code&gt; 而非 &lt;code&gt;v1.5.2&lt;/code&gt;，主版本一升就是
&lt;code&gt;v2.0.0&lt;/code&gt;。这条归零规则保证了「同一主版本下，版本号越大、含的兼容功能越全」，&lt;a href=".././minimum"&gt;17.3&lt;/a&gt;
的「取最大」才有明确语义。&lt;/p&gt;</description></item><item><title>17.3 最小版本选择算法</title><link>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch17modules/minimum/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch17modules/minimum/</guid><description>&lt;h1 id="173-最小版本选择算法"&gt;17.3 最小版本选择算法&lt;/h1&gt;
&lt;p&gt;确定了语义化版本（&lt;a href=".././semantics"&gt;17.2&lt;/a&gt;）后，还剩最后一问：当依赖图里各模块要求同一个依赖的
不同版本时，到底&lt;strong&gt;选哪个版本&lt;/strong&gt;？Go 的答案是一个反直觉、却异常简洁的算法,&lt;strong&gt;最小版本选择&lt;/strong&gt;
（Minimal Version Selection, MVS）。它是 Go 模块最具特色、也最值得玩味的设计：别家包管理器把
这件事做成了一个需要约束求解器的难题，Go 却把它压成了一遍图遍历。本节先把规则讲清，再看它为何
能这么简单，最后把它放回整个领域的谱系里，看清它究竟换走了什么、换来了什么。&lt;/p&gt;
&lt;h2 id="1731-选满足要求的最低版本"&gt;17.3.1 选「满足要求的最低版本」&lt;/h2&gt;
&lt;p&gt;MVS 的规则简单到令人意外：对每个依赖，选&lt;strong&gt;所有要求中最高的那个最低要求&lt;/strong&gt;,换句话说，选择
&lt;strong&gt;能满足所有模块要求的最低版本&lt;/strong&gt;。先用本书一贯的小例子建立直觉。你的程序直接要求 C ≥ v1.2;
依赖 A 要求 C ≥ v1.3;依赖 B 要求 C ≥ v1.1。三个要求里最高的下限是 v1.3,MVS 就选 &lt;strong&gt;C v1.3&lt;/strong&gt;。
注意这里的关键：哪怕此刻 C 已经发布了 v1.9，MVS &lt;strong&gt;也不选 v1.9&lt;/strong&gt;,它只选「满足所有要求的最低
版本」，即 v1.3。&lt;/p&gt;
&lt;p&gt;把这条规则放进一张真实的依赖图，就是 Go 官方文档采用的例子。主模块要求 A ≥ v1.2、B ≥ v1.2;
A v1.2 要求 C ≥ v1.3;B v1.2 要求 C ≥ v1.4;而 C v1.3 与 C v1.4 都要求 D ≥ v1.2。MVS 从主模块
出发，沿 &lt;code&gt;require&lt;/code&gt; 边走遍整张图，为每个模块记下「见过的最高要求」：&lt;/p&gt;</description></item><item><title>17.4 vgo 与 dep 之争</title><link>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch17modules/fight/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch17modules/fight/</guid><description>&lt;h1 id="174-vgo-与-dep-之争"&gt;17.4 vgo 与 dep 之争&lt;/h1&gt;
&lt;p&gt;今天的 Go 模块（Go Modules）并非一开始就有。它从一场颇具戏剧性、也颇有争议的社区事件里
诞生：&lt;strong&gt;vgo 与 dep 之争&lt;/strong&gt;。这段历史不只是八卦，它折射出开源项目治理、技术决策与社区情绪之间
的真实张力，是理解 Go 模块「为何是今天这样」的最后一块拼图，也是这本书一路谈下来的设计哲学
在工具链层面的一次集中演出。&lt;/p&gt;
&lt;p&gt;前面两节已经把 Go 模块的技术内核摆出来了：语义化版本管理（&lt;a href=".././semantics"&gt;17.2&lt;/a&gt;）把主版本
写进导入路径，最小版本选择（&lt;a href=".././minimum"&gt;17.3&lt;/a&gt;）用一条确定性算法挑出每个依赖的版本，二者
合起来，让 Go 既不需要 lock 文件、也不需要约束求解器。这一节回答的是另一个问题：为什么是
&lt;strong&gt;这套&lt;/strong&gt;方案，而不是当时社区已经投入两年、几乎被默认为「未来」的那一套。要讲清楚这个，得把
时间拨回到 2016 年。&lt;/p&gt;
&lt;h2 id="1741-dep社区的官方实验"&gt;17.4.1 dep：社区的「官方实验」&lt;/h2&gt;
&lt;p&gt;GOPATH 时代的依赖混乱（&lt;a href=".././challenges"&gt;17.1&lt;/a&gt;）催生过一大批第三方工具：&lt;code&gt;godep&lt;/code&gt;、&lt;code&gt;glide&lt;/code&gt;、
&lt;code&gt;govendor&lt;/code&gt;，各自为政，互不兼容。2016 年，Go 团队决定收口，成立了一个依赖管理工作组，由
Sam Boyer 等社区成员牵头，做出一个被定位为&lt;strong&gt;官方实验&lt;/strong&gt;（official experiment）的工具：
&lt;strong&gt;&lt;code&gt;dep&lt;/code&gt;&lt;/strong&gt;。它不是某个人的玩具，而是 Go 团队背书、社区深度参与、被广泛理解为「将来会并入
&lt;code&gt;go&lt;/code&gt; 命令」的候选方案。许多公司和个人据此调整了工程实践，把项目迁到 dep 上。&lt;/p&gt;
&lt;p&gt;技术上，dep 走的是当时&lt;strong&gt;主流&lt;/strong&gt;的路子，与 Rust 的 Cargo、Ruby 的 Bundler、JavaScript 的 npm
一脉相承：一个清单文件声明你想要的版本范围，一个 &lt;strong&gt;lock 文件&lt;/strong&gt;钉死本次解析出的精确版本，中间
夹着一个&lt;strong&gt;约束求解器&lt;/strong&gt;。求解器要做的事，本质是布尔可满足性（SAT）：把「A 需要 B 的 &lt;code&gt;&amp;gt;=1.2&lt;/code&gt;」
「C 需要 B 的 &lt;code&gt;&amp;lt;2.0&lt;/code&gt;」这类约束翻译成逻辑公式，搜一组同时满足所有人的版本赋值出来。&lt;/p&gt;</description></item></channel></rss>