<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Tony Bai &#187; dep</title>
	<atom:link href="http://tonybai.com/tag/dep/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Fri, 17 Apr 2026 00:21:29 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.2.1</generator>
		<item>
		<title>Go 模块构建与依赖管理：我们到底在“折腾”什么？</title>
		<link>https://tonybai.com/2025/10/27/the-ultimate-guide-to-go-module/</link>
		<comments>https://tonybai.com/2025/10/27/the-ultimate-guide-to-go-module/#comments</comments>
		<pubDate>Mon, 27 Oct 2025 00:08:18 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[asm]]></category>
		<category><![CDATA[Cgo]]></category>
		<category><![CDATA[CI/CD]]></category>
		<category><![CDATA[dep]]></category>
		<category><![CDATA[exclude]]></category>
		<category><![CDATA[GET]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-get]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[go.sum]]></category>
		<category><![CDATA[go.work]]></category>
		<category><![CDATA[go1.21]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomodtidy]]></category>
		<category><![CDATA[GoModuleProxy协议]]></category>
		<category><![CDATA[GoModules]]></category>
		<category><![CDATA[GoModules核心原理]]></category>
		<category><![CDATA[GONOSUMDB]]></category>
		<category><![CDATA[GOPATH]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[GOPRIVATE]]></category>
		<category><![CDATA[GOPROXY]]></category>
		<category><![CDATA[Go专家]]></category>
		<category><![CDATA[Go开发]]></category>
		<category><![CDATA[Go插件]]></category>
		<category><![CDATA[Go构建与依赖管理体系]]></category>
		<category><![CDATA[Go构建体系]]></category>
		<category><![CDATA[Go模块]]></category>
		<category><![CDATA[Go模块构建与依赖管理]]></category>
		<category><![CDATA[Go熟练工]]></category>
		<category><![CDATA[Go语言]]></category>
		<category><![CDATA[Go语言第一课]]></category>
		<category><![CDATA[Go语言进阶课]]></category>
		<category><![CDATA[HTTP协议]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[list]]></category>
		<category><![CDATA[mvs]]></category>
		<category><![CDATA[npm]]></category>
		<category><![CDATA[pip]]></category>
		<category><![CDATA[Plugin插件]]></category>
		<category><![CDATA[replace]]></category>
		<category><![CDATA[retract]]></category>
		<category><![CDATA[tidy]]></category>
		<category><![CDATA[TonyBai]]></category>
		<category><![CDATA[toolchain]]></category>
		<category><![CDATA[toolchaingo1.22.0]]></category>
		<category><![CDATA[v1]]></category>
		<category><![CDATA[v2]]></category>
		<category><![CDATA[vendor]]></category>
		<category><![CDATA[三剑客]]></category>
		<category><![CDATA[中文资料]]></category>
		<category><![CDATA[主版本]]></category>
		<category><![CDATA[从入门到精通]]></category>
		<category><![CDATA[企业级实践]]></category>
		<category><![CDATA[作者]]></category>
		<category><![CDATA[使用者]]></category>
		<category><![CDATA[依赖]]></category>
		<category><![CDATA[依赖关系]]></category>
		<category><![CDATA[依赖添加]]></category>
		<category><![CDATA[依赖管理]]></category>
		<category><![CDATA[借鉴]]></category>
		<category><![CDATA[入门宝典]]></category>
		<category><![CDATA[全面]]></category>
		<category><![CDATA[公众号]]></category>
		<category><![CDATA[兼容性]]></category>
		<category><![CDATA[兼容性机制]]></category>
		<category><![CDATA[兼容性的承诺]]></category>
		<category><![CDATA[内网]]></category>
		<category><![CDATA[出书]]></category>
		<category><![CDATA[创建]]></category>
		<category><![CDATA[动态库]]></category>
		<category><![CDATA[包]]></category>
		<category><![CDATA[包罗万象]]></category>
		<category><![CDATA[升华与实战]]></category>
		<category><![CDATA[升级]]></category>
		<category><![CDATA[历史]]></category>
		<category><![CDATA[历史的源头]]></category>
		<category><![CDATA[发布]]></category>
		<category><![CDATA[可重现的构建]]></category>
		<category><![CDATA[合伙创业]]></category>
		<category><![CDATA[告别]]></category>
		<category><![CDATA[咨询]]></category>
		<category><![CDATA[商务合作]]></category>
		<category><![CDATA[圣经]]></category>
		<category><![CDATA[在线课程]]></category>
		<category><![CDATA[培训]]></category>
		<category><![CDATA[复杂场景]]></category>
		<category><![CDATA[多模块开发]]></category>
		<category><![CDATA[大型Go项目]]></category>
		<category><![CDATA[天坑]]></category>
		<category><![CDATA[天文数字]]></category>
		<category><![CDATA[学习之旅]]></category>
		<category><![CDATA[实战解剖]]></category>
		<category><![CDATA[工作流]]></category>
		<category><![CDATA[工具]]></category>
		<category><![CDATA[工具链]]></category>
		<category><![CDATA[工程能力]]></category>
		<category><![CDATA[巨型项目]]></category>
		<category><![CDATA[广告合作]]></category>
		<category><![CDATA[手术刀]]></category>
		<category><![CDATA[报错]]></category>
		<category><![CDATA[指令]]></category>
		<category><![CDATA[排错技巧]]></category>
		<category><![CDATA[探索]]></category>
		<category><![CDATA[撰稿]]></category>
		<category><![CDATA[最小版本选择]]></category>
		<category><![CDATA[本地多模块开发]]></category>
		<category><![CDATA[权衡]]></category>
		<category><![CDATA[极客时间]]></category>
		<category><![CDATA[构建]]></category>
		<category><![CDATA[构建之道]]></category>
		<category><![CDATA[构建模式]]></category>
		<category><![CDATA[构建问题]]></category>
		<category><![CDATA[核心原理]]></category>
		<category><![CDATA[案例与排错]]></category>
		<category><![CDATA[模块作者]]></category>
		<category><![CDATA[模块使用者]]></category>
		<category><![CDATA[模块的生命周期]]></category>
		<category><![CDATA[次要版本]]></category>
		<category><![CDATA[泥潭]]></category>
		<category><![CDATA[深入]]></category>
		<category><![CDATA[混乱]]></category>
		<category><![CDATA[混合构建]]></category>
		<category><![CDATA[演进史]]></category>
		<category><![CDATA[知其所以然]]></category>
		<category><![CDATA[知其然]]></category>
		<category><![CDATA[知识体系]]></category>
		<category><![CDATA[私信]]></category>
		<category><![CDATA[私有Proxy]]></category>
		<category><![CDATA[私有仓库]]></category>
		<category><![CDATA[私有库]]></category>
		<category><![CDATA[秩序]]></category>
		<category><![CDATA[移除]]></category>
		<category><![CDATA[系统]]></category>
		<category><![CDATA[终极排错指南]]></category>
		<category><![CDATA[终章]]></category>
		<category><![CDATA[补丁]]></category>
		<category><![CDATA[认知]]></category>
		<category><![CDATA[设计哲学]]></category>
		<category><![CDATA[跨越边界]]></category>
		<category><![CDATA[逻辑]]></category>
		<category><![CDATA[降级]]></category>
		<category><![CDATA[静态库]]></category>
		<category><![CDATA[顶级案例]]></category>
		<category><![CDATA[高保真]]></category>
		<category><![CDATA[高效学习]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=5318</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/10/27/the-ultimate-guide-to-go-module 大家好，我是Tony Bai。 我想问大家一个问题：在你日常的 Go 开发中，有没有哪个瞬间，让你觉得明明是在做一件简单的事，却被工具链“折腾”得心力交瘁？ 或许是那个深夜，CI/CD 系统一片爆红，只因为一位同事提交代码时，忘记删掉了 go.mod 文件里那行指向他本地路径的 replace 指令。 或许是你刚接手一个老项目，面对 GOPATH 和 vendor 的“历史遗迹”，想升级一个包却发现牵一发而动全身，最后只能无奈放弃。 又或许，你只是想在公司内网拉取一个私有库，却被 GOPROXY、GOPRIVATE、GONOSUMDB 这“三兄弟”搞得晕头转向，在 404 和 410 的报错中反复挣扎。 如果这些场景让你会心一笑（或者苦笑），那么，欢迎来到我们的世界。 Go 模块的构建与依赖管理，就像我们呼吸的空气。 它无处不在，支撑着我们所有的Go开发活动。但正因为它如此基础，我们常常满足于“能用就行”，而忽略了其背后深刻的设计哲学和强大的工程能力。直到有一天，我们被一个棘手的构建问题拦住去路，才发现自己对这套最熟悉的工具，其实知之甚少。 为什么我要写这个微专栏？ 市面上关于 Go Modules 的文章很多，但大多是“点状”的：教你一个命令，解决一个问题。但我发现，很少有内容能系统性地回答那几个更深层次的“为什么”： Go 为什么会放弃 GOPATH，经历 vendor、dep 的探索，最终选择了 Go Modules 这条路？这背后是怎样的历史和权衡？ 最小版本选择（MVS）算法到底是什么？它和 npm/pip 的逻辑有何本质不同，为什么说它带来了“高保真”的可重现的构建？ go.mod 里的 go 1.21 和 toolchain go1.22.0 到底是什么关系？它们是如何维系 Go [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/the-ultimate-guide-to-go-module.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/10/27/the-ultimate-guide-to-go-module">本文永久链接</a> &#8211; https://tonybai.com/2025/10/27/the-ultimate-guide-to-go-module</p>
<p>大家好，我是Tony Bai。</p>
<p>我想问大家一个问题：在你日常的 Go 开发中，有没有哪个瞬间，让你觉得明明是在做一件简单的事，却被工具链“折腾”得心力交瘁？</p>
<p>或许是那个深夜，CI/CD 系统一片爆红，只因为一位同事提交代码时，忘记删掉了 go.mod 文件里那行指向他本地路径的 replace 指令。</p>
<p>或许是你刚接手一个老项目，面对 GOPATH 和 vendor 的“历史遗迹”，想升级一个包却发现牵一发而动全身，最后只能无奈放弃。</p>
<p>又或许，你只是想在公司内网拉取一个私有库，却被 GOPROXY、GOPRIVATE、GONOSUMDB 这“三兄弟”搞得晕头转向，在 404 和 410 的报错中反复挣扎。</p>
<p>如果这些场景让你会心一笑（或者苦笑），那么，欢迎来到我们的世界。</p>
<p><strong>Go 模块的构建与依赖管理，就像我们呼吸的空气。</strong> 它无处不在，支撑着我们所有的Go开发活动。但正因为它如此基础，我们常常满足于“能用就行”，而忽略了其背后深刻的设计哲学和强大的工程能力。直到有一天，我们被一个棘手的构建问题拦住去路，才发现自己对这套最熟悉的工具，其实知之甚少。</p>
<h2>为什么我要写这个微专栏？</h2>
<p>市面上关于 Go Modules 的文章很多，但大多是“点状”的：教你一个命令，解决一个问题。但我发现，很少有内容能系统性地回答那几个更深层次的“<strong>为什么</strong>”：</p>
<ul>
<li>Go 为什么会放弃 GOPATH，经历 vendor、dep 的探索，最终选择了 Go Modules 这条路？这背后是怎样的历史和权衡？</li>
<li>最小版本选择（MVS）算法到底是什么？它和 npm/pip 的逻辑有何本质不同，为什么说它带来了“高保真”的可重现的构建？</li>
<li>go.mod 里的 go 1.21 和 toolchain go1.22.0 到底是什么关系？它们是如何维系 Go 强大的兼容性承诺的？</li>
<li>GOPROXY 背后那套简单的 HTTP 协议是什么样的？理解了它，我们就能自己动手模拟一次 go get 的全过程。</li>
<li>像Kubernetes这样的大型Go项目是如何进行Go module依赖管理和构建的？有什么值得我们借鉴的地方？</li>
</ul>
<p>这些问题，才是我认为真正能让我们“<strong>从入门到精通</strong>”的关键。</p>
<h2>在这个专栏里，你将得到什么？</h2>
<p>为此，我花了数月时间，整理、实践、并最终策划了这门<strong>《Go 模块构建与依赖管理: 从入门到精通》</strong>的微专栏。</p>
<p>这是一份写给所有 Gopher 的Go构建体系“圣经”，旨在帮助大家彻底搞懂 Go 的“包”罗万象 。我们将：</p>
<ul>
<li><strong>从历史的源头出发</strong>，回顾 Go 依赖管理的演进史，建立完整的认知。</li>
<li><strong>深入核心原理</strong>，彻底搞懂go.mod、MVS、go.sum 和兼容性机制。</li>
<li><strong>精通所有工具</strong>，从 go get、go mod tidy 到 replace、exclude，再到 本地多模块开发 神器 go.work。</li>
<li><strong>覆盖作者和使用者双工作流</strong>，从模块作者的创建与发布(v1)、发布补丁/次要版本、发布主版本（v2+)，到模块使用者的依赖添加与升级、降级与移除。</li>
<li><strong>驾驭复杂场景</strong>，无论是私有仓库、带有cgo/asm的混合构建，还是将 Go 编译成静态库、动态库或Plugin插件。</li>
<li><strong>解剖顶级案例</strong>，看看 Kubernetes 这种巨型项目，是如何管理其“天文数字”般的依赖的。</li>
<li><strong>终结所有“天坑”</strong>，我会把我踩过的所有坑、总结的所有排错技巧，毫无保留地分享给你。</li>
</ul>
<p><strong>以下是本专栏的完整大纲（共 13 讲）：</strong></p>
<p><strong>模块一：历史与原理 (建立认知)</strong></p>
<ol>
<li>前世今生：从 GOPATH 的“混乱”到 Go Modules 的“秩序”</li>
<li>Go Modules 核心原理：go.mod, go.sum 与最小版本选择 (MVS)</li>
<li>兼容性的承诺：深入 go 与 toolchain 指令</li>
</ol>
<p><strong>模块二：工作流与高级操作 (精通工具)</strong></p>
<ol>
<li>日常操作精通：get, tidy, list 三剑客</li>
<li>模块的生命周期：作者与使用者的工作流</li>
<li>依赖关系“手术刀”：replace, exclude 与 retract</li>
<li>告别 replace 泥潭：go.work 与多模块开发</li>
</ol>
<p><strong>模块三：企业级与复杂场景 (驾驭复杂)</strong></p>
<ol>
<li>深入 Go Module Proxy 协议</li>
<li>企业级实践：私有仓库与私有 Proxy</li>
<li>跨越边界：cgo 与 asm 的构建之道</li>
<li>构建模式的魔力: 从静态库、动态库到 Go 插件</li>
</ol>
<p><strong>模块四：案例与排错 (升华与实战)</strong></p>
<ol>
<li>实战解剖：Kubernetes 是如何管理上千个依赖的？</li>
<li>终章：常见构建“天坑”与终极排错指南</li>
</ol>
<h2>小结</h2>
<p>我相信，这是<strong>全网第一份</strong>如此系统、全面、深入 Go 构建与依赖管理体系的中文资料。</p>
<p>如果你也曾被这些问题“折腾”过，如果你也渴望一次性地、系统性地掌握这门 Gopher 的“必修内功”，那么，我诚挚地邀请你加入这场学习之旅。</p>
<p>让我们一起，告别“知其然”，真正做到“知其所以然”，<strong>彻底搞懂 Go 的模块构建与依赖管理</strong>。</p>
<p>点击<a href="https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzIyNzM0MDk0Mg==&amp;action=getalbum&amp;album_id=4225702928272949254#wechat_redirect">阅读全文</a>/扫描下方二维码，立即订阅《Go 模块构建与依赖管理: 从入门到精通》，开启你的全面且有深度的探索之旅！</p>
<p><img src="https://tonybai.com/wp-content/uploads/2025/paid/the-ultimate-guide-to-go-module-qr.png" alt="" /></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 src="https://tonybai.com/wp-content/uploads/course-card/iamtonybai-banner-2.gif" alt="" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/10/27/the-ultimate-guide-to-go-module/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go 1.22新特性前瞻</title>
		<link>https://tonybai.com/2023/12/25/go-1-22-foresight/</link>
		<comments>https://tonybai.com/2023/12/25/go-1-22-foresight/#comments</comments>
		<pubDate>Mon, 25 Dec 2023 13:58:12 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[append]]></category>
		<category><![CDATA[Array]]></category>
		<category><![CDATA[bisect]]></category>
		<category><![CDATA[Channel]]></category>
		<category><![CDATA[Compiler]]></category>
		<category><![CDATA[dep]]></category>
		<category><![CDATA[devirtualize]]></category>
		<category><![CDATA[for]]></category>
		<category><![CDATA[for-range]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[go1.21]]></category>
		<category><![CDATA[go1.22]]></category>
		<category><![CDATA[GODEBUG]]></category>
		<category><![CDATA[GOEXPERIMENT]]></category>
		<category><![CDATA[goimports]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[gopls]]></category>
		<category><![CDATA[gorilla]]></category>
		<category><![CDATA[govet]]></category>
		<category><![CDATA[Go语言第一课]]></category>
		<category><![CDATA[Go语言精进之路]]></category>
		<category><![CDATA[HandleFunc]]></category>
		<category><![CDATA[host]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[httprouter]]></category>
		<category><![CDATA[Inline]]></category>
		<category><![CDATA[iteration]]></category>
		<category><![CDATA[Iterator]]></category>
		<category><![CDATA[loop]]></category>
		<category><![CDATA[loopvar]]></category>
		<category><![CDATA[map]]></category>
		<category><![CDATA[math]]></category>
		<category><![CDATA[mux]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[PGO]]></category>
		<category><![CDATA[playground]]></category>
		<category><![CDATA[rand]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[RussCox]]></category>
		<category><![CDATA[ServeMux]]></category>
		<category><![CDATA[Slice]]></category>
		<category><![CDATA[slog]]></category>
		<category><![CDATA[stdlib]]></category>
		<category><![CDATA[sync]]></category>
		<category><![CDATA[syscall]]></category>
		<category><![CDATA[TCPConn]]></category>
		<category><![CDATA[toolchain]]></category>
		<category><![CDATA[Trace]]></category>
		<category><![CDATA[Tracer]]></category>
		<category><![CDATA[unique]]></category>
		<category><![CDATA[UnixConn]]></category>
		<category><![CDATA[vendor]]></category>
		<category><![CDATA[version]]></category>
		<category><![CDATA[waitgroup]]></category>
		<category><![CDATA[zerocopy]]></category>
		<category><![CDATA[主机]]></category>
		<category><![CDATA[优化]]></category>
		<category><![CDATA[兼容性]]></category>
		<category><![CDATA[内联]]></category>
		<category><![CDATA[切片]]></category>
		<category><![CDATA[包]]></category>
		<category><![CDATA[工具链]]></category>
		<category><![CDATA[循环]]></category>
		<category><![CDATA[数组]]></category>
		<category><![CDATA[标准库]]></category>
		<category><![CDATA[索引]]></category>
		<category><![CDATA[编译器]]></category>
		<category><![CDATA[语法糖]]></category>
		<category><![CDATA[运行时]]></category>
		<category><![CDATA[迭代]]></category>
		<category><![CDATA[迭代器]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4087</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2023/12/25/go-1-22-foresight 美国时间2023年12月20日，Go官方宣布Go 1.22rc1发布，开启了为期2个多月的、常规的公测之旅，Go 1.22预计将于2024.2月份正式发布！ 除了在官网下载Go 1.22rc1版本进行新特性体验之外，我们还可以通过在线的Go Playground选择“Go dev branch”来体验(相比下载安装，在线版本体验会有一些局限)： 注：关于Go的多种安装方法，《Go语言第一课》专栏有系统全面的讲解，欢迎订阅阅读。 本文将和大家一起看看Go 1.22都会带来哪些新特性。不过由于目前为时尚早，下面列出的有些变化最终不一定能进入到Go 1.22的最终版本中，所以切记一切变更点要以最终Go 1.22版本发布时为准。 1. 语言变化 Go 1.22的语言特性变化主要是围绕for loop的。 1.1 loopvar试验特性转正 在Go 1.21版本中，作为试验特性loopvar在Go 1.22中正式转正。如果你还不知道这个特性是啥，我们来看一下下面这个最能说明问题的示例： // go1.22-foresight/lang/for-range/for_range.go package main import ( "fmt" "sync" ) func main() { sl := []int{11, 12, 13, 14, 15} var wg sync.WaitGroup for i, v := range sl { [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/go-1-22-foresight-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2023/12/25/go-1-22-foresight">本文永久链接</a> &#8211; https://tonybai.com/2023/12/25/go-1-22-foresight</p>
<p>美国时间2023年12月20日，<a href="https://groups.google.com/g/golang-announce/c/FIUY9kd7fc0">Go官方宣布Go 1.22rc1发布</a>，开启了为期2个多月的、常规的公测之旅，Go 1.22预计将于2024.2月份正式发布！</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-1-22-foresight-2.png" alt="" /></p>
<p>除了<a href="https://go.dev/dl/#go1.22rc1">在官网下载Go 1.22rc1版本</a>进行新特性体验之外，我们还可以通过<a href="https://go.dev/play/">在线的Go Playground</a>选择“Go dev branch”来体验(相比下载安装，在线版本体验会有一些局限)：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-1-22-foresight-3.png" alt="" /></p>
<blockquote>
<p>注：关于Go的多种安装方法，<a href="http://gk.link/a/10AVZ">《Go语言第一课》</a>专栏有系统全面的讲解，欢迎订阅阅读。</p>
</blockquote>
<p>本文将和大家一起看看Go 1.22都会带来哪些新特性。不过由于目前为时尚早，下面列出的有些变化最终不一定能进入到Go 1.22的最终版本中，所以切记一切变更点要以最终Go 1.22版本发布时为准。</p>
<h2>1. 语言变化</h2>
<p>Go 1.22的语言特性变化主要是<strong>围绕for loop的</strong>。</p>
<h3>1.1 loopvar试验特性转正</h3>
<p>在<a href="https://tonybai.com/2023/08/20/some-changes-in-go-1-21">Go 1.21版本</a>中，作为试验特性loopvar在Go 1.22中正式转正。如果你还不知道这个特性是啥，我们来看一下下面这个最能说明问题的示例：</p>
<pre><code>// go1.22-foresight/lang/for-range/for_range.go

package main

import (
    "fmt"
    "sync"
)

func main() {
    sl := []int{11, 12, 13, 14, 15}
    var wg sync.WaitGroup
    for i, v := range sl {
        wg.Add(1)
        go func() {
            fmt.Printf("%d : %d\n", i, v)
            wg.Done()
        }()
    }
    wg.Wait()
}
</code></pre>
<p>我们分别用Go 1.22rc1和Go 1.21.0来运行上面这段代码：</p>
<pre><code>// 使用go 1.22rc1的运行结果：

$go run for_range.go
4 : 15
1 : 12
0 : 11
3 : 14
2 : 13

// 使用go 1.21.0的运行结果：
$go run for_range.go
4 : 15
4 : 15
4 : 15
4 : 15
4 : 15
</code></pre>
<p>之所以存在差异，是因为Go 1.22版本开始，for range语句中声明的循环变量（比如这里的i和v）不再是整个loop一份(loop var per loop)，而是每次iteration都会有自己的变量(loop var per-iteration)，这样在Go 1.22中，for range中的goroutine启动的<a href="https://tonybai.com/2021/08/09/when-variables-captured-by-closures-are-recycled-in-go/">闭包函数</a>中捕获的变量是loop var per-iteration，这样才会输出5个不同的索引值和对应的切片值。</p>
<blockquote>
<p>注：关于Go 1.22版本之前的for range的坑，<a href="http://gk.link/a/10AVZ">《Go语言第一课》</a>专栏有图文并茂的原理讲解，欢迎订阅阅读。</p>
</blockquote>
<p>那传统的3-clause的for loop呢？其中的循环变量的语义是否也发生变化了呢？我们看下面示例：</p>
<pre><code>// go1.22-foresight/lang/for-range/classic_for_loop.go

package main

import (
    "fmt"
    "sync"
)

func main() {
    sl := []int{11, 12, 13, 14, 15}
    var wg sync.WaitGroup
    for i := 0; i &lt; len(sl); i++ {
        wg.Add(1)
        go func() {
            v := sl[i]
            fmt.Printf("%d : %d\n", i, v)
            wg.Done()
        }()
    }
    wg.Wait()
}
</code></pre>
<p>我们依然分别用Go 1.22rc1和Go 1.21.0版本运行这段代码，得到的结果如下：</p>
<pre><code>// 使用go 1.22rc1的运行结果：

$go run classic_for_loop.go
0 : 11
4 : 15
2 : 13
3 : 14
1 : 12

// 使用go 1.21.0的运行结果：

$go run classic_for_loop.go
panic: runtime error: index out of range [5] with length 5

goroutine 20 [running]:
main.main.func1()
    /Users/tonybai/test/go/go1.22-foresight/lang/for-range/classic_for_loop.go:14 +0xc9
created by main.main in goroutine 1
    /Users/tonybai/test/go/go1.22-foresight/lang/for-range/classic_for_loop.go:13 +0x7f
panic: runtime error: index out of range [5] with length 5

goroutine 19 [running]:
main.main.func1()
    /Users/tonybai/test/go/go1.22-foresight/lang/for-range/classic_for_loop.go:14 +0xc9
created by main.main in goroutine 1
    /Users/tonybai/test/go/go1.22-foresight/lang/for-range/classic_for_loop.go:13 +0x7f
exit status 2
</code></pre>
<p>从输出结果来看，3-clause的for语句中声明的循环变量也变成了loop var per-iteration了。</p>
<p>在Go 1.22之前，go vet工具在遇到像上面代码那样在闭包中引用循环变量的情况时会给出警告，但由于Go 1.22的这个语义修正，go vet对于Go 1.22及以后版本(根据go.mod中的指示)的类似Go代码将不再报错。</p>
<p>不过就像Russ Cox在<a href="https://github.com/golang/go/issues/60078">spec: less error-prone loop variable scoping</a>这一issue中提及那样，该特性落地可能会带来不兼容问题，即对存量代码行为的破坏性改变。为此Go团队提供了一个<a href="https://pkg.go.dev/golang.org/x/tools/cmd/bisect">名为bisect的工具</a>，该工具可以检测出存量代码在for loop语义发生变更后是否会导致问题。不过该工具似乎只能与go test一起使用，也就是说你只能对那些被<a href="https://tonybai.com/2023/07/16/the-guide-of-go-testing-with-testify-package/">Go测试</a>覆盖到的for loop进行检测。</p>
<p>目前<a href="https://github.com/golang/go/issues/60078">spec: less error-prone loop variable scoping</a>这一issue还处于open状态，也没有放入Go 1.22 milestone中，不知道后续是否还会存在变数！</p>
<h3>1.2 range支持整型表达式</h3>
<p>在Go 1.22版本中，for range后面的range表达式除了支持传统的像数组、切片、map、channel等表达式外，<strong>还支持放置整型表达式</strong>，比如下面这个例子：</p>
<pre><code>// lang/range-expr-support-integer/main.go

func main() {
    n := 5
    for i := range n {
        fmt.Println(i)
    }
}
</code></pre>
<p>我们知道：for range会在执行伊始对range表达式做一次求值，这里对n求值结果为5。按照新增的for range后接整型表达式的语义，对于整数值n，for range每次迭代值会从0到n-1按递增顺序进行。上面代码中的for range会从0迭代到4(5-1)，我们执行一下上述代码就可以印证这一点：</p>
<pre><code>$go run main.go
0
1
2
3
4
</code></pre>
<p>如果n &lt;= 0，则循环不运行任何迭代。</p>
<p>这个新语法特性，可以理解为是一种“语法糖”，是下面等价代码的“语法糖”：</p>
<pre><code>for i := 0; i &lt; 5; i++ {
    fmt.Println(i)
}
</code></pre>
<p>不过，迭代总是从0开始，似乎限制了该语法糖的使用范围。</p>
<h3>1.3 试验特性：range-over-function iterators</h3>
<p>在for range支持整型表达式的时候，<a href="https://github.com/golang/go/issues/61405">Go团队也考虑了增加函数迭代器(iterator)</a>，不过前者语义清晰，实现简单。后者展现形式、语义和实现都非常复杂，于是在Go 1.22中，函数迭代器以试验特性提供，通过GOEXPERIMENT=rangefunc可以体验该功能特性。</p>
<p>在没有函数迭代器之前，我们实现一个通用的反向迭代切片的函数可能是像这样：</p>
<pre><code>// lang/range-over-function-iterator/backward_iterate_slice_old.go

func Backward(s []E) func(func(int, E) bool) {
    return func(yield func(int, E) bool) {
        for i := len(s)-1; i &gt;= 0; i-- {
            if !yield(i, s[i]) {
                return
            }
        }
        return
    }
}
</code></pre>
<p>下面是在Go 1.21.0版本中使用上面Backward函数的方式：</p>
<pre><code>// lang/range-over-function-iterator/backward_iterate_slice_old.go

func main() {
    sl := []string{"hello", "world", "golang"}
    Backward(sl)(func(i int, s string) bool {
        fmt.Printf("%d : %s\n", i, s)
        return true
    })
}
</code></pre>
<p>我们用Go 1.21.0运行一下上述示例：</p>
<pre><code>$go run backward_iterate_slice_old.go
2 : golang
1 : world
0 : hello
</code></pre>
<p>在以前版本中，这种对切片、数组或map中进行元素迭代的情况在实际开发中非常常见，也比较模式化，但基于目前语法，使用起来非常不便。于是Go团队提出将它们<a href="https://github.com/golang/go/issues/61405">与for range结合在一起的提案</a>。有了range-over-function iterator机制后，我们就可以像下面这样使用Backward泛型函数了：</p>
<pre><code>// lang/range-over-function-iterator/backward_iterate_slice_new.go

func main() {
    sl := []string{"hello", "world", "golang"}
    for i, s := range Backward(sl) {
        fmt.Printf("%d : %s\n", i, s)
    }
}
</code></pre>
<p>相比于上面的老版本代码，这也的代码更简洁清晰了，使用Go 1.22rc1的运行结果也与老版本别无二致：</p>
<pre><code>$GOEXPERIMENT=rangefunc  go run backward_iterate_slice_new.go
2 : golang
1 : world
0 : hello
</code></pre>
<p>但代价就是要理解什么样原型的函数才能与for range一起使用实现函数迭代，这的确有些复杂，本文就不展开说了，有兴趣的童鞋可以先看看有关<a href="https://go.dev/wiki/RangefuncExperiment">range-over-function iterator的wiki</a>先行了解一下。</p>
<h2>2. 编译器、运行时与工具链</h2>
<h3>2.1 继续增强<a href="https://github.com/golang/go/issues/61577">PGO优化</a></h3>
<p>自<a href="https://tonybai.com/2023/02/08/some-changes-in-go-1-20">Go 1.20版本引入PGO</a>(profile-guided optimization)后，PGO这种优化技术带来的优化效果就得到了持续的提升：Go 1.20实测性能提升仅为1.05%；<a href="https://tonybai.com/2023/08/20/some-changes-in-go-1-21">Go 1.21版本发布</a>时，官方的数据是2%~7%，而Go 1.21编译器自身在PGO优化过后编译速度提升约6%。</p>
<p>在Go 1.22中，官方给出的数字则是2%~14%，这14%的提升想必是来自Google内部的某个实际案例。</p>
<h3>2.2  inline和devirtualize</h3>
<p>在Go 1.22中，Go编译器可以更灵活的运用devirtualize和inline对代码进行优化了。</p>
<p>在面向对象的编程中，虚拟函数是一种在运行时动态确定调用的函数。当调用虚拟函数时，编译器通常会为其生成一段额外的代码，用于在运行时确定要调用的具体函数。这种动态调度的机制使得程序可以根据实际对象类型来执行相应的函数，但也带来了一定的性能开销。通过devirtualize优化技术，编译器会尝试在编译时确定调用的具体函数，而不是在运行时进行动态调度。这样可以避免运行时的开销，并<strong>允许编译器进行更多的优化</strong>。</p>
<p>对应到Go来说，就是在编译阶段<strong>将使用接口进行的方法调用转换为通过接口的实际类型的实例直接调用该方法</strong>。</p>
<blockquote>
<p>注：我的《<a href="https://item.jd.com/13694000.html">Go语言精进之路</a>》一书中有对通过接口调用方法的原理的详尽说明，欢迎阅读。</p>
</blockquote>
<p>关于内联优化，今年Austin Clements发起了<a href="https://github.com/golang/go/issues/61502">inline大修项目</a>，对Go编译器中的内联优化过程进行全面调整，目标是在Go 1.22中拥有更有效的、具有启发能力的内联，为后续内联的进一步增强奠定基础。该大修的成果目前以GOEXPERIMENT=newinliner试验特性的形式在Go 1.22中提供。</p>
<h3>2.3 运行时</h3>
<p>运行时的变化主要还是来自<a href="https://tonybai.com/2023/06/13/understand-go-gc-overhead-behind-the-convenience/">GC</a>。</p>
<p>Go 1.22中，运行时会将基于类型的垃圾回收的元数据放在每个堆对象附近，从而可以将Go程序的CPU性能提高1-3%。同时，通过减少重复的元数据的优化，内存开销也将降低约1%。不确定减少重复元数据(metadata)这一优化是否<a href="https://github.com/golang/go/issues/62483#issuecomment-1800913220">来自对unique包的讨论</a>。</p>
<h3>2.4 工具链</h3>
<p>在Go工具链改善方面，首当其冲的要数go module相关工具了。</p>
<p>在Go 1.22中，go work增加了一个与go mod一致的特性：支持vendor。通过go work vendor，可以将workspace中的依赖放到vendor目录下，同时在构建时，如果module root下有vendor目录，那么默认的构建是go build -mod=vendor，即基于vendor的构建。</p>
<p>go mod init在Go 1.22中将不再考虑GOPATH时代的包依赖工具的配置文件了，比如Gopkg.lock。在Go 1.22版本之前，如果go module之前使用的是类似<a href="https://tonybai.com/2017/06/08/first-glimpse-of-dep/">dep这样的工具来管理包依赖</a>，go mod init会尝试读取dep配置文件来生成go.mod。</p>
<p>go vet工具取消了对loop变量引用的警告，增加了对空append的行为的警告(比如：slice = append(slice))、增加了deferring time.Since的警告以及在log/slog包的方法调用时key-value pair不匹配的警告。</p>
<h2>3. 标准库</h2>
<p>最后，我们来看看标准库的变化。每次Go发布新版本，标准库都是占更新的大头儿，这里无法将所有变更点一一讲解，仅说说几个重要的变更点。</p>
<h3>3.1 增强http.ServerMux表达能力</h3>
<p>Go内置电池，从诞生伊始就内置了强大的http库，不过长期以来http原生的ServeMux表达能力比较单一，不支持通配符等，这也是Go社区长期以来一直使用像<a href="https://github.com/gorilla/mux/">gorilla/mux</a>、<a href="https://github.com/julienschmidt/httprouter">httprouter</a>等第三方路由库的原因。</p>
<p>今年log/slog的作者Jonathan Amsterdam又创建了新的提案：<a href="https://github.com/golang/go/issues/61410">net/http: enhanced ServeMux routing</a>，提高http.ServeMux的表达能力。在新提案中，<a href="https://pkg.go.dev/net/http@go1.22rc1#ServeMux">新的ServeMux将支持如下路由策略</a>(来自http.ServeMux的官方文档)：</p>
<ul>
<li>“/index.html”路由将匹配任何主机和方法的路径”/index.html”；</li>
<li>“GET /static/”将匹配路径以”/static/”开头的GET请求；</li>
<li>“example.com/”可以与任何指向主机为”example.com”的请求匹配；</li>
<li>“example.com/{$}”会匹配主机为”example.com”、路径为”/”的请求，即”example.com/”；</li>
<li>“/b/{bucket}/o/{objectname&#8230;}”匹配第一段为”b”、第三段为”o”的路径。名称”bucket”表示第二段，”objectname”表示路径的其余部分。</li>
</ul>
<p>下面就是基于上面的规则编写的示例代码：</p>
<pre><code>// lib/servemux/main.go

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/index.html", func(w http.ResponseWriter, req *http.Request) {
        fmt.Fprintln(w, `match /index.html`)
    })
    mux.HandleFunc("GET /static/", func(w http.ResponseWriter, req *http.Request) {
        fmt.Fprintln(w, `match "GET /static/"`)
    })
    mux.HandleFunc("example.com/", func(w http.ResponseWriter, req *http.Request) {
        fmt.Fprintln(w, `match "example.com/"`)
    })
    mux.HandleFunc("example.com/{$}", func(w http.ResponseWriter, req *http.Request) {
        fmt.Fprintln(w, `match "example.com/{$}"`)
    })
    mux.HandleFunc("/b/{bucket}/o/{objectname...}", func(w http.ResponseWriter, req *http.Request) {
        bucket := req.PathValue("bucket")
        objectname := req.PathValue("objectname")
        fmt.Fprintln(w, `match /b/{bucket}/o/{objectname...}`+":"+"bucket="+bucket+",objectname="+objectname)
    })

    http.ListenAndServe(":8080", mux)
}
</code></pre>
<p>我们使用curl对上述示例进行一个测试(前提是在/etc/hosts中设置example.com为127.0.0.1)：</p>
<pre><code>$curl localhost:8080/index.html
match /index.html

$curl example.com:8080/static/abc
match "example.com/"

$curl localhost:8080/static/abc
match "GET /static/"

$curl example.com:8080/
match "example.com/{$}"

$curl example.com:8080/b/mybucket/o/myobject/tonybai
match "example.com/"

$curl localhost:8080/b/mybucket/o/myobject/tonybai
match /b/{bucket}/o/{objectname...}:bucket=mybucket,objectname=myobject/tonybai
</code></pre>
<p>从测试情况来看，不同路由设置之间存在交集，这就需要路由匹配优先级规则。新版Go ServeMux规定：如果一个请求有两个或两个以上的模式匹配，则更具体(specific)的模式优先。如果P1符合P2请求的严格子集，也就是说，如果P2符合P1及更多的所有请求，那么P1就比P2更具体。</p>
<p>举个例子：”/images/thumbnails/”比”/images/”更具体，因此两者都可以注册。前者匹配以”/images/thumbnails/”开头的路径，后者则匹配”/images/”子树中的任何其他路径。</p>
<p>如果两者都不更具体，那么模式就会发生冲突。为了向后兼容，这一规则有一个例外：如果两个模式发生冲突，而其中一个模式有主机(host)，另一个没有，那么有主机的模式优先(比如上面测试中的第二次curl执行)。如果通过ServeMux.Handle或ServeMux.HandleFunc设置的模式与另一个已注册的模式发生冲突，这些函数就会panic。</p>
<p>增强后的ServeMux可能会影响向后兼容性，使用GODEBUG=httpmuxgo121=1可以保留原先的ServeMux行为。</p>
<h3>3.2 增加math/rand/v2包</h3>
<p>在日常开发中，我们多会在生成随机数的场景下使用math/rand包，其他时候使用的较少。但Go 1.22中新增了math/rand/v2包，我之所以将这个列为Go 1.22版本标准库的一次重要变化，是因为这是标准库第一次为某个包建立v2版本包，<a href="https://github.com/golang/go/issues/61716">按照Russ Cox的说法</a>，这次v2包的创建，为标准库中的其他可能的v2包树立了榜样。创建math/rand/v2可以使Go团队能够在一个相对不常用且风险较低的包中解决工具问题（如gopls、goimports等对v2包的支持），然后再转向更常用、风险更高的包，如sync/v2或encoding/json/v2等。</p>
<p><a href="https://github.com/golang/go/issues/61716">新增rand/v2包的直接原因</a>是清理math/rand并修复其中许多悬而未决的问题，特别是使用过时的生成器、慢速算法以及与crypto/rand冲突的问题，这里就不针对v2包举具体的示例了，对该包感兴趣的同学可以自行阅读该包的在线文档，并探索如何使用v2包。</p>
<p>同时，该提案也为标准库中的v2包的创建建立了一种模式，即v2包是原始包的子目录，并且以原始包的API为起点，每个偏离点都要有明确的理由。</p>
<p>想当初，go module刚落地到Go中时，Go module支持两种识别major的两种方式，一种是通过branch或tag号来识别，另外一种就是利用vN目录来定义新包。当时还不是很理解为什么要有vN目录这种方式，现在从math/rand/v2包的增加来看，足以体现出当初module设计时的前瞻性考量了。</p>
<h3>3.3 <a href="https://github.com/golang/go/issues/60773">大修Go execution tracer</a></h3>
<p>Go Execution Tracer是解决Go应用性能方面“疑难杂症”的杀手锏级工具，它可以提供Go程序在一段时间内发生的情况的即时视图。这些信息对于了解程序随时间推移的行为非常宝贵，可辅助开发人员对应用进行性能改进。我曾在《<a href="https://tonybai.com/2021/06/28/understand-go-execution-tracer-by-example">通过实例理解Go Execution Tracer</a>》中对其做过系统的说明。</p>
<p>不过当前版本的Go Execution Tracer在原理和使用方面还存在诸多问题，Google的Michael Knyszek在年初发起了<a href="https://github.com/golang/go/issues/60773">Execution tracer overhaul的提案</a>，旨在对Go Execution Tracer进行改进，使Go Execution Tracer可扩展到大型Go部署的Go执行跟踪。具体目标如下：</p>
<ul>
<li>使跟踪解析所需的内存占用量仅为当前的一小部分。</li>
<li>支持可流式传输的跟踪，以便在无需存储的情况下进行分析。</li>
<li>实现部分自描述的跟踪，以减少跟踪消费者的升级负担。</li>
<li>修复长期存在的错误，并提供一条清理实现的路径。</li>
</ul>
<p>在近一年的时间里，Knyszek与Felix Geisendorfer、Nick Ripley、Michael Pratt等一起实现了该提案的目标。</p>
<p>鉴于篇幅，这里就不对新版Tracer的使用做展开说明，有兴趣的童鞋可结合《<a href="https://tonybai.com/2021/06/28/understand-go-execution-tracer-by-example">通过实例理解Go Execution Tracer</a>》中的使用方法自行体验新版Tracer。</p>
<blockquote>
<p>注：<a href="https://go.googlesource.com/proposal/+/ac09a140c3d26f8bb62cbad8969c8b154f93ead6/design/60773-execution-tracer-overhaul.md">新版Tracer的设计文档</a> &#8211; https://go.googlesource.com/proposal/+/ac09a140c3d26f8bb62cbad8969c8b154f93ead6/design/60773-execution-tracer-overhaul.md</p>
</blockquote>
<h3>3.4 其他</h3>
<ul>
<li>“出尔反尔” &#8211; <a href="https://github.com/golang/go/issues/60797">syscall包：取消弃用(undeprecate)</a></li>
</ul>
<p>自<a href="https://tonybai.com/2014/11/04/some-changes-in-go-1-4/">Go 1.4版本</a>以来，syscall包新特性就已经被冻结，并在<a href="https://tonybai.com/2018/11/19/some-changes-in-go-1-11/">Go 1.11版本</a>中被标记为不推荐使用(deprecate)。Go团队推荐gopher使用golang.org/x/sys/unix或golang.org/x/sys/windows。syscall包的大多数功能都能被golang.org/x/sys包替代，除了下面这几个：</p>
<pre><code>syscall.SysProcAttr（类型os/exec.Cmd.SysProcAttr)
syscall.Signal（参考文献os.Signal)
syscall.WaitStatus（参考文献os.(*ProcessState).Sys)
syscall.Stat_t
... ...
</code></pre>
<p>由于syscall包已经弃用，IDE等工具在开发人员使用上述内容时总是得到警告！这引发了众多开发人员的抱怨。为此，在Go 1.22版本中，syscall取消了弃用状态，但其功能特性依旧保持冻结，不再添加新特性。</p>
<ul>
<li>TCPConn to UnixConn：支持zerocopy </li>
</ul>
<p><a href="https://tonybai.com/2021/07/31/io-multiplexing-model-tcp-stream-protocol-parsing-practice-in-go/">gnet</a>作者Andy Pan的提案：<a href="https://github.com/golang/go/issues/58808">TCPConn to UnixConn：支持zerocopy</a>在Go 1.22落地，具体内容可以看一下<a href="https://github.com/golang/go/issues/58808">原始提案issue</a>。</p>
<ul>
<li>新增go/version包</li>
</ul>
<p>在Go 1.21版本发布后，Go团队对Go语言的版本规则做了调整，并明确了<a href="https://tonybai.com/2023/09/10/understand-go-forward-compatibility-and-toolchain-rule/">Go语言的向前兼容性和toolchain规则</a>，Go 1.22中增加go/version包实现了按照上述版本规则的Go version判断，这个包既用于go工具链，也可以用于Gopher自行开发的工具中。</p>
<h2>4. 小结</h2>
<p>Go 1.22版本具有至少两点重要的里程碑意义：</p>
<ul>
<li>通过对loopvar语义的修正，开启了Go已有“语法坑”的fix之路</li>
<li>通过math/rand/v2包树立了Go标准库建立vN版本的模式</li>
</ul>
<p>“语法坑”fix是否能得到社区正向反馈还是一个未知数，其导致的兼容性问题势必会成为Go社区在升级到Go 1.22版本的重要考虑因素，即便决定升级到Go 1.22，严格的代码审查和测试也是必不可少的。</p>
<p>最后，感谢Go团队以及所有Go 1.22贡献者做出的伟大工作！</p>
<p>文本涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/go1.22-foresight">这里</a>下载。</p>
<h2>5. 参考资料</h2>
<p>-<a href="https://github.com/golang/go/milestone/298">Go 1.22 Milestone</a> &#8211; https://github.com/golang/go/milestone/298</p>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2023, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2023/12/25/go-1-22-foresight/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Go语言有哪些“劣势”</title>
		<link>https://tonybai.com/2020/12/24/the-disadvantages-of-go/</link>
		<comments>https://tonybai.com/2020/12/24/the-disadvantages-of-go/#comments</comments>
		<pubDate>Thu, 24 Dec 2020 05:00:33 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Android]]></category>
		<category><![CDATA[BFE]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Cpp]]></category>
		<category><![CDATA[DaveCheney]]></category>
		<category><![CDATA[dep]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomobile]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[InfluxDB]]></category>
		<category><![CDATA[iOS]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[JS]]></category>
		<category><![CDATA[Module]]></category>
		<category><![CDATA[mvs]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[RobertGriesemer]]></category>
		<category><![CDATA[RobPike]]></category>
		<category><![CDATA[RussCox]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[TiDB]]></category>
		<category><![CDATA[vendor]]></category>
		<category><![CDATA[vgo]]></category>
		<category><![CDATA[WebAssembly]]></category>
		<category><![CDATA[zhihu]]></category>
		<category><![CDATA[包]]></category>
		<category><![CDATA[垃圾回收]]></category>
		<category><![CDATA[最小版本选择]]></category>
		<category><![CDATA[泛型]]></category>
		<category><![CDATA[知乎]]></category>
		<category><![CDATA[类型参数]]></category>
		<category><![CDATA[语义导入版本]]></category>
		<category><![CDATA[错误处理]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3046</guid>
		<description><![CDATA[本文源于笔者对知乎上的一个问题“Go有哪些劣势？”(https://www.zhihu.com/question/300163211)的一次回答(https://www.zhihu.com/question/300163211/answer/1632229924)。当时随手花几分钟在手机上写了一些点。但事后我觉得应该再做一些系统地思考。在这里我就将更系统地思考后的答案整理并分享给大家。 关于Go语言，我是喜欢的，甚至可以算作“鼓吹者”阵营的一份子。但我一贯秉承“Go并非完美语言”这个观点来尽可能客观地看待Go。每种编程语言都有自己的劣势，Go也不例外，下面我们就来列举一下Go的那些“劣势”： 1. 技术路线选择导致的“性能劣势” 众所周知，Go是带垃圾回收的编程语言，因此不管Go的STW(Stop The World)的时间有多么短，GC的延迟有多么的小，它依然属于GC类编程语言，和Java、C#属于一个阵营，同时天然与C、C++、Rust这样的手动管理内存、没有运行时GC负担的编程语言之间划清了界线。虽然Go语言的初衷是成为系统级编程语言(关于Go语言的诞生语言演化历史，可以参考我的技术专栏文章“Go语言的前生今世” https://www.imooc.com/read/87/article/2320 )，虽然Go的运行时性能可以满足99.99%的场合的需要，虽然百度的万亿流量转发引擎BFE、时序数据库influxdb、分布式关系数据库TiDB等性能敏感的项目都选择了用Go实现，但不能否认的是在一些性能超级敏感的场合，选择Go依然要慎重。 2 坚持自己的设计哲学所带来的“表达劣势” 1) “单一”的表达方法 很多从其他语言转到Go阵营的开发人员抱怨Go能玩的花样太少，套路不多，Go之所以表现出“表达劣势”，源于其设计哲学中的一个原则：“崇尚一个事情只有一个或少数几种写法”。这个原则不符合某些开发人员炫技的心理需求，于是Go就被诟病为是资质平平的程序员才会去用的语言。 Go 1.18将加入泛型（类型参数），这算是对此类“劣势”的一个“弥补”。不过对于我们这些对Go价值观和设计哲学认同已久的Gopher而言，我们十分担心大幅提高Go表达能力的泛型将成为奇技淫巧的“滋生地”。 2) “过时”的显式的错误处理 Go语言从诞生那天起就没有像C++、Java、Python等主流编程语言那样提供基于异常（exception）的结构化try-catch-finally错误处理机制，Go的设计者们认为将异常耦合到程序控制结构中会导致代码混乱。Go提供了一种简单的基于错误值比较的错误处理机制，这“强迫”每个Go开发人员都必须显式地去关注和处理每个错误，经过显式错误处理的代码会更为健壮，也会让Go开发人员对这些代码更有信心。但这一设计哲学的坚持却被很多来自其他语言的开发者嘲笑为“过时”，被称为“半个世纪前的古老机制”。(笔者注：二十世纪70年代C语言诞生时采用的错误处理机制) Go开发团队也曾“动摇过”，Go开发团队在发布Go2计划后曾发布过多版Go错误处理的新机制草案。Go社区也针对此问题做过长时间的讨论甚至是“争吵”，知名Gopher Dave Cheney发声、Rob Pike发声，著名Go培训师、《Go语言实战》联合作者之一的威廉·肯尼迪（William Kennedy）更是在Go团队try 提案公示之后，发表了对Go社区的公开信反对try方案(更多内容可参考笔者的专栏文章“if err != nil 重复太多可以这么办”(https://www.imooc.com/read/87/article/2434)，最终坚持Go设计哲学的一派占据了上风，try提案被否决，没有加入到Go 1.13版本中！ 3. 背离主流的“小众劣势” Go早期设计的包依赖管理机制的确存在不小的“瑕疵”，这源于Google内部大单一代码仓库与基于主干的开发模型的影响。走出Google的Go语言听到了不同方面的声音，Go包管理机制长期无法满足社区的需求。于是先后出现了vendor机制、dep等对包依赖管理的改进尝试。 2018 年初，正当广大gopher们认为dep将“顺理成章”地升级为go官方工具链的一部分的时候，Go核心团队的技术负责人，也是Go 核心团队早期成员之一的Russ Cox在个人博客上连续发表了七篇文章，系统阐述了Go团队解决“包依赖管理” 的技术方案: vgo，即go module的前身。 vgo的主要思路包括：语义导入版本 (Semantic Import Versioning)、 最小版本选择 (Minimal Version Selection) ，这些都与当前主流编程语言的包依赖管理的规则相悖，尤其是最小版本选择(MVS)，算是另辟蹊径，背离主流！(更多关于go module最佳实践的内容可以参考我的专栏文章“与时俱进！使用module管理依赖包”(https://www.imooc.com/read/87/article/2476))。 4. Go核心团队的“民主集中制”导致的“社区劣势” 和Rust团队广泛采纳社区建议“猛加语言特性”不同，Go像是另外一个极端：Go核心团队对语言演化的把控力十足，不是社区多数人赞同的就一定会被采纳而加入Go语言，我这里将其戏称为“民主集中制”吧，即真正的投票权其实在Go核心团队的代表社区的少数人手中。 2018年初的dep与vgo之争就是这一“劣势”的典型表现。社区费劲一年多努力精心打造的dep项目被Russ Cox等少数人集中花掉一些时间设计出的vgo给“挤出”了Go包依赖管理工具标准的位置，成为了Go [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/the-disadvantages-of-go-1.png" alt="img{512x368}" /></p>
<p>本文源于笔者对知乎上的一个问题<a href="https://www.zhihu.com/question/300163211">“Go有哪些劣势？”</a>(https://www.zhihu.com/question/300163211)的一次<a href="https://www.zhihu.com/question/300163211/answer/1632229924">回答</a>(https://www.zhihu.com/question/300163211/answer/1632229924)。当时随手花几分钟在手机上写了一些点。但事后我觉得应该再做一些系统地思考。在这里我就将更系统地思考后的答案整理并分享给大家。</p>
<p>关于Go语言，我是喜欢的，甚至可以算作“鼓吹者”阵营的一份子。但我一贯秉承“Go并非完美语言”这个观点来尽可能客观地看待Go。每种编程语言都有自己的劣势，Go也不例外，下面我们就来列举一下Go的那些“劣势”：</p>
<h2>1. 技术路线选择导致的“性能劣势”</h2>
<p>众所周知，Go是带垃圾回收的编程语言，因此不管Go的STW(Stop The World)的时间有多么短，GC的延迟有多么的小，它依然属于GC类编程语言，和Java、C#属于一个阵营，同时天然与C、C++、Rust这样的手动管理内存、没有运行时GC负担的编程语言之间划清了界线。虽然<a href="https://www.imooc.com/read/87/article/2320">Go语言的初衷是成为系统级编程语言</a>(关于Go语言的诞生语言演化历史，可以参考我的技术专栏文章<a href="https://www.imooc.com/read/87/article/2320">“Go语言的前生今世”</a> https://www.imooc.com/read/87/article/2320 )，虽然Go的运行时性能可以满足99.99%的场合的需要，虽然百度的万亿流量<a href="https://github.com/bfenetworks/bfe">转发引擎BFE</a>、时序数据库<a href="https://github.com/influxdata/influxdb">influxdb</a>、分布式关系数据库<a href="https://github.com/pingcap/tidb">TiDB</a>等性能敏感的项目都选择了用Go实现，但不能否认的是在一些性能超级敏感的场合，选择Go依然要慎重。</p>
<h2>2 坚持自己的设计哲学所带来的“表达劣势”</h2>
<h3>1) “单一”的表达方法</h3>
<p>很多从其他语言转到Go阵营的开发人员抱怨<strong>Go能玩的花样太少，套路不多</strong>，Go之所以表现出“表达劣势”，源于其设计哲学中的一个原则：“崇尚一个事情只有一个或少数几种写法”。这个原则不符合某些开发人员炫技的心理需求，于是Go就被诟病为是<strong>资质平平的程序员才会去用的语言</strong>。</p>
<p><a href="https://mp.weixin.qq.com/s/SMT40557JgQ9FjUkswznlA">Go 1.18将加入泛型（类型参数）</a>，这算是对此类“劣势”的一个“弥补”。不过对于我们这些对Go价值观和设计哲学认同已久的Gopher而言，我们十分担心<strong>大幅提高Go表达能力的<a href="https://mp.weixin.qq.com/s/14WeOQBdezWTC5OqQrJtfg">泛型</a>将成为奇技淫巧的“滋生地”</strong>。</p>
<h3>2) “过时”的显式的错误处理</h3>
<p>Go语言从诞生那天起就没有像C++、Java、Python等主流编程语言那样提供基于异常（exception）的结构化try-catch-finally错误处理机制，Go的设计者们认为<a href="https://tip.golang.org/doc/faq#exceptions">将异常耦合到程序控制结构中会导致代码混乱</a>。Go提供了一种简单的基于错误值比较的错误处理机制，这“强迫”每个Go开发人员都必须显式地去关注和处理每个错误，经过显式错误处理的代码会更为健壮，也会让Go开发人员对这些代码更有信心。但这一设计哲学的坚持却被很多来自其他语言的开发者嘲笑为“过时”，被称为“半个世纪前的古老机制”。(笔者注：二十世纪70年代C语言诞生时采用的错误处理机制)</p>
<p>Go开发团队也曾“动摇过”，Go开发团队在发布Go2计划后曾发布过多版<a href="https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md">Go错误处理的新机制草案</a>。Go社区也针对此问题做过长时间的讨论甚至是“争吵”，知名Gopher Dave Cheney发声、Rob Pike发声，著名Go培训师、《Go语言实战》联合作者之一的威廉·肯尼迪（William Kennedy）更是在Go团队try 提案公示之后，发表了对Go社区的公开信反对try方案(更多内容可参考笔者的专栏文章<a href="https://www.imooc.com/read/87/article/2434">“if err != nil 重复太多可以这么办”</a>(https://www.imooc.com/read/87/article/2434)，最终坚持Go设计哲学的一派占据了上风，try提案被否决，没有加入到<a href="https://mp.weixin.qq.com/s/Txqvanb17LYQYgohNiUHig">Go 1.13版本</a>中！</p>
<h2>3. 背离主流的“小众劣势”</h2>
<p>Go早期设计的包依赖管理机制的确存在不小的“瑕疵”，这源于Google内部大单一代码仓库与基于主干的开发模型的影响。走出Google的Go语言听到了不同方面的声音，Go包管理机制长期无法满足社区的需求。于是先后出现了<a href="https://tonybai.com/2015/07/31/understand-go15-vendor/">vendor机制</a>、<a href="https://tonybai.com/2017/06/08/first-glimpse-of-dep/">dep</a>等对包依赖管理的改进尝试。</p>
<p>2018 年初，正当广大gopher们认为dep将“顺理成章”地升级为go官方工具链的一部分的时候，Go核心团队的技术负责人，也是Go 核心团队早期成员之一的Russ Cox在个人博客上连续发表了<a href="https://research.swtch.com/vgo">七篇文章</a>，系统阐述了Go团队解决“包依赖管理” 的技术方案: <a href="https://tonybai.com/2018/07/15/hello-go-module/">vgo</a>，即go module的前身。</p>
<p>vgo的主要思路包括：语义导入版本 (Semantic Import Versioning)、 最小版本选择 (Minimal Version Selection) ，这些都与当前主流编程语言的包依赖管理的规则相悖，尤其是<a href="https://tonybai.com/2019/12/21/go-modules-minimal-version-selection/">最小版本选择(MVS)</a>，算是另辟蹊径，背离主流！(更多关于go module最佳实践的内容可以参考我的专栏文章<a href="https://www.imooc.com/read/87/article/2476">“与时俱进！使用module管理依赖包”</a>(https://www.imooc.com/read/87/article/2476))。</p>
<h2>4. Go核心团队的“民主集中制”导致的“社区劣势”</h2>
<p>和Rust团队广泛采纳社区建议“猛加语言特性”不同，Go像是另外一个极端：Go核心团队对语言演化的把控力十足，不是社区多数人赞同的就一定会被采纳而加入Go语言，我这里将其戏称为“民主集中制”吧，即真正的投票权其实在Go核心团队的代表社区的少数人手中。</p>
<p>2018年初的dep与vgo之争就是这一“劣势”的典型表现。社区费劲一年多努力精心打造的dep项目被Russ Cox等少数人集中花掉一些时间设计出的vgo给“挤出”了Go包依赖管理工具标准的位置，成为了Go module成功的“垫脚石”。即便最终证明Go团队使用go module的决策的结果是正确的，但 这导致的Go社区与Go核心团队的“裂痕”是确确实实存在的，以致于这两年Go核心团队极力改善与Go社区的关系，规范化和透明化Go proposal的提出、review和接纳流程。</p>
<h2>5. 全面出击失败后，期望的落空导致的“功能孱弱劣势”</h2>
<p>Go 1.5发布之后，由于实现了自举和GC延迟的大幅下降，Go受关注程度逐渐升高，直至2017年初第二次拿到TIOBE年度最佳编程语言，让Go语言有些“膨胀”，甚至狂热的Go鼓吹者曾一度希望Go一统江湖：不仅牢牢把持住自己的云原生市场，占领Java的企业级市场，还要在终端(android. ios)、前端(js)上击败现有对手。</p>
<p>有人可能觉得我的上述说法可笑，但这些说法并非空穴来风。Go语言在终端、前端方面还真的曾经发过力，了解Go历史的都知道，Go团队曾经有全职开发人员参与<a href="http://golang.org/x/mobile">gomobile项目</a>(http://golang.org/x/mobile)，该项目旨在构建在Android和iOS上的Go技术栈，实现用Go语言编写终端应用的目的。</p>
<p>在前端方面，<a href="https://github.com/gopherjs/gopherjs">gopherjs项目</a>(https://github.com/gopherjs/gopherjs)可以将go代码编译为js代码并运行于各大浏览器中。后来gopherjs的作者又帮助go项目原生支持webassembly，支持将go编译为webassembly运行在浏览器中。</p>
<p>但上面的尝试最终没能“得偿如愿”，现状是在终端、前端应用领域，使用Go编码的人少之又少。于是Go又逐渐冷静下来，回到自己擅长的主力战场，回归到了企业级应用、基础设施、中间件、微服务、命令行应用等领域，并且在这些领域取得了越来越多开发者的青睐。</p>
<p>但曾经的全面出击失败给很多开发者留下了“Go功能孱弱”的口实，甚至有人说<a href="https://mp.weixin.qq.com/s/itMeNYq3qBn6tJTz3H89RA">亲爹Google</a>也没能让亲兄弟Android给Go走个后门。</p>
<h2>小结</h2>
<p>记得有人问过Go核心开发团队这样一个问题：<strong>未来Go语言演化之路上最困难的事情是什么</strong>？Go团队的回答是：<strong>使Go语言一直保持简单</strong>。</p>
<p>在本文列出的几点“劣势”中，除了第一点的性能劣势和最后两点有待商榷外，其他几点对于不爱Go的开发人员来说，这些的确都是“劣势”。但对于真正认同Go价值观和设计哲学的开发者而言，这些难道不正是Go语言的“优势”吗！</p>
<hr />
<p><strong>“Gopher部落”知识星球开球了！</strong>高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！星球首开，福利自然是少不了的！2020年年底之前，8.8折(很吉利吧^_^)加入星球，下方图片扫起来吧！</p>
<p><img src="http://image.tonybai.com/img/202011/gopher-tribe-zsxq.png" alt="" /></p>
<p>Go技术专栏“<a href="https://www.imooc.com/read/87">改善Go语⾔编程质量的50个有效实践</a>”正在慕课网火热热销中！本专栏主要满足>广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！78元简直就<br />
是白菜价，简直就是白piao! 欢迎大家订阅！</p>
<p><img src="http://image.tonybai.com/img/202011/go-column-pgo-with-qr-and-text.png" alt="" /></p>
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网热卖中，欢迎小伙伴们订阅学习！</p>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-practice-with-qr-and-text.png" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</p>
<p>2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>我的联系方式：</p>
<ul>
<li>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</li>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>微信公众号：iamtonybai</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>“Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544</li>
</ul>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2020, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2020/12/24/the-disadvantages-of-go/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>vendor目录是否需要提交到代码库中？答案全在这一篇</title>
		<link>https://tonybai.com/2020/12/03/should-you-commit-the-vendor-folder-in-go/</link>
		<comments>https://tonybai.com/2020/12/03/should-you-commit-the-vendor-folder-in-go/#comments</comments>
		<pubDate>Thu, 03 Dec 2020 05:57:51 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[cargo]]></category>
		<category><![CDATA[CD]]></category>
		<category><![CDATA[CI]]></category>
		<category><![CDATA[dep]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[glide]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.11]]></category>
		<category><![CDATA[Go1.5]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[govendor]]></category>
		<category><![CDATA[Module]]></category>
		<category><![CDATA[mvs]]></category>
		<category><![CDATA[trunk]]></category>
		<category><![CDATA[vendor]]></category>
		<category><![CDATA[vgo]]></category>
		<category><![CDATA[包依赖]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3015</guid>
		<description><![CDATA[如果您还在使用vendor机制管理依赖包，那么说明您肯定是处于下面两种情况之一！ 还工作在传统的GOPATH模式下(使用Go 1.10及之前版本；或Go 1.11及之后版本，但GO111MODULE=off)，利用vendor管理目标包的特定依赖； 工作在go module模式下，但仍然利用vendor管理目标module的特定依赖并使用go build -mod=vendor来构建。 那么我们是否应该将项目中存储依赖包的vendor目录提交到源代码仓库进行管理呢？如果让笔者给出答案，那就是：应该。 要想理解为什么“应该”，我们看看下面Go语言包依赖管理的演化过程就知道了。 Go语言在构建设计方面深受Google内部开发实践模型的影响。 Google内部基于主干的开发模型： &#8211; 所有开发人员基于主干trunk/mainline开发：提交到trunk或从trunk获取最新的代码（同步到本地workspace） &#8211; 版本发布时，建立Release branch，release branch实质上就是某一个时刻主干代码的快照； &#8211; 必须同步到release branch上的bug fix和增强改进代码也通常是先在主干上提交(commit)，然后再cherry-pick到release branch上 Go最初的构建管理以及go get就采用了基于Google内部单一代码仓库(single monorepo)和基于主干(trunk/mainline based)的开发构建模型。具体逻辑是：在Go 1.5版本之前，go get获取的都是各个Go包所在仓库的trunk/mainline的最新代码。go get会将获取的最新代码放在\$GOPATH/src下面，而go build会在\$GOROOT/src和\$GOPATH/src下面按照包导入路径(import path)去搜索这些包并执行构建操作。 我们看到1.5版本之前Go编译器都是基于目标Go程序依赖包的trunk/mainline上的最新代码去编译的，这样的机制带来的问题是显而易见的，至少包括几点： 因依赖包的trunk的变化，导致不同人获取和编译你的包/程序时得到的结果实质是不同的，即构建结果不能重现； 因依赖包的trunk的变化，引入不兼容的实现，导致你的包/程序无法通过编译； 因依赖包演进而无法通过编译，导致你的包/程序无法通过编译。 为了实现可重现的构建(reproduceable build)，Go语言于1.5版本引入了vendor机制：即Go编译器会优先在vendor目录下搜索依赖的第三方包，这样如果开发者将特定版本的依赖包存放在vendor下面并提交到代码仓库，那么所有人理论上都会得到同样的编译结果，从而实现可重现的构建。 在Go 1.5发布后的若干年，Gopher们把注意力都集中在如何利用vendor解决包依赖问题，从手工添加依赖到vendor、手工更新依赖，到一众包依赖管理工具的诞生：比如: govendor、glide以及当时号称准官方工具的dep，都在努力地尝试着按照当今主流思路解决着诸如：“钻石型依赖”等难题。 但Go核心开发团队没有走寻常路，而是另辟蹊径地在Go 1.11中引入了采用了最小版本选择(mvs)的go module。至此，Go的构建模式被一分为二：gopath mode和module-aware mode。在module-aware mode下，Go构建工具链默认不再使用传统GOPATH下或顶层vendor下面的包了，而是使用\$GOPATH/pkg/mod下面的第三方依赖Go module的local cache。理论上，go module真正实现了“可重复的构建”，我们无需再使用Go 1.5引入的vendor机制了。但社区的反馈让Go核心开发团队将module顶层目录下的vendor目录保留了下来，主要考虑vendor还能在下面场合“发光发热”： 保持Go1兼容性 可继续支持Go 1.5以后，Go 1.10之前的Go版本编译Go [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/should-you-commit-the-vendor-folder-in-go-1.png" alt="img{512x368}" /></p>
<p>如果您还在使用vendor机制管理依赖包，那么说明您肯定是处于下面两种情况之一！</p>
<ul>
<li>还工作在传统的GOPATH模式下(使用Go 1.10及之前版本；或Go 1.11及之后版本，但GO111MODULE=off)，利用vendor管理目标包的特定依赖；</li>
<li>工作在go module模式下，但仍然利用vendor管理目标module的特定依赖并使用go build -mod=vendor来构建。</li>
</ul>
<p>那么<strong>我们是否应该将项目中存储依赖包的vendor目录提交到源代码仓库进行管理呢</strong>？如果让笔者给出答案，那就是：<strong>应该</strong>。</p>
<p>要想理解为什么“应该”，我们看看下面Go语言包依赖管理的演化过程就知道了。</p>
<p>Go语言在构建设计方面深受Google内部开发实践模型的影响。</p>
<p><img src="https://tonybai.com/wp-content/uploads/google-trunk-based-and-release-branch-dev-model.png" alt="img{512x368}" /></p>
<blockquote>
<p>Google内部基于主干的开发模型：<br />
   &#8211; 所有开发人员基于主干trunk/mainline开发：提交到trunk或从trunk获取最新的代码（同步到本地workspace）<br />
   &#8211; 版本发布时，建立Release branch，release branch实质上就是某一个时刻主干代码的快照；<br />
   &#8211; 必须同步到release branch上的bug fix和增强改进代码也通常是先在主干上提交(commit)，然后再cherry-pick到release branch上</p>
</blockquote>
<p>Go最初的构建管理以及go get就采用了基于<a href="https://cacm.acm.org/magazines/2016/7/204032-why-google-stores-billions-of-lines-of-code-in-a-single-repository/pdf">Google内部单一代码仓库(single monorepo)和基于主干(trunk/mainline based)的开发构建模型</a>。具体逻辑是：在<a href="https://tonybai.com/2015/07/10/some-changes-in-go-1-5/">Go 1.5版本</a>之前，go get获取的都是各个Go包所在仓库的trunk/mainline的最新代码。go get会将获取的最新代码放在\$GOPATH/src下面，而go build会在\$GOROOT/src和\$GOPATH/src下面按照包导入路径(import path)去搜索这些包并执行构建操作。</p>
<p>我们看到1.5版本之前Go编译器都是基于目标Go程序依赖包的trunk/mainline上的最新代码去编译的，这样的机制带来的问题是显而易见的，至少包括几点：</p>
<ul>
<li>因依赖包的trunk的变化，导致不同人获取和编译你的包/程序时得到的结果实质是不同的，即构建结果不能重现；</li>
<li>因依赖包的trunk的变化，引入不兼容的实现，导致你的包/程序无法通过编译；</li>
<li>因依赖包演进而无法通过编译，导致你的包/程序无法通过编译。</li>
</ul>
<p>为了实现<strong>可重现的构建(reproduceable build)</strong>，Go语言于1.5版本引入了<a href="https://tonybai.com/2015/07/31/understand-go15-vendor/">vendor机制</a>：即Go编译器会优先在vendor目录下搜索依赖的第三方包，这样如果开发者将特定版本的依赖包存放在vendor下面并提交到代码仓库，那么所有人理论上都会得到同样的编译结果，从而实现可重现的构建。</p>
<p>在Go 1.5发布后的若干年，Gopher们把注意力都集中在如何利用vendor解决包依赖问题，从手工添加依赖到vendor、手工更新依赖，到一众包依赖管理工具的诞生：比如: <a href="https://github.com/kardianos/govendor">govendor</a>、<a href="https://github.com/Masterminds/glide">glide</a>以及当时号称准官方工具的<a href="https://github.com/golang/dep">dep</a>，都在努力地尝试着按照当今主流思路解决着诸如：“钻石型依赖”等难题。</p>
<p>但Go核心开发团队没有走寻常路，而是另辟蹊径地在<a href="https://tonybai.com/2018/11/19/some-changes-in-go-1-11/">Go 1.11</a>中引入了采用了<a href="https://tonybai.com/2019/12/21/go-modules-minimal-version-selection/">最小版本选择(mvs)</a>的go module。至此，Go的构建模式被一分为二：gopath mode和module-aware mode。在module-aware mode下，Go构建工具链默认不再使用传统GOPATH下或顶层vendor下面的包了，而是使用\$GOPATH/pkg/mod下面的第三方依赖Go module的local cache。理论上，go module真正实现了“可重复的构建”，我们无需再使用Go 1.5引入的vendor机制了。但社区的反馈让Go核心开发团队<a href="https://groups.google.com/g/golang-dev/c/FTMScX1fsYk/m/uEUSjBAHAwAJ">将module顶层目录下的vendor目录保留了下来</a>，主要考虑vendor还能在下面场合“发光发热”：</p>
<ul>
<li>保持Go1兼容性</li>
</ul>
<p>可继续支持<a href="https://tonybai.com/2015/07/10/some-changes-in-go-1-5/">Go 1.5</a>以后，<a href="https://tonybai.com/2018/02/17/some-changes-in-go-1-10/">Go 1.10</a>之前的Go版本编译Go 1.11后续版本的源码(仅限于：启用了module并带有vendor)。</p>
<ul>
<li>支持离线构建(offline build)</li>
</ul>
<p>module/包构建所需的全部依赖都放入了vendor目录，这样即便在无网络连接的情况下，我们依然可以进行module的构建。这尤其适合企业内部执行CI/CD的那些可能没有外网访问权限的主机。</p>
<ul>
<li>提高构建性能，缩短CI/CD时间</li>
</ul>
<p>在CI/CD时，由于每次都是重新构建，在module-aware模式(非vendor)下，每次都需要重新下载依赖的module到本地，这样十分耗时。而采用vendor方式则无需下载依赖module，提高了构建性能，缩短CI/CD的时间。</p>
<ul>
<li>解决“消失的包/module”的问题</li>
</ul>
<p>一些module/包在经年岁月后可能被从github等托管站点删除了，这时我们如果依赖这些module/包，我们将遇到构建错误（Go Proxy的存在显然让这种可能行极大的降低了）。而使用vendor已经将包/module存放到了本地(以及自己的代码仓库中)，可以解决“包/module消失”的问题。</p>
<ul>
<li>快速分发module的所有依赖包</li>
</ul>
<p>vendor目录下存放了当面module的所有依赖包(及版本)，易于打包并分发。尤其对一些无法通过go get获取到的依赖包/module，这尤为适用。</p>
<p>上述“演化简史”反复提到了<strong>“可重复构建”</strong>，这就是Go核心团队先后推出vendor、go module所基于的核心“痛点”。并且“可重复构建”不单单是个人行为，更多是一个“团队(可以扩展到整个Go社区)”行为：<strong>让团队所有人拿到同样的代码并构建出同样的成果物</strong>。这样来看，<strong>如果不将vendor提交到源码仓库，我们就无法实现这一目标</strong>。</p>
<p>在将vendor提交到代码仓库过程中，你也许会抱怨依赖的代码包太多、依赖变化频繁的问题。但go module所使用的<a href="https://tonybai.com/2019/12/21/go-modules-minimal-version-selection/">“最小版本选择”</a>已经将依赖变动降低到不能再低的程度了，至少比采用主流“依赖管理”思路的其他语言，比如js，构建时面临的变动要少很多了。另外降低依赖的代码包的数量也是你自己的责任，<a href="https://www.imooc.com/read/87/article/2341">Go是“自带电池”的编程语言</a>，其标准库中有很多优秀的包可用，尽量使用标准库包以降低过多的“依赖”。</p>
<p>更多关于Go module和包依赖管理的内容，请查看技术专栏<a href="https://www.imooc.com/read/87/">《改善Go语言编程质量的50个有效实践》</a>。</p>
<p><img src="http://image.tonybai.com/img/202011/qgo-column-pgo-with-qr-and-text.png" alt="" /></p>
<hr />
<p><strong>“Gopher部落”知识星球开球了！</strong>高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！星球首开，福利自然是少不了的！2020年年底之前，8.8折(很吉利吧^_^)加入星球，下方图片扫起来吧！</p>
<p><img src="http://image.tonybai.com/img/202011/gopher-tribe-zsxq.png" alt="" /></p>
<p>我的Go技术专栏：“<a href="https://www.imooc.com/read/87">改善Go语⾔编程质量的50个有效实践</a>”上线了，欢迎大家订阅学习！</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-column-pgo-with-qr-and-text.png" alt="img{512x368}" /></p>
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网热卖中，欢迎小伙伴们订阅学习！</p>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-practice-with-qr-and-text.png" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</p>
<p>2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>微信公众号：iamtonybai</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>“Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544</li>
</ul>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2020, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2020/12/03/should-you-commit-the-vendor-folder-in-go/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go modules：最小版本选择</title>
		<link>https://tonybai.com/2019/12/21/go-modules-minimal-version-selection/</link>
		<comments>https://tonybai.com/2019/12/21/go-modules-minimal-version-selection/#comments</comments>
		<pubDate>Sat, 21 Dec 2019 14:12:14 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[dep]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-get]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[go.sum]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[golib]]></category>
		<category><![CDATA[logrus]]></category>
		<category><![CDATA[Module]]></category>
		<category><![CDATA[mvs]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[rethinkdb-go]]></category>
		<category><![CDATA[RussCox]]></category>
		<category><![CDATA[semver]]></category>
		<category><![CDATA[vgo]]></category>
		<category><![CDATA[依赖管理]]></category>
		<category><![CDATA[最小版本选择]]></category>
		<category><![CDATA[模块]]></category>
		<category><![CDATA[语义版本]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=2839</guid>
		<description><![CDATA[一. 介绍 每个依赖管理解决方案都必须解决选择依赖项版本的问题。当前存在的许多版本选择算法都试图识别任何依赖项的“最新最大(latest greatest)”版本。如果您认为语义版本控制(sematic versioning)将被正确应用并且这种社会契约得到遵守，那么这是有道理的。在这样的情况下，依赖项的“最新最大”版本应该是最稳定和安全的版本，并且应与较早版本具有向后兼容性。至少在相同的主版本(major verion)依赖树中是如此。 Go决定采用其他方法，Russ Cox花费了大量时间和精力撰写文章和演讲探讨Go团队的版本选择方法，即最小版本选择或MVS(Minimal Version Selection)。从本质上讲，Go团队相信MVS为Go程序实现痴线持久的和可重复的构建提供了最佳的方案。我建议大家阅读这篇文章以了解Go团队为什么相信这一点。 在本文中，我将尽最大努力解释MVS语义，展示一个实际的Go语言示例，并实际使用MVS算法。 二. MVS语义 将Go的依赖项版本选择算法命名为“最小版本选择”是有点用词不当，但是一旦您了解了它的工作原理，您会发现这个名称真的很贴切。如我之前所述，许多选择算法会选择依赖项的“最新最大”版本。我喜欢将MVS视为选择“最新非最大(latest non-greatest)”版本的算法。并不是说MVS不能选择“最新最大”，而是只要项目中的任何依赖项都不需要“最新最大”，那么就不需要该版本。 为了更好地理解这一点，让我们创建一种情况，其中几个module（A，B和C）依赖于同一module（D），但是每个module都需要不同的版本。 上图显示了module A，B和C如何分别独立地需要module D和各自需要D的不同版本。 如果我启动一个需要module A的项目，那么为了构建代码，我还需要module D。module D可能有很多版本可供选择。例如，假设module D代表sirupsen的logrus module。我可以要求Go向我提供module D所有已存在（打tag)的版本列表。 清单1： $ go list -m -versions github.com/sirupsen/logrus github.com/sirupsen/logrus v0.1.0 v0.1.1 v0.2.0 v0.3.0 v0.4.0 v0.4.1 v0.5.0 v0.5.1 v0.6.0 v0.6.1 v0.6.2 v0.6.3 v0.6.4 v0.6.5 v0.6.6 v0.7.0 v0.7.1 v0.7.2 v0.7.3 v0.8.0 v0.8.1 [...]]]></description>
			<content:encoded><![CDATA[<h2>一. 介绍</h2>
<p>每个<a href="https://tonybai.com/2019/09/21/brief-history-of-go-package-management/">依赖管理解决方案</a>都必须解决选择依赖项版本的问题。<a href="https://tonybai.com/2017/06/08/first-glimpse-of-dep/">当前存在的许多版本</a>选择算法都试图识别任何依赖项的“最新最大(latest greatest)”版本。如果您认为<a href="https://semver.org">语义版本控制(sematic versioning)</a>将被正确应用并且这种社会契约得到遵守，那么这是有道理的。在这样的情况下，依赖项的“最新最大”版本应该是最稳定和安全的版本，并且应与较早版本具有向后兼容性。至少在相同的主版本(major verion)依赖树中是如此。</p>
<p><a href="https://tonybai.com/tag/golang">Go</a>决定采用其他方法，<a href="https://swtch.com/%7Ersc/">Russ Cox</a>花费了大量时间和精力<a href="https://research.swtch.com/vgo">撰写文章</a>和<a href="https://www.youtube.com/watch?v=F8nrpe0XWRg">演讲探讨</a>Go团队的版本选择方法，即最小版本选择或MVS(Minimal Version Selection)。从本质上讲，Go团队相信MVS为Go程序实现痴线持久的和可重复的构建提供了最佳的方案。我建议大家阅读<a href="https://research.swtch.com/vgo-principles">这篇文章</a>以了解Go团队为什么相信这一点。</p>
<p>在本文中，我将尽最大努力解释MVS语义，展示一个实际的Go语言示例，并实际使用MVS算法。</p>
<h2>二. MVS语义</h2>
<p>将Go的依赖项版本选择算法命名为“最小版本选择”是有点用词不当，但是一旦您了解了它的工作原理，您会发现这个名称真的很贴切。如我之前所述，许多选择算法会选择依赖项的“最新最大”版本。我喜欢将MVS视为选择“最新非最大(latest non-greatest)”版本的算法。并不是说MVS不能选择“最新最大”，而是只要项目中的任何依赖项都不需要“最新最大”，那么就不需要该版本。</p>
<p>为了更好地理解这一点，让我们创建一种情况，其中几个module（A，B和C）依赖于同一module（D），但是每个module都需要不同的版本。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-modules-mvs-1.png" alt="img{512x368}" /></p>
<p>上图显示了module A，B和C如何分别独立地需要module D和各自需要D的不同版本。</p>
<p>如果我启动一个需要module A的项目，那么为了构建代码，我还需要module D。module D可能有很多版本可供选择。例如，假设module D代表sirupsen的logrus module。我可以要求Go向我提供module D所有已存在（打tag)的版本列表。</p>
<p>清单1：</p>
<pre><code>$ go list -m -versions github.com/sirupsen/logrus

github.com/sirupsen/logrus v0.1.0 v0.1.1 v0.2.0
v0.3.0 v0.4.0 v0.4.1 v0.5.0 v0.5.1 v0.6.0 v0.6.1
v0.6.2 v0.6.3 v0.6.4 v0.6.5 v0.6.6 v0.7.0 v0.7.1
v0.7.2 v0.7.3 v0.8.0 v0.8.1 v0.8.2 v0.8.3 v0.8.4
v0.8.5 v0.8.6 v0.8.7 v0.9.0 v0.10.0 v0.11.0 v0.11.1
v0.11.2 v0.11.3 v0.11.4 v0.11.5 v1.0.0 v1.0.1 v1.0.3
v1.0.4 v1.0.5 v1.0.6 v1.1.0 v1.1.1 v1.2.0 v1.3.0
v1.4.0 v1.4.1 v1.4.2
</code></pre>
<p>清单2显示了module D存在的所有版本，我们看到其中显示的“最新最大”版本为1.4.2。</p>
<p>该项目应选择哪个版本的module D呢？确实有两种选择。首选是选择“最新的”版本（在主要版本为1的这一行中），即v1.4.2。第二个选择是选择module A所需的版本v1.0.6。</p>
<p>像<a href="https://tonybai.com/2017/06/08/first-glimpse-of-dep/">dep</a>这样的依赖工具将选择v1.4.2版，并在语义版本化和遵守社会契约的前提下可以正常工作。但是，考虑到Russ Cox在<a href="https://research.swtch.com/vgo-principles">这里</a>阐述的一些原因，Go会尊重module A的要求并选择版本1.0.6。在需要module的项目的所有依赖项的当前所需版本集合中，Go会选择“最小”版本。换句话说，现在只有module A需要module D，而module A已指定它要求的版本为v1.0.6，所需版本集合中只有v1.0.6，因此Go选择的module D的版本即是它。</p>
<p>如果我引入要求项目导入module B的新代码时会怎样？将module B导入项目后，Go会将项目的module D版本从v1.0.6升级到v1.2.0。Go再次在项目依赖项module A和B的当前所需版本集合(v1.0.6和v1.2.0)中选择了module D的“最小”版本。</p>
<p>如果我再次引入需要项目导入module C的新代码时会怎样？Go将从当前所需版本集合（v1.0.6，v1.2.0，v1.3.2）中选择最新版本（v1.3.2）。请注意，版本v1.3.2仍然是module D（v1.4.2）的“最小”版本，而不是“最新最大”版本。</p>
<p>最后，如果删除刚刚添加的依赖module C的代码会怎样？Go会将项目锁定到module D的版本v1.3.2上。降级到版本v1.2.0将是一个更大的更改，而Go知道版本v1.3.2可以正常并稳定运行，因此版本v1.3.2仍然是module D的“最新但非最大(latest non-greatest)“版本。另外，module文件(go.mod)仅维护快照，而不是日志。没有有关历史撤消或降级的信息。</p>
<p>这就是为什么我喜欢将MVS视为选择“最新非最大(latest non-greatest)”module 版本的算法的原因。希望您现在可以理解为什么Russ Cox在命名算法时选择名称“minimal”。</p>
<h2>三. 示例项目</h2>
<p>有了上述基础，我将用一个示例项目让你看到Go和MVS算法实际是如何工作的。在此项目中，module D将用<a href="https://tonybai.com/2018/01/13/the-problems-i-encountered-when-writing-go-code-issue-1st/">logrus module</a>代表，而该项目将直接依赖于<a href="https://github.com/rethinkdb/rethinkdb-go">rethinkdb-go</a>（moduleA）和<a href="https://github.com/Bhinneka/golib">golib</a>（moduleB）module。rethinkdb-go和golib module直接依赖<a href="https://github.com/sirupsen/logrus">logrus</a> module，并且每个module都需要一个不同的logrus版本，并且这些版本都不是logrus的“最新”版本。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-modules-mvs-2.png" alt="img{512x368}" /></p>
<p>上图显示了三个module之间的独立关系。首先，我将创建项目，初始化module，然后加载<a href="https://tonybai.com/2016/12/23/write-go-code-in-vscode/">VS Code</a>。</p>
<p>清单2：</p>
<pre><code>$ cd $HOME
$ mkdir app
$ mkdir app/cmd
$ mkdir app/cmd/db
$ touch app/cmd/db/main.go
$ cd app
$ go mod init app
$ code .
</code></pre>
<p>清单2显示了所有要运行的命令。运行这些命令后，以下代码应出现在VS Code中。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-modules-mvs-3.png" alt="img{512x368}" /></p>
<p>上图显示了项目结构和module文件应包含的内容。有了这个，现在该添加使用rethinkdb-go module的代码了。</p>
<p>清单3：</p>
<p>https://play.golang.org/p/bc5I0Afxhvc</p>
<pre><code>01 package main
02
03 import (
04     "context"
05     "log"
06
07     db "gopkg.in/rethinkdb/rethinkdb-go.v5"
08 )
09
10 func main() {
11     c, err := db.NewCluster([]db.Host{{Name: "localhost", Port: 3000}}, nil)
12     if err != nil {
13         log.Fatalln(err)
14     }
15
16     if _, err = c.Query(context.Background(), db.Query{}); err != nil {
17         log.Fatalln(err)
18     }
19 }
</code></pre>
<p>清单3引入了rethinkdb-go module的major版本v5。添加并保存此代码后，Go会查找、下载和提取module，并更新go.mod和go.sum文件。</p>
<p>清单4：</p>
<pre><code>01 module app
02
03 go 1.13
04
05 require gopkg.in/rethinkdb/rethinkdb-go.v5 v5.0.1
</code></pre>
<p>清单4显示了go.mod需要rethinkdb-go module作为直接依赖项，并选择了v5.0.1版本，该版本是该module的“最新最大版本”。</p>
<p>清单5：</p>
<pre><code>...
github.com/sirupsen/logrus v1.0.6 h1:hcP1GmhGigz/O7h1WVUM5KklBp1JoNS9FggWKdj/j3s=
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
...
</code></pre>
<p>清单5显示了go.sum文件中引入logrus module v1.0.6版本的两行。在这一点上，您可以看到MVS算法已经选择了满足rethinkdb-go module指定要求所需的logrus module的“最小”版本。记住logrus module的“最新最大”版本是1.4.2。</p>
<blockquote>
<p>注意：go.sum文件不应用于理解依赖关系。我在上面所做的版本确定的操作是错误的，稍后我将向您展示确定项目所使用的版本的正确方法。</p>
</blockquote>
<p><img src="https://tonybai.com/wp-content/uploads/go-modules-mvs-4.png" alt="img{512x368}" /></p>
<p>上图显示了Go将使用哪个版本的logrus module来构建项目。</p>
<p>接下来，我将添加引入对golib module有依赖关系的代码。</p>
<p>清单6：</p>
<p>https://play.golang.org/p/h23opcp5qd0</p>
<pre><code>01 package main
02
03 import (
04     "context"
05     "log"
06
07     "github.com/Bhinneka/golib"
08     db "gopkg.in/rethinkdb/rethinkdb-go.v5"
09 )
10
11 func main() {
12     c, err := db.NewCluster([]db.Host{{Name: "localhost", Port: 3000}}, nil)
13     if err != nil {
14         log.Fatalln(err)
15     }
16
17     if _, err = c.Query(context.Background(), db.Query{}); err != nil {
18         log.Fatalln(err)
19     }
20
21     golib.CreateDBConnection("")
22 }
</code></pre>
<p>清单6向该程序添加了07和21行行代码。Go查找、下载并解压缩golib module后，以下更改将显示在go.mod文件中。</p>
<p>清单7：</p>
<pre><code>01 module app
02
03 go 1.13
04
05 require (
06     github.com/Bhinneka/golib v0.0.0-20191209103129-1dc569916cba
07     gopkg.in/rethinkdb/rethinkdb-go.v5 v5.0.1
08 )
</code></pre>
<p>清单7显示go.mod文件已被修改为包括golib module的“最新最大”版本依赖关系，该版本恰好没有语义版本标签。</p>
<p>清单8：</p>
<pre><code>...
github.com/sirupsen/logrus v1.0.6 h1:hcP1GmhGigz/O7h1WVUM5KklBp1JoNS9FggWKdj/j3s=
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
...

</code></pre>
<p>清单8显示了go.sum文件中的四行，现在包括logrus module的v1.0.6和v1.2.0版本。查看go.sum文件中列出的两个版本会带来两个问题：</p>
<ul>
<li>为什么在go.sum文件中列出了两个版本？</li>
<li>Go执行构建时将使用哪个版本？</li>
</ul>
<p>Go团队的Bryan Mills很好地回答了go.sum文件中列出两个版本的原因。</p>
<blockquote>
<p>“go.sum文件仍包含旧版本（1.0.6），因为其传递依赖的要求可能会影响其他module的选定版本。我们真的只需要为go.mod文件提供校验和，因为go.mod中声明了这些传递要求的内容，但是由于go mod tidy不够精确，最终我们也保留了源代码的校验和。” golang.org/issue/33008</p>
</blockquote>
<p>现在仍然存在在构建项目时将使用哪个版本的logrus module的问题。要正确确定将使用哪些module及其版本，请不要查看该go.sum文件，而应使用go list命令。</p>
<p>清单9：</p>
<pre><code>$ go list -m all | grep logrus

github.com/sirupsen/logrus v1.2.0
</code></pre>
<p>清单9显示了在构建项目时将使用logrus module的v1.2.0版本。该-m标志指示go list列出module而不是package。</p>
<p>查看module图可以更深入地了解项目对logrus module的要求。</p>
<p>清单10：</p>
<pre><code>$ go mod graph | grep logrus

github.com/sirupsen/logrus@v1.2.0 github.com/pmezard/go-difflib@v1.0.0
github.com/sirupsen/logrus@v1.2.0 github.com/stretchr/objx@v0.1.1
github.com/sirupsen/logrus@v1.2.0 github.com/stretchr/testify@v1.2.2
github.com/sirupsen/logrus@v1.2.0 golang.org/x/crypto@v0.0.0-20180904163835-0709b304e793
github.com/sirupsen/logrus@v1.2.0 golang.org/x/sys@v0.0.0-20180905080454-ebe1bf3edb33
gopkg.in/rethinkdb/rethinkdb-go.v5@v5.0.1 github.com/sirupsen/logrus@v1.0.6
github.com/sirupsen/logrus@v1.2.0 github.com/konsorten/go-windows-terminal-sequences@v1.0.1
github.com/sirupsen/logrus@v1.2.0 github.com/davecgh/go-spew@v1.1.1
github.com/Bhinneka/golib@v0.0.0-20191209103129-1dc569916cba github.com/sirupsen/logrus@v1.2.0
github.com/prometheus/common@v0.2.0 github.com/sirupsen/logrus@v1.2.0
</code></pre>
<p>清单10显示了logrus module在项目中的关系。我将直接提取显示对logrus的依赖要求的行。</p>
<p>清单11：</p>
<pre><code>gopkg.in/rethinkdb/rethinkdb-go.v5@v5.0.1 github.com/sirupsen/logrus@v1.0.6
github.com/Bhinneka/golib@v0.0.0-20191209103129-1dc569916cba github.com/sirupsen/logrus@v1.2.0
github.com/prometheus/common@v0.2.0 github.com/sirupsen/logrus@v1.2.0
</code></pre>
<p>在清单11中，这些行显示三个module（rethinkdb-go，golib和common）都需要logrus module。由于有了go list命令，我知道所需的最低版本为v1.2.0。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-modules-mvs-5.png" alt="img{512x368}" /></p>
<p>上图展示了Go现在将使用哪个版本的logrus module来构建项目中的代码。</p>
<h2>四. Go Mod Tidy</h2>
<p>在将代码提交/推回存储库之前，请运行go mod tidy以确保module文件是最新且准确的。您在本地构建，运行或测试的代码将随时影响Go对module文件中内容的更新。运行go mod tidy将确保项目具有所需内容的准确和完整的快照，这将帮助您团队中的其他人和您的CI/CD环境。</p>
<p>清单12：</p>
<pre><code>$ go mod tidy

go: finding github.com/Bhinneka/golib latest
go: finding github.com/bitly/go-hostpool latest
go: finding github.com/bmizerany/assert latest
</code></pre>
<p>清单12显示了运行go mod tidy后的输出结果。您会在输出中看到两个新的依赖项。这将更改module文件。</p>
<p>清单13：</p>
<pre><code>01 module app
02
03 go 1.13
04
05 require (
06     github.com/Bhinneka/golib v0.0.0-20191209103129-1dc569916cba
07     github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 // indirect
08     github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
09     gopkg.in/rethinkdb/rethinkdb-go.v5 v5.0.1
10 )
</code></pre>
<p>清单13显示了go-hostpool和assert module被列为构建项目所需的间接module。之所以在此处列出它们，是因为这些项目当前与module机制不兼容。换句话说，这些项目的任何tag版本或master中“最新的”版本都不存在go.mod文件。</p>
<p>为什么运行go mod tidy后包含了这些module？我可以使用go mod why命令找出答案。</p>
<p>清单14：</p>
<pre><code>$ go mod why github.com/hailocab/go-hostpool

# github.com/hailocab/go-hostpool
app/cmd/db
gopkg.in/rethinkdb/rethinkdb-go.v5
github.com/hailocab/go-hostpool

------------------------------------------------

$ go mod why github.com/bmizerany/assert

# github.com/bmizerany/assert
app/cmd/db
gopkg.in/rethinkdb/rethinkdb-go.v5
github.com/hailocab/go-hostpool
github.com/hailocab/go-hostpool.test
github.com/bmizerany/assert
</code></pre>
<p>清单14显示了为什么项目间接需要这些module。rethinkdb-go module需要go-hostpool module，而go-hostpool module需要assert module。</p>
<h2>五. 升级依赖关系</h2>
<p>该项目具有三个依赖项，每个依赖项都需要logrus module，其中当前正在选择logrus module的v1.2.0版本。在项目生命周期的某个时刻，升级直接和间接依赖关系以确保项目所需的代码是最新的并且可以利用新功能、错误修复和升级安全补丁将变得很重要。要进行升级，Go提供了go get命令。</p>
<p>在运行go get升级项目的依赖项之前，需要考虑几个选项。</p>
<h3>使用MVS仅升级必需的直接和间接依赖项</h3>
<p>我建议从这种升级开始，直到您了解更多有关项目和module的信息。这是的最保守的形式go get。</p>
<p>清单15：</p>
<pre><code>$ go get -t -d -v ./...
</code></pre>
<p>清单15显示了如何使用MVS算法对那些必需依赖项的升级。下面是命令中一些命令行选型的定义。</p>
<ul>
<li>-t flag：考虑构建测试所需的module。</li>
<li>-d flag：下载每个module的源代码，但不要构建或安装它们。</li>
<li>-v flag：提供详细输出。</li>
<li>./&#8230; ：在整个源代码树中执行这些操作，并且仅更新所需的依赖项。</li>
</ul>
<p>对当前项目运行此命令不会导致任何更改，因为该项目已经是最新版本，并且具有构建和测试该项目所需的最低版本。那是因为我刚运行了go mod tidy，项目是新的。</p>
<h3>使用最新最大版本仅升级必需的直接和间接依赖项</h3>
<p>这种升级会将整个项目的依赖性从“最小”提高到“最新最大”。所需要做的只是将-u标志添加到命令行。</p>
<p>清单16：</p>
<pre><code>$ go get -u -t -d -v ./...

go: finding golang.org/x/net latest
go: finding golang.org/x/sys latest
go: finding github.com/hailocab/go-hostpool latest
go: finding golang.org/x/crypto latest
go: finding github.com/google/jsonapi latest
go: finding gopkg.in/bsm/ratelimit.v1 latest
go: finding github.com/Bhinneka/golib latest
</code></pre>
<p>清单16显示了运行带有-u标志的go get命令的输出。此输出无法说明真实情况。如果我问go list命令现在使用哪个版本的logrus module来构建项目，会发生什么情况呢？</p>
<p>清单17：</p>
<pre><code>$ go list -m all | grep logrus

github.com/sirupsen/logrus v1.4.2
</code></pre>
<p>清单17显示了如何选择“最新”的logrus。为了使这一选择更加明确，对go.mod文件进行了更改。</p>
<p>清单18：</p>
<pre><code>01 module app
02
03 go 1.13
04
05 require (
06     github.com/Bhinneka/golib v0.0.0-20191209103129-1dc569916cba
07     github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 // indirect
08     github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
09     github.com/cenkalti/backoff v2.2.1+incompatible // indirect
10     github.com/golang/protobuf v1.3.2 // indirect
11     github.com/jinzhu/gorm v1.9.11 // indirect
12     github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
13     github.com/sirupsen/logrus v1.4.2 // indirect
14     golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 // indirect
15     golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect
16     golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 // indirect
17     gopkg.in/rethinkdb/rethinkdb-go.v5 v5.0.1
18 )
</code></pre>
<p>清单18在第13行显示版本v1.4.2现在是项目中logrus module的选定版本。构建项目时，Go会注意module文件中的这一行。即使删除了对logrus module的依赖关系更改的代码，该项目的v1.4.2版现在也已被锁定。请记住，降级将是一个更大的变化，而v1.4.2版将不受影响。</p>
<p>go.sum文件中可以看到哪些更改？</p>
<p>清单19：</p>
<pre><code>github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
</code></pre>
<p>清单19显示了go.sum文件中表示logrus的所有三个版本。正如上面的Bryan所解释的，这是因为传递要求可能会影响其他module的选定版本。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-modules-mvs-6.png" alt="img{512x368}" /><br />
上图展示了Go现在将使用哪个版本的logrus module来构建项目中的代码。</p>
<h3>使用最新最大版本升级所有直接和间接依赖项</h3>
<p>您可以将./&#8230;选项替换为all来升级所有直接和间接依赖项，包括构建项目时也并不需要的依赖项。</p>
<p>清单20：</p>
<pre><code>$ go get -u -t -d -v all

go: downloading github.com/mattn/go-sqlite3 v1.11.0
go: extracting github.com/mattn/go-sqlite3 v1.11.0
go: finding github.com/bitly/go-hostpool latest
go: finding github.com/denisenkom/go-mssqldb latest
go: finding github.com/hailocab/go-hostpool latest
go: finding gopkg.in/bsm/ratelimit.v1 latest
go: finding github.com/google/jsonapi latest
go: finding golang.org/x/net latest
go: finding github.com/Bhinneka/golib latest
go: finding golang.org/x/crypto latest
go: finding gopkg.in/tomb.v1 latest
go: finding github.com/bmizerany/assert latest
go: finding github.com/erikstmartin/go-testdb latest
go: finding gopkg.in/check.v1 latest
go: finding golang.org/x/sys latest
go: finding github.com/golang-sql/civil latest
</code></pre>
<p>清单20显示了现在为该项目找到、下载和提取了多少个依赖项。</p>
<p>清单21：</p>
<pre><code>Added to Module File
   cloud.google.com/go v0.49.0 // indirect
   github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73 // indirect
   github.com/google/go-cmp v0.3.1 // indirect
   github.com/jinzhu/now v1.1.1 // indirect
   github.com/lib/pq v1.2.0 // indirect
   github.com/mattn/go-sqlite3 v2.0.1+incompatible // indirect
   github.com/onsi/ginkgo v1.10.3 // indirect
   github.com/onsi/gomega v1.7.1 // indirect
   github.com/stretchr/objx v0.2.0 // indirect
   google.golang.org/appengine v1.6.5 // indirect
   gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
   gopkg.in/yaml.v2 v2.2.7 // indirect

Removed from Module File
   github.com/golang/protobuf v1.3.2 // indirect
</code></pre>
<p>清单21显示了对该go.mod文件的更改。添加了更多module，并删除了一个module。</p>
<blockquote>
<p>注意：如果你使用vendor，则go mod vendor命令将从vendor文件夹中剥离test文件。</p>
</blockquote>
<p>通常，通过go get升级项目的依赖项时不要使用all或-u选项。坚持只升级需要的module，并使用MVS算法选择这些module及其版本。必要时手动更改为特定的module版本。手动更改可以通过手动编辑go.mod文件来完成，我将在以后的文章中向您展示。</p>
<h2>五. 重置依赖关系</h2>
<p>如果您在任何时候都不满意所选的module和版本，则你始终可以通过删除module文件并再次运行go mod tidy来重置选择。当项目还很年轻并且情况不稳定时，这更是一种选择。项目稳定并发布后，我会犹豫重新设置依赖关系。正如我上面提到的，随着时间的推移，可能会设置module版本，并且您需要长期持久且可重复的构建。</p>
<p>清单22：</p>
<pre><code>$ rm go.*
$ go mod init &lt;module name&gt;
$ go mod tidy
</code></pre>
<p>清单22显示了允许MVS从头开始再次执行所有选择的命令。在撰写本文的整个过程中，我一直在进行此操作以重置项目并提供本文的代码清单。</p>
<h2>六. 结论</h2>
<p>在这篇文章中，我解释了MVS语义，并展示了Go和MVS算法实际应用的真实示例。我还展示了一些Go命令，这些命令可以在您遇到未知问题时为您提供信息。在为项目添加越来越多的依赖项时，可能会遇到一些极端情况。这是因为Go生态系统已有<a href="https://tonybai.com/2019/11/09/go-opensource-10-years/">10年的历史</a>，所有现有项目都需要更多时间才能符合module要求。</p>
<p>在以后的文章中，我将讨论在同一项目中使用不同主要版本的依赖关系，以及如何手动检索和锁定依赖关系的特定版本。现在，我希望您对module和Go工具有更多的信任，并且对MVS如何随着时间的推移选择版本有了更清晰的了解。如果您遇到任何问题，可以在#module组的Gopher Slack上找到一群愿意提供帮助的人。</p>
<p>本文翻译自<a href="https://www.ardanlabs.com/blog/2019/12/modules-03-minimal-version-selection.html">《Modules Part 03: Minimal Version Selection》</a>。</p>
<hr />
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网上线了，感谢小伙伴们学习支持！</p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<p>微博：https://weibo.com/bigwhite20xx<br />
微信公众号：iamtonybai<br />
博客：tonybai.com<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2019, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2019/12/21/go-modules-minimal-version-selection/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Go语言包管理简史</title>
		<link>https://tonybai.com/2019/09/21/brief-history-of-go-package-management/</link>
		<comments>https://tonybai.com/2019/09/21/brief-history-of-go-package-management/#comments</comments>
		<pubDate>Sat, 21 Sep 2019 02:55:51 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[dep]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[glide]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[go.sum]]></category>
		<category><![CDATA[go1.11]]></category>
		<category><![CDATA[go1.12]]></category>
		<category><![CDATA[go1.13]]></category>
		<category><![CDATA[Go1.5]]></category>
		<category><![CDATA[gobuild]]></category>
		<category><![CDATA[goget]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[golist]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[GOPATH]]></category>
		<category><![CDATA[Module]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[package-management]]></category>
		<category><![CDATA[semver]]></category>
		<category><![CDATA[vendor]]></category>
		<category><![CDATA[versioning]]></category>
		<category><![CDATA[vgo]]></category>
		<category><![CDATA[依赖]]></category>
		<category><![CDATA[包]]></category>
		<category><![CDATA[包管理]]></category>
		<category><![CDATA[模块]]></category>
		<category><![CDATA[版本控制]]></category>
		<category><![CDATA[语义化版本]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=2771</guid>
		<description><![CDATA[包管理是Go一直被诟病做得不好的功能之一。先前版本（go 1.11之前）的主要缺点之一是go get是缺乏对依赖包版本的管理和对可复制构建(reproducible build)的支持。Go社区已经开发了一些包管理器和工具作为版本化包依赖的事实标准解决方案，如glide，dep以及一些辅助工具等。 “我在生产构建中使用go get。” &#8211; 没有人这么说过。 Go语言的包管理实现可追溯到Google公司内的代码依赖管理（Google将内部所有源代码都存放在一个巨大的单体存储库中）。我们来分析一下在”Go module”之前Go语言的包管理工具都出了什么问题。 依赖包的版本化 依赖包的本地缓存(vendor) GOPATH的必要性 依赖包的版本化 go get默认情况下不支持包版本控制。go软件包管理的第一版实现背后的想法是-不需要包版本控制，不需要第三方包存储库，您可以从当前分支中构建所有内容。 在Go 1.11之前的版本中，添加依赖项意味着将该依赖项的源代码仓库克隆到$GOPATH下面。就是这样，没有版本的概念。版本始终指向克隆时刻的主分支。出现了另一个主要问题是，当不同的项目需要依赖包的不同版本时，Go包管理工具无法实现。 依赖包的本地缓存(vendor) 依赖包本地缓存通常是指相关依赖包与项目存储在同一位置。这通常意味着将您的依赖项源码也提交到源管理系统中，例如Git。 考虑这样一种情况- A使用依赖项B，而B使用了C版本在1.5版本中引入一个功能，这时B必须确保A在构建时使用的也是C 1.5或更高版本。在Go 1.5之前的版本中，没有一种机制可以在不重写导入路径的情况下将依赖包代码与命令绑定在一起。 GOPATH的必要性 GOPATH存在的主要原因有两个： 在Go中，import声明通过其完全限定的导入路径来引用包。GOPATH存在可以方便Go工具计算GOPATH/src内的任何目录所涉及软件包的绝对导入路径。 它是Go get命令存储包依赖项的位置。 这有什么问题？ GOPATH 不允许开发人员像其他语言一样选择任意喜欢的目录签出项目的源代码。 此外，GOPATH不允许开发人员同时检出某个项目（或其依赖项）的多个副本。 Go Module介绍 Go 1.11引入了对Go模块(module)的初步支持。下面摘自Go Wiki： 一个模块是一组相关的Go包的集合，这个包集合被当做一个独立的单元进行统一版本管理。模块精确记录了依赖要求并支持创建可复制的构建。 Go模块带来了三个重要的内置功能： go.mod文件，它与package.json或Pipfile文件的功能类似。 机器生成的传递依赖项描述文件 &#8211; go.sum。 不再有GOPATH限制。模块可以位于任何路径中。 $ go help mod Go mod provides access to operations [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/go-package-management-history.png" alt="img{512x368}" /></p>
<p>包管理是Go一直被诟病做得不好的功能之一。先前版本（<a href="https://tonybai.com/2018/11/19/some-changes-in-go-1-11/">go 1.11</a>之前）的主要缺点之一是go get是缺乏对依赖包版本的管理和对可复制构建(reproducible build)的支持。Go社区已经开发了一些包管理器和工具作为版本化包依赖的事实标准解决方案，如<a href="https://github.com/Masterminds/glide">glide</a>，<a href="https://tonybai.com/2017/06/08/first-glimpse-of-dep">dep</a>以及一些<a href="https://github.com/golang/go/wiki/PackageManagementTools">辅助工具</a>等。</p>
<blockquote>
<p>“我在生产构建中使用go get。” &#8211; 没有人这么说过。</p>
</blockquote>
<p>Go语言的包管理实现可追溯到Google公司内的代码依赖管理（Google将内部所有源代码都存放在一个巨大的单体存储库中）。我们来分析一下在”Go module”之前Go语言的包管理工具都出了什么问题。</p>
<ul>
<li>依赖包的版本化</li>
<li>依赖包的本地缓存(vendor)</li>
<li>GOPATH的必要性</li>
</ul>
<h2>依赖包的版本化</h2>
<p>go get默认情况下不支持包版本控制。go软件包管理的第一版实现背后的想法是-不需要包版本控制，不需要第三方包存储库，您可以从当前分支中构建所有内容。</p>
<p>在<a href="https://tonybai.com/2018/11/19/some-changes-in-go-1-11/">Go 1.11</a>之前的版本中，添加依赖项意味着将该依赖项的源代码仓库克隆到$GOPATH下面。就是这样，没有版本的概念。版本始终指向克隆时刻的主分支。出现了另一个主要问题是，当不同的项目需要依赖包的不同版本时，Go包管理工具无法实现。</p>
<h2>依赖包的本地缓存(vendor)</h2>
<p>依赖包本地缓存通常是指相关依赖包与项目存储在同一位置。这通常意味着将您的依赖项源码也提交到源管理系统中，例如Git。</p>
<p>考虑这样一种情况- A使用依赖项B，而B使用了C版本在1.5版本中引入一个功能，这时B必须确保A在构建时使用的也是C 1.5或更高版本。在<a href="https://tonybai.com/2015/07/10/some-changes-in-go-1-5/">Go 1.5</a>之前的版本中，没有一种机制可以在不重写导入路径的情况下将依赖包代码与命令绑定在一起。</p>
<h2>GOPATH的必要性</h2>
<p>GOPATH存在的主要原因有两个：</p>
<ol>
<li>在Go中，import声明通过其完全限定的导入路径来引用包。GOPATH存在可以方便Go工具计算GOPATH/src内的任何目录所涉及软件包的绝对导入路径。</li>
<li>它是Go get命令存储包依赖项的位置。</li>
</ol>
<p>这有什么问题？</p>
<ol>
<li>GOPATH 不允许开发人员像其他语言一样选择任意喜欢的目录签出项目的源代码。</li>
<li>此外，GOPATH不允许开发人员同时检出某个项目（或其依赖项）的多个副本。</li>
</ol>
<h2>Go Module介绍</h2>
<p><a href="https://tonybai.com/2018/11/19/some-changes-in-go-1-11/">Go 1.11</a>引入了对<a href="https://tonybai.com/2019/06/03/the-practice-of-upgrading-major-version-under-go-module/">Go模块</a>(module)的初步支持。下面摘自Go Wiki：</p>
<blockquote>
<p>一个模块是一组相关的Go包的集合，这个包集合被当做一个独立的单元进行统一版本管理。模块精确记录了依赖要求并支持创建可复制的构建。</p>
</blockquote>
<p>Go模块带来了三个重要的内置功能：</p>
<ol>
<li>go.mod文件，它与package.json或Pipfile文件的功能类似。</li>
<li>机器生成的传递依赖项描述文件 &#8211; go.sum。</li>
<li>不再有GOPATH限制。模块可以位于任何路径中。</li>
</ol>
<pre><code>$ go help mod
Go mod provides access to operations on modules.

Note that support for modules is built into all the go commands,
not just 'go mod'. For example, day-to-day adding, removing, upgrading,
and downgrading of dependencies should be done using 'go get'.
See 'go help modules' for an overview of module functionality.

Usage:

    go mod &lt;command&gt; [arguments]

The commands are:

    download    download modules to local cache
    edit        edit go.mod from tools or scripts
    graph       print module requirement graph
    init        initialize new module in current directory
    tidy        add missing and remove unused modules
    vendor      make vendored copy of dependencies
    verify      verify dependencies have expected content
    why         explain why packages or modules are needed

Use "go help mod &lt;command&gt;" for more information about a command.
</code></pre>
<p>更多相关讨论在<a href="https://groups.google.com/forum/#!topic/golang-dev/a5PqQuBljF4">这里</a>。</p>
<h2>迁移到Go Module</h2>
<p>要使用Go模块，请更新Go到1.11及以上版本。由于不再需要GOPATH，因此可以通过以下两种方式之一激活模块支持(译注：下面的行为仅适用于Go 1.11~<a href="https://tonybai.com/2019/03/02/some-changes-in-go-1-12/">Go 1.12</a>，<a href="https://tip.golang.org/doc/go1.13">Go 1.13版本</a>默认开启Go module，无论是否在GOPATH下，除非GO111MODULE=off)：</p>
<ul>
<li>在GOPATH/src之外的目录中调用Go命令，并在当前目录中存在一个有效的go.mod文件。</li>
<li>如果源码在GOPATH之下，Go模块将不起作用。要改变此行为，请设置环境变量GO111MODULE=on后再调用Go命令。</li>
</ul>
<p>让我们通过以下简单的步骤开始迁移：</p>
<ul>
<li>
<p>由于GOPATH不再必要的了，将module移出GOPATH。</p>
</li>
<li>
<p>在项目根目录中，创建初始模块定义 &#8211; go mod init github.com/username/repository。go mod还会自动转换现有的包管理器（如dep和Gopkg，glide以及<a href="https://tip.golang.org/pkg/cmd/go/internal/modconv/?m=all#pkg-variables">其他六种</a>）的依赖关系。这将创建一个名为go.mod的文件，该文件存储了模块名以及模块的依赖项及其版本。</p>
</li>
</ul>
<pre><code>$ cat go.mod
module github.com/deepsourcelabs/cli

go 1.12

require (
    github.com/certifi/gocertifi v0.0.0-20190410005359-59a85de7f35e
    github.com/getsentry/raven-go v0.2.0
    github.com/pkg/errors v0.0.0-20190227000051-27936f6d90f9
)
</code></pre>
<ul>
<li>运行go build会创建一个go.sum文件，其中包含特定模块版本的内容的预期校验和。这是为了确保这些模块将来的下载内容与第一次下载是相同的。请注意，go.sum不是锁文件。</li>
</ul>
<pre><code>$ cat go.sum
github.com/certifi/gocertifi v0.0.0-20190410005359-59a85de7f35e h1:9574pc8MX6rF/QyO14SPHhM5KKIOo9fkb/1ifuYMTKU=
github.com/certifi/gocertifi v0.0.0-20190410005359-59a85de7f35e/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4=
github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs=
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/pkg/errors v0.0.0-20190227000051-27936f6d90f9 h1:dIsTcVF0w9viTLHXUEkDI7cXITMe+M/MRRM2MwisVow=
github.com/pkg/errors v0.0.0-20190227000051-27936f6d90f9/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
</code></pre>
<blockquote>
<p>关于版本控制的注意事项：为了保持向后兼容性，如果模块的版本为v2或更高版本，则模板的主版本必须以/vN的形式被包含在go.mod文件中使用的模块路径的末尾。比如：module github.com/username/repository/v2</p>
</blockquote>
<h2>日常命令</h2>
<h3>列出依赖项</h3>
<p>go list -m all 列出当前模块及其所有依赖项。</p>
<pre><code>$ go list -m all
github.com/deepsourcelabs/cli
github.com/certifi/gocertifi v0.0.0-20190410005359-59a85de7f35e
github.com/getsentry/raven-go v0.2.0
github.com/pkg/errors v0.0.0-20190227000051-27936f6d90f9
</code></pre>
<blockquote>
<p>在go list输出中，当前模块（也称为主模块）始终是第一行，其后是路径排序所有依赖模块。</p>
</blockquote>
<h3>列出软件包的可用版本</h3>
<p>go list -m -versions github.com/username/repository 列出软件包的可用版本。</p>
<pre><code>$ go list -m -versions github.com/getsentry/raven-go
github.com/getsentry/raven-go v0.1.0 v0.1.1 v0.1.2 v0.2.0
</code></pre>
<h3>添加依赖</h3>
<p>添加依赖项是隐式的。在代码中导入依赖项后，运行go build或go test命令将获取模块的最新版本并将其添加到go.mod文件中。如果要显式添加依赖项，请运行go get github.com/username/repository。</p>
<h3>依赖项的升级/降级</h3>
<p>go get github.com/username/repository@vx.x.x下载并设置依赖项和更新go.mod文件的特定版本。</p>
<pre><code>$ go get github.com/getsentry/raven-go@v0.1.2
go: finding github.com/getsentry/raven-go v0.1.2
go: downloading github.com/getsentry/raven-go v0.1.2
go: extracting github.com/getsentry/raven-go v0.1.2

$ cat go.mod
module github.com/deepsourcelabs/marvin-go

go 1.12

require (
    github.com/certifi/gocertifi v0.0.0-20190410005359-59a85de7f35e
    github.com/getsentry/raven-go v0.1.2
    github.com/pkg/errors v0.0.0-20190227000051-27936f6d90f9
)

$ cat go.sum
github.com/certifi/gocertifi v0.0.0-20190410005359-59a85de7f35e h1:9574pc8MX6rF/QyO14SPHhM5KKIOo9fkb/1ifuYMTKU=
github.com/certifi/gocertifi v0.0.0-20190410005359-59a85de7f35e/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4=
github.com/getsentry/raven-go v0.1.2 h1:4V0z512S5mZXiBvmW2RbuZBSIY1sEdMNsPjpx2zwtSE=
github.com/getsentry/raven-go v0.1.2/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs=
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/pkg/errors v0.0.0-20190227000051-27936f6d90f9 h1:dIsTcVF0w9viTLHXUEkDI7cXITMe+M/MRRM2MwisVow=
github.com/pkg/errors v0.0.0-20190227000051-27936f6d90f9/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
</code></pre>
<h3>vendor依赖项</h3>
<p>使用模块时，go命令将完全忽略vendor目录。为了向后兼容旧版Go，或确保将用于构建的所有文件一起存储在单个文件树中，请运行go mod vendor。</p>
<p>这将在主模块的根目录中创建一个vendor目录，并将依赖模块中的所有软件包存储在该目录中。</p>
<blockquote>
<p>注意：要使用主模块的顶级vendor目录进行构建，请运行&#8217;go build -mod=vendor&#8217;。</p>
</blockquote>
<h3>删除未使用的依赖项</h3>
<p>go mod tidy将删除未使用的依赖项并更新go.mod文件。</p>
<h2>常见问题解答</h2>
<ol>
<li>
<p>GOPATH不再需要了？<br />
是，永别了GOPATH。</p>
</li>
<li>
<p>默认情况下拉取哪个版本？<br />
go.mod文件和go命令通常将语义版本用作描述模块版本的标准形式，以便可以比较版本以确定哪个版本应早于或晚于其他版本。v1.2.3通过在基础源存储库中标记(tag)修订来引入类似的模块版本。未标记(untag)的修订版可以使用“伪版本”之类的来引用：v0.0.0-yyyymmddhhmmss-abcdefabcdef，其中时间是UTC的提交时间，最后的后缀是提交哈希的前缀。</p>
</li>
<li>
<p>go.sum应该被检入到版本库中吗？<br />
是。</p>
</li>
</ol>
<p>鉴于本人近期较忙，又不希望让博客长草，近一段时间会挑选翻译一些笔者认为比较优秀的外文文章分享给大家。</p>
<p>本文翻译自<a href="https://deepsource.io/blog/go-modules/">《Package management in Go &#8211;  brief overview of package management in Go — pre and post Go modules》</a>。</p>
<hr />
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网上线了，感谢小伙伴们学习支持！</p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>我的联系方式：</p>
<p>微博：https://weibo.com/bigwhite20xx<br />
微信公众号：iamtonybai<br />
博客：tonybai.com<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2019, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2019/09/21/brief-history-of-go-package-management/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Go module机制下升级major版本号的实践</title>
		<link>https://tonybai.com/2019/06/03/the-practice-of-upgrading-major-version-under-go-module/</link>
		<comments>https://tonybai.com/2019/06/03/the-practice-of-upgrading-major-version-under-go-module/#comments</comments>
		<pubDate>Mon, 03 Jun 2019 09:20:59 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[bitbucket]]></category>
		<category><![CDATA[client-go]]></category>
		<category><![CDATA[dep]]></category>
		<category><![CDATA[gitee]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[go.sum]]></category>
		<category><![CDATA[go1.11]]></category>
		<category><![CDATA[go1.12]]></category>
		<category><![CDATA[go1.13]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GOPROXY]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Module]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[RussCox]]></category>
		<category><![CDATA[SemanticImportVersioning]]></category>
		<category><![CDATA[vgo]]></category>
		<category><![CDATA[包导入]]></category>
		<category><![CDATA[包管理]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=2718</guid>
		<description><![CDATA[Go module机制在Go 1.11版本引入，虽然也伴随着不小的质疑声，但总体上Go社区多数Gopher是接受go module的，很多标杆式的Go项目(比如kubernetes、kubernetes client-go等)也都逐渐转向了Go module，并且Gopher也在向core team反馈了自己的建议和问题。Go core team也在go module最初设计的基础上持续进行着改进，比如：即将到来的Go 1.13版本中将增加默认GOPROXY(https://proxy.golang.org)、GOSUMDB(sum.golang.org)；增加GONOPROXY、GONOSUMDB以应对私有module的处理；不断丰富的go mod子命令功能等。 随着Go module应用的日益逐步广泛和深入，Gopher们也开始遇到一些最初使用Go module时未曾遇到过的问题，比如升级major版本号（这是由于多数Go project仍处于untag的状态或者1.x.x状态，因此在go module引入初期，少有gopher遇到该类问题）。这篇文章我们就来简单看看如何在go module机制下面升级库的主版本号(major version number)。 一. Go module的“semantic import versioning” 在Russ Cox关于go module的系列论述文章“semantic import versioning”一文中，Russ说明了Go import包兼容性的总原则： 如果新旧版本的包使用相同的导入路径(import path)，那么新包与旧包是兼容的。 也就是说如果新旧两个包不兼容，那么应该采用不同的导入路径。 因此，Russ采用了将“major版本”作为导入路径的一部分的设计。这种设计支持在同一个项目或go source文件中import同一个module下的package的不同版本。同一个package虽然包名字相同，但是import path不同。vN作为import path的一部分将用于区分包的不同版本。同时在同一个源文件中，我们可以使用包别名的方式来区分同一个包的不同版本，比如： import ( "github.com/bigwhite/foo/bar" barV2 "github.com/bigwhite/foo/v2/bar" ... ... ) go module的这种设计对Go包的consumer(包的使用者)来说似乎并未有太多额外工作，但是这给Go包的author们带来了一定的复杂性，他们需要考虑在go module机制下如何将自己的Go module升级major version。稍有不慎，可能就会导致自身代码库的混乱或者package consumer侧无法通过编译或执行行为的混乱。 下面我们就从go package author的角度实践一下究竟该如何做module [...]]]></description>
			<content:encoded><![CDATA[<p><a href="https://tip.golang.org/cmd/go/#hdr-Module_proxy_protocol">Go module机制</a>在<a href="https://tonybai.com/2018/11/19/some-changes-in-go-1-11/">Go 1.11版本</a>引入，虽然也伴随着不小的质疑声，但总体上Go社区多数Gopher是接受go module的，很多标杆式的Go项目(比如kubernetes、kubernetes client-go等)也都逐渐转向了Go module，并且Gopher也在向core team反馈了自己的建议和问题。Go core team也在go module最初设计的基础上持续进行着改进，比如：即将到来的Go 1.13版本中将增加默认<a href="https://groups.google.com/forum/#!topic/golang-dev/4Kw_OfGa7cc">GOPROXY(https://proxy.golang.org)</a>、GOSUMDB(sum.golang.org)；增加GONOPROXY、GONOSUMDB以应对私有module的处理；不断丰富的<a href="https://tip.golang.org/cmd/go/#hdr-Module_proxy_protocol">go mod子命令功能</a>等。</p>
<p>随着Go module应用的日益逐步广泛和深入，Gopher们也开始遇到一些最初使用Go module时未曾遇到过的问题，比如升级major版本号（这是由于多数Go project仍处于untag的状态或者1.x.x状态，因此在go module引入初期，少有gopher遇到该类问题）。这篇文章我们就来简单看看如何在go module机制下面升级库的主版本号(major version number)。</p>
<h2>一. Go module的“semantic import versioning”</h2>
<p>在Russ Cox关于go module的系列论述文章<a href="https://research.swtch.com/vgo-import">“semantic import versioning”</a>一文中，Russ说明了Go import包兼容性的总原则：</p>
<pre><code>如果新旧版本的包使用相同的导入路径(import path)，那么新包与旧包是兼容的。

</code></pre>
<p>也就是说如果新旧两个包不兼容，那么应该采用不同的导入路径。</p>
<p>因此，Russ采用了将“major版本”作为导入路径的一部分的设计。这种设计支持在同一个项目或go source文件中import同一个module下的package的不同版本。同一个package虽然包名字相同，但是import path不同。<strong>vN</strong>作为import path的一部分将用于区分包的不同版本。同时在同一个源文件中，我们可以使用包别名的方式来区分同一个包的不同版本，比如：</p>
<pre><code>import (

    "github.com/bigwhite/foo/bar"

    barV2 "github.com/bigwhite/foo/v2/bar"

    ... ...

)

</code></pre>
<p>go module的这种设计对Go包的consumer(包的使用者)来说似乎并未有太多额外工作，但是这给Go包的author们带来了一定的复杂性，他们需要考虑在go module机制下如何将自己的Go module升级major version。稍有不慎，可能就会导致自身代码库的混乱或者package consumer侧无法通过编译或执行行为的混乱。</p>
<p>下面我们就从go package author的角度实践一下究竟该如何做module major版本号的升级。Go module为go package author提供了两种major version升级的方案，我们下面逐一看一下。我们的实验环境基于<a href="https://tonybai.com/2019/03/02/some-changes-in-go-1-12/">go 1.12.5</a> (ubuntu 16.04)。</p>
<h2>二. 使用“major branch”方案</h2>
<p>“major branch”方案对于多数gopher来说是一个过渡比较自然的方案，它通过建立vN分支并基于vN分支打vN.x.x的tag的方式做major version的升级。当然是否建立vN分支以及打vN.x.x tag都是一个可选的操作。</p>
<p>我们在bitbucket.org上建立一个公共仓库：bitbucket.org/bigwhite/modules-major-branch，其初始结构和代码如下(注意：此时本地开发环境中GO111MODULE=on)：</p>
<pre><code># tree -LF 2 modules-major-branch
modules-major-branch
├── foo/
│   └── foo.go
├── go.mod
└── README.md

1 directory, 3 files

//go.mod

# cat go.mod
module bitbucket.org/bigwhite/modules-major-branch

go 1.12

// foo.go

package foo

import "fmt"

func Foo() {
        fmt.Println("foo.Foo of module: bitbucket.org/bigwhite/modules-major-branch pre-v1")
}

</code></pre>
<p>接下来，我们建立modules-major-branch/foo包的消费者项目：modules-major-branch-test</p>
<pre><code># tree -LF 1 ./modules-major-branch-test/
./modules-major-branch-test/
├── go.mod
├── go.sum
└── main.go

0 directories, 3 files

# cat go.mod
module bitbucket.org/bigwhite/modules-major-branch-test

go 1.12

# cat main.go
package main

import (
    "bitbucket.org/bigwhite/modules-major-branch/foo"
)

func main() {
    foo.Foo()
}

</code></pre>
<p>我们run一下“消费者”：</p>
<pre><code># go run main.go
go: finding bitbucket.org/bigwhite/modules-major-branch/foo latest
go: finding bitbucket.org/bigwhite/modules-major-branch latest
go: downloading bitbucket.org/bigwhite/modules-major-branch v0.0.0-20190602132049-2d924da2e295
go: extracting bitbucket.org/bigwhite/modules-major-branch v0.0.0-20190602132049-2d924da2e295
foo.Foo of module: bitbucket.org/bigwhite/modules-major-branch pre-v1

</code></pre>
<p>我们看到在这个阶段消费成功。</p>
<p>作为modules-major-branch的author，随着module功能演进，modules-major-branch到达了发布1.0版本的节点：</p>
<pre><code># cat foo/foo.go
package foo

import "fmt"

func Foo() {
    fmt.Println("foo.Foo of module: bitbucket.org/bigwhite/modules-major-branch v1.0.0")
}

# git tag v1.0.0
# git push --tag origin master

</code></pre>
<p>接下来，我们让consumer升级对modules-major-branch/foo的依赖到v1.0.0。这种升级是不会自动进行，是需要consumer的开发者自己决策后手工进行的，否则会给开发者带来困惑。我们通过go mod edit命令修改consumer的require：</p>
<pre><code># go mod edit -require=bitbucket.org/bigwhite/modules-major-branch@v1.0.0

# cat go.mod
module bitbucket.org/bigwhite/modules-major-branch-test

go 1.12

require bitbucket.org/bigwhite/modules-major-branch v1.0.0

</code></pre>
<p>我们来运行一下升级依赖后的程序：</p>
<pre><code># go run main.go
go: finding bitbucket.org/bigwhite/modules-major-branch v1.0.0
go: downloading bitbucket.org/bigwhite/modules-major-branch v1.0.0
go: extracting bitbucket.org/bigwhite/modules-major-branch v1.0.0
foo.Foo of module: bitbucket.org/bigwhite/modules-major-branch v1.0.0

</code></pre>
<p>从pre-v1到v1在最新的go module机制中还算不上major版本的升级，接下来我们就来看看foo包的作者应该如何对modules-major-branch module做出不兼容的升级：v1 -> v2。</p>
<p>当modules-major-branch module即将做出不兼容升级时，一般会为当前版本建立维护分支(比如：v1分支，并在v1分支上继续对v1版本进行维护、打补丁)，然后再在master分支上做出不兼容的修改。</p>
<pre><code># git checkout -b v1

# git checkout master

# cat foo/foo.go
package foo

import "fmt"

func Foo2() {
    fmt.Println("foo.Foo2 of module: bitbucket.org/bigwhite/modules-major-branch v2.0.0")
}

</code></pre>
<p>从代码可以看到，在master分支上，我们删除了foo包中的Foo函数，新增了Foo2函数。但仅做这些还不够。在本文一开始我们就提到过原则：如果新旧两个包不兼容，那么应该采用不同的导入路径。我们为modules-major-branch module做出了不兼容的修改，也需要modules-major-branch module有着不同的导入路径，我们需要修改modules-major-branch module的module根路径：</p>
<pre><code># cat go.mod
module bitbucket.org/bigwhite/modules-major-branch/v2

go 1.12

# git tag v2.0.0
# git push --tag origin master

</code></pre>
<p>我们在module根路径后面加上了v2，并基于master建立了tag: v2.0.0。</p>
<p>我们再来看看consumer端应该如何应对modules-major-branch module的不兼容修改。如果consumer要使用最新的Foo2函数的话，我们需要对main.go做出如下改动：</p>
<pre><code>//modules-major-branch-test/main.go

package main

import (
    "bitbucket.org/bigwhite/modules-major-branch/v2/foo"
)

func main() {
    foo.Foo2()
}

</code></pre>
<p>接下来我们不需要手工修改modules-major-branch-test的go.mod中依赖，直接运行go run即可：</p>
<pre><code># go run main.go
go: finding bitbucket.org/bigwhite/modules-major-branch/v2/foo latest
go: finding bitbucket.org/bigwhite/modules-major-branch/v2 v2.0.0
go: downloading bitbucket.org/bigwhite/modules-major-branch/v2 v2.0.0
go: extracting bitbucket.org/bigwhite/modules-major-branch/v2 v2.0.0
foo.Foo2 of module: bitbucket.org/bigwhite/modules-major-branch v2.0.0

</code></pre>
<p>我们看到go编译器会自动发现依赖变更，并下载对应的包并更新go.mod和go.num：</p>
<pre><code># cat go.mod
module bitbucket.org/bigwhite/modules-major-branch-test

go 1.12

require (
    bitbucket.org/bigwhite/modules-major-branch v1.0.0
    bitbucket.org/bigwhite/modules-major-branch/v2 v2.0.0 // indirect
)

</code></pre>
<p>modules-major-branch-test此时已经不再需要依赖v1.0.0了，我们可以通过go mod tidy清理一下go.mod中的依赖：</p>
<pre><code># go mod tidy
# cat go.mod
module bitbucket.org/bigwhite/modules-major-branch-test

go 1.12

require bitbucket.org/bigwhite/modules-major-branch/v2 v2.0.0

</code></pre>
<p>我们看到：现在就只剩下对modules-major-branch v2的依赖了。</p>
<p>后续modules-major-branch可以在master分支上持续演进，直到又有不兼容改动时，可以基于master建立v2维护分支，master分支将升级为v3(module)。</p>
<p>再小结一下：</p>
<p>对包的作者而言，升级major版本号需要：</p>
<ul>
<li>
<p>升级module的根路径，增加vN</p>
</li>
<li>
<p>建立vN.x.x形式的tag（可选，如果不打tag，go会在consumer的go.mod中使用伪版本号，比如：bitbucket.org/bigwhite/modules-major-branch/v2 v2.0.0-20190603050009-28a5b8da279e）</p>
</li>
</ul>
<p>如果modules-major-branch内部有相互的包引用，那么在升级major号的时候，这些包的import路径也要增加vN，否则就会存在在高major version的代码中引用低major version包代码的情况，这也是包作者最容易忽略的事情。github.com/marwan-at-work/mod是一个为module作者提供的升级/降级major version号的工具，它可以帮助包作者方便地自动修改项目内所有源文件中的import path。有gopher已经<a href="https://github.com/golang/go/issues/32014">提出希望go官方提供upgrade/downgrade的支持</a>，但目前core team尚未明确是否增加。</p>
<p>对于consumer而言，升级依赖包的major版本号，只需要在import包时在import path中增加vN即可，当然代码中也要针对不兼容的部分进行修改，然后go工具会自动下载相关包。</p>
<h2>三. 使用 “major subdirectory”方案</h2>
<p>go module机制还提供了一种我个人觉得较为怪异的方案或者说用起来不那么自然的方案，那就是利用子目录分割不同主版本。如果某个module目前已经演化到v3版本了，那么这个module所在仓库的目录结构应该是这样的：</p>
<pre><code># tree modules-major-subdir
modules-major-subdir
├── bar
│   └── bar.go
├── go.mod
├── v2
│   ├── bar
│   │   └── bar.go
│   └── go.mod
└── v3
    ├── bar
    │   └── bar.go
    └── go.mod

</code></pre>
<p>这里直接用vN作为子目录名字，在代码仓库中将不同版本module放置在不同的subdir中，这样即便不打tag，通过subdir也能找到对应版本的module包。以上图中的v2为例，该子目录下go.mod如下：</p>
<pre><code># cat go.mod
module bitbucket.org/bigwhite/modules-major-subdir/v2

go 1.12

</code></pre>
<p>v3也是类似。在各自子目录中，module的根路径都是带有vN扩展的。</p>
<p>接下来，我们就来创建consumer来分别调用不同版本的modules-major-subdir/bar包。和modules-major-branch-test类似，我们建立modules-major-subdir-test来作为consumer调用modules-major-subdir/bar包：</p>
<pre><code>// modules-major-subdir-test

# cat go.mod
module bitbucket.org/bigwhite/modules-major-subdir-test

go 1.12

# cat main.go
package main

import (
    "bitbucket.org/bigwhite/modules-major-subdir/bar"
)

func main() {
    bar.Bar()
}

</code></pre>
<p>运行一下consumer：</p>
<pre><code># go run main.go

go: finding bitbucket.org/bigwhite/modules-major-subdir/bar latest
go: finding bitbucket.org/bigwhite/modules-major-subdir latest
go: downloading bitbucket.org/bigwhite/modules-major-subdir v0.0.0-20190603053114-50b15f581aba
go: extracting bitbucket.org/bigwhite/modules-major-subdir v0.0.0-20190603053114-50b15f581aba
bar.Bar of module: bitbucket.org/bigwhite/modules-major-subdir

</code></pre>
<p>我们修改main.go，调用v2版本bar包中Bar2函数：</p>
<pre><code>package main

import (
    "bitbucket.org/bigwhite/modules-major-subdir/v2/bar"
)

func main() {
    bar.Bar2()
}

</code></pre>
<p>再次运行main.go：</p>
<pre><code># go run main.go
go: finding bitbucket.org/bigwhite/modules-major-subdir v0.0.0-20190603053114-50b15f581aba
go: downloading bitbucket.org/bigwhite/modules-major-subdir v0.0.0-20190603053114-50b15f581aba
go: extracting bitbucket.org/bigwhite/modules-major-subdir v0.0.0-20190603053114-50b15f581aba
go: finding bitbucket.org/bigwhite/modules-major-subdir/v2/bar latest
go: finding bitbucket.org/bigwhite/modules-major-subdir/v2 latest
go: downloading bitbucket.org/bigwhite/modules-major-subdir/v2 v2.0.0-20190603063223-4be5d54167e9
go: extracting bitbucket.org/bigwhite/modules-major-subdir/v2 v2.0.0-20190603063223-4be5d54167e9
bar.Bar2 of module: bitbucket.org/bigwhite/modules-major-subdir v2

</code></pre>
<p>我们看到：go编译器自动找到了位于modules-major-subdir仓库下v2子目录下的v2版本bar包。</p>
<p>从demo来看，似乎这种通过subdir方式来实现major version升级的方式更为“简单”一些。但笔者总感觉这种方式有些“怪”，尤其是在与tag交叉使用时可能会带来一些困惑，其他主流语言也鲜有使用这种方式进行major version升级的。另外一旦使用这种方式，似乎也很难利用git工具在不同major版本之间进行代码的merge（复用）了。</p>
<p>另外和major branch方案一样，如果module内部有相互的包引用，那么在升级major号的时候，这些包的import路径也要增加vN，否则也会存在在高major version的代码中引用低major version包代码的情况。</p>
<h2>四. 小结</h2>
<p>Go module作为主流语言依赖管理思路之外的一个“探索性”创新，势必在初期要有一段坎坷的道路要走。好事多磨，相信经过Go 1.11~Go 1.13三个版本的改进以及社区在工具方面对go module的逐渐的完善的支持，Go module会成为gopher日常Go开发的一柄利器，彻底解决Go的包依赖问题。</p>
<p>上述demo源码可在bitbucket.org/bigwhite下找到。</p>
<p>另外这里要提一点：国内的码云(gitee.com)目前对go module major version升级支持的还有问题。同样的操作，但在gitee.com下总是提示：<strong>“go.mod has post-v0 module path”</strong>。</p>
<hr />
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网上线了，感谢小伙伴们学习支持！</p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>我的联系方式：</p>
<p>微博：https://weibo.com/bigwhite20xx<br />
微信公众号：iamtonybai<br />
博客：tonybai.com<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2019, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2019/06/03/the-practice-of-upgrading-major-version-under-go-module/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Go 1.11中值得关注的几个变化</title>
		<link>https://tonybai.com/2018/11/19/some-changes-in-go-1-11/</link>
		<comments>https://tonybai.com/2018/11/19/some-changes-in-go-1-11/#comments</comments>
		<pubDate>Mon, 19 Nov 2018 14:09:48 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[athens]]></category>
		<category><![CDATA[debugger]]></category>
		<category><![CDATA[delve]]></category>
		<category><![CDATA[dep]]></category>
		<category><![CDATA[error-handling]]></category>
		<category><![CDATA[generics]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[go.sum]]></category>
		<category><![CDATA[go1.11]]></category>
		<category><![CDATA[go1.12]]></category>
		<category><![CDATA[GO111MODULE]]></category>
		<category><![CDATA[Go2]]></category>
		<category><![CDATA[GOARCH]]></category>
		<category><![CDATA[gocmpp]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[golang.org]]></category>
		<category><![CDATA[GOOS]]></category>
		<category><![CDATA[GOPATH]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[GopherCon]]></category>
		<category><![CDATA[GOPROXY]]></category>
		<category><![CDATA[html]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[macos]]></category>
		<category><![CDATA[Module]]></category>
		<category><![CDATA[node]]></category>
		<category><![CDATA[node.js]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[RobPike]]></category>
		<category><![CDATA[vendor]]></category>
		<category><![CDATA[vgo]]></category>
		<category><![CDATA[WebAssembly]]></category>
		<category><![CDATA[Windows]]></category>
		<category><![CDATA[兼容性]]></category>
		<category><![CDATA[调试器]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=2655</guid>
		<description><![CDATA[转眼间又近年底，距8月25日Go 1.11版本正式发布已过去快三个月了。由于种种原因，Go语言发布变化系列的Go 1.11版本没能及时放出。近期网课发布上线后，个人时间压力稍缓和。又恰看到近期Go 1.12 release note的initial version已经加入到master，于是这篇文章便上升到个人Todo list的Top3的位置，我也尽一切可能的碎片时间收集素材，撰写文章内容。这个时候谈Go 1.11，总有炒“冷饭”的嫌疑，虽然这碗饭还有一定温度^_^。 一. Go 1.11版本的重要意义 在Go 1.11版本之前的Go user官方调查中，Gopher抱怨最多的三大问题如下： 包依赖管理 缺少泛型 错误处理 而Go 1.11开启了问题1：包依赖管理解决的实验。这表明了社区的声音在影响Go语言演化的过程中扮演着日益重要的角色了。 同时，Go 1.11是Russ Cox在GopherCon 2017大会上发表 “Toward Go2&#8243;之后的第一个Go版本，是为后续“Go2”的渐进落地奠定基础的一个版本。 二. Go 1.11版本变化概述 在”Go2&#8243;声音日渐响亮的今天，兼容性(compatibility)也依旧是Go team考虑的Go语言演化的第一原则，这一点通过Rob Pike在9月份的Go Sydney Meetup上的有关Go 2 Draft Specifications的Talk可以证明(油管视频)。 兼容性依然是”Go2&#8243;的第一考虑 Go 1.11也一如既往版本那样，继续遵守着Go1兼容协议，这意味使用从Go1.0到Go1.10编写的代码理论上依旧可以通过Go 1.11版本编译并正常运行。 随着Go 1.11版本的发布，一些老版本的操作系统将不再被支持，比如Windows XP、macOS 10.9.x等。不被支持不意味着完全不能用，只是Go 1.11在这些老旧os上运行时出现问题将不被官方support了。同时根据Go的release support规定，Go 1.11发布也同时意味着Go 1.9版本将和之前的older go release版本一样，官方将不再提供支持了(关键bug fix、security problem fix等)。 Go [...]]]></description>
			<content:encoded><![CDATA[<p>转眼间又近年底，距8月25日<a href="https://github.com/golang/go/releases/tag/go1.11">Go 1.11版本</a>正式发布已过去快三个月了。由于种种原因，<a href="https://tonybai.com/tag/golang">Go语言发布变化系列</a>的Go 1.11版本没能及时放出。近期<a href="https://tonybai.com/2018/10/17/imooc-course-kubernetes-practice-go-online/">网课发布上线</a>后，个人时间压力稍缓和。又恰看到近期<a href="https://github.com/golang/go/commit/02aa1aeeb1baf9bcfb8b9eeff9c92e93426ae512">Go 1.12 release note的initial version已经加入到master</a>，于是这篇文章便上升到个人Todo list的Top3的位置，我也尽一切可能的碎片时间收集素材，撰写文章内容。这个时候谈Go 1.11，总有炒“冷饭”的嫌疑，虽然这碗饭还有一定温度^_^。</p>
<h2>一. Go 1.11版本的重要意义</h2>
<p>在Go 1.11版本之前的<a href="https://blog.golang.org/survey2017-results">Go user官方调查</a>中，Gopher抱怨最多的三大问题如下：</p>
<ul>
<li>包依赖管理</li>
<li>缺少泛型</li>
<li>错误处理</li>
</ul>
<p>而Go 1.11开启了问题1：<strong>包依赖管理</strong>解决的实验。这表明了社区的声音在影响Go语言演化的过程中扮演着日益重要的角色了。</p>
<p>同时，<a href="https://github.com/golang/go/releases/tag/go1.11">Go 1.11</a>是<a href="https://research.swtch.com/">Russ Cox</a>在<a href="https://www.gophercon.com/">GopherCon</a> 2017大会上发表 <a href="https://blog.golang.org/toward-go2">“Toward Go2&#8243;</a>之后的第一个Go版本，是为后续<a href="https://github.com/golang/go/wiki/Go2">“Go2”</a>的渐进落地奠定基础的一个版本。</p>
<h2>二. Go 1.11版本变化概述</h2>
<p>在”Go2&#8243;声音日渐响亮的今天，兼容性(compatibility)也依旧是Go team考虑的Go语言演化的第一原则，这一点通过Rob Pike在9月份的<a href="https://www.meetup.com/golang-syd/">Go Sydney Meetup</a>上的有关<a href="https://blog.golang.org/go2draft">Go 2 Draft Specifications</a>的<a href="https://www.youtube.com/watch?v=RIvL2ONhFBI">Talk</a>可以证明(油管视频)。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go1.11-go2-primary-concern-by-robpike.png" alt="img{512x368}" /><br />
兼容性依然是”Go2&#8243;的第一考虑</p>
<p>Go 1.11也一如既往版本那样，继续遵守着<a href="https://golang.org/doc/go1compat.html">Go1兼容协议</a>，这意味使用从Go1.0到<a href="https://tonybai.com/2018/02/17/some-changes-in-go-1-10/">Go1.10</a>编写的代码理论上依旧可以通过Go 1.11版本编译并正常运行。</p>
<p>随着Go 1.11版本的发布，一些老版本的操作系统将不再被支持，比如Windows XP、macOS 10.9.x等。不被支持不意味着完全不能用，只是Go 1.11在这些老旧os上运行时出现问题将不被官方support了。同时根据Go的<a href="https://golang.org/doc/devel/release.html">release support规定</a>，Go 1.11发布也同时意味着Go 1.9版本将和之前的older go release版本一样，官方将不再提供支持了(关键bug fix、security problem fix等)。</p>
<p>Go 1.11中为近两年逐渐兴起的<a href="https://en.wikipedia.org/wiki/RISC-V">RISC-V</a>cpu架构预留了GOARCH值：riscv和riscv64。</p>
<p>Go 1.11中为调试器增加了一个新的实验功能，那就是允许在调试过程中动态调用Go函数，比如在断点处调用String方法等。<a href="https://github.com/derekparker/delve">Delve</a> 1.1.0及以上版本可以使用该功能。</p>
<p>在运行时方面，Go 1.11使用了一个稀疏heap布局，这样就去掉了以往Go heap最大512G的限制。</p>
<p>通过Go 1.11编译的Go程序一般来说性能都会有小幅的提升。对于使用math/big包的程序或arm64架构上的Go程序而言，这次的提升尤为明显。</p>
<p>Go 1.11中最大的变化莫过于两点：</p>
<ul>
<li>module机制的实验性引入，以试图解决长久以来困扰Gopher们的包依赖问题；</li>
<li>增加对<a href="https://webassembly.org/">WebAssembly</a>的支持，这样以后Gopher们可以通过Go语言编写前端应用了。</li>
</ul>
<p>Go 1.11的change很多，这是core team和社区共同努力的结果。但在我这个系列文章中，我们只能详细关注少数重要的变化。下面我们就来稍微详细地说说go module和go support WebAssembly这两个显著的变化。</p>
<h2>三. go module</h2>
<p>在Go 1.11 beta2版本发布之前，我曾经基于当时的Go tip版本撰写了一篇 <a href="https://tonybai.com/2018/07/15/hello-go-module/">《初窥go module》</a>的文章，重点描述了go module的实现机制，包括<a href="https://research.swtch.com/vgo-import">Semantic Import Versioning</a>、<a href="https://research.swtch.com/vgo-mvs">Minimal Version Selection</a>等，因此对go module（前身为<a href="https://github.com/golang/vgo">vgo</a>)是什么以及实现机制感兴趣的小伙伴儿们可以先移步到那篇文章了解。在这里我将通过为一个<a href="https://github.com/bigwhite/gocmpp">已存在的repo</a>添加go.mod的方式来描述go module。</p>
<p>这里我们使用的是<a href="https://github.com/golang/go/releases/tag/go1.11.2">go 1.11.2版本</a>，repo为<a href="https://github.com/bigwhite/gocmpp">gocmpp</a>。注意：我们没有显式设置GO111MODULE的值，这样只有在GOPATH之外的路径下，且当前路径下有go.mod或子路径下有go.mod文件时，go compiler才进入module-aware模式(相比较于gopath模式)。</p>
<h3>1. 初始化go.mod</h3>
<p>我们先把gocmpp clone到gopath之外的一个路径下：</p>
<pre><code># git clone https://github.com/bigwhite/gocmpp.git
Cloning into 'gocmpp'...
remote: Enumerating objects: 1, done.
remote: Counting objects: 100% (1/1), done.
remote: Total 950 (delta 0), reused 0 (delta 0), pack-reused 949
Receiving objects: 100% (950/950), 3.85 MiB | 0 bytes/s, done.
Resolving deltas: 100% (396/396), done.
Checking connectivity... done.
</code></pre>
<p>在应用go module之前，我们先来在传统的gopath模式下build一次：</p>
<pre><code># go build
connect.go:24:2: cannot find package "github.com/bigwhite/gocmpp/utils" in any of:
    /root/.bin/go1.11.2/src/github.com/bigwhite/gocmpp/utils (from $GOROOT)
    /root/go/src/github.com/bigwhite/gocmpp/utils (from $GOPATH)
</code></pre>
<p>正如我们所料，由于处于GOPATH外面，且GO111MODULE并未显式设置，Go compiler会尝试在当前目录或子目录下查找go.mod，如果没有go.mod文件，则会采用传统gopath模式编译，即在$GOPATH/src下面找相关的import package，因此失败。</p>
<p>下面我们通过建立go.mod，将编译mode切换为module-aware mode。</p>
<p>我们通过go mod init命令来为gocmpp创建go.mod文件：</p>
<pre><code># go mod init github.com/bigwhite/gocmpp
go: creating new go.mod: module github.com/bigwhite/gocmpp

# cat go.mod
module github.com/bigwhite/gocmpp
</code></pre>
<p>我们看到，go mod init命令在当前目录下创建一个go.mod文件，内有一行内容，描述了该module为 github.com/bigwhite/gocmpp。</p>
<p>我们再来构建一下gocmpp：</p>
<pre><code># go build
go: finding golang.org/x/text/transform latest
go: finding golang.org/x/text/encoding/unicode latest
go: finding golang.org/x/text/encoding/simplifiedchinese latest
go: finding golang.org/x/text v0.3.0
go: finding golang.org/x/text/encoding latest
go: downloading golang.org/x/text v0.3.0
</code></pre>
<p>由于当前目录下有了go.mod文件，go compiler将工作在module-aware模式下，自动分析gocmpp的依赖、确定gocmpp依赖包的初始版本，并下载这些版本的依赖包缓存到特定目录下（目前是存放在$GOPATH/pkg/mod下面）</p>
<pre><code># cat go.mod
module github.com/bigwhite/gocmpp

require golang.org/x/text v0.3.0
</code></pre>
<p>我们看到go.mod中多了一行信息：<strong>“require golang.org/x/text v0.3.0&#8243;</strong>。这就是gocmpp这个module所依赖的第三方包以及经过go compiler初始分析确定使用的版本(v0.3.0)。</p>
<h3>2. 用于verify的go.sum</h3>
<p>go build后，当前目录下还多出了一个go.sum文件。</p>
<pre><code># cat go.sum
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
</code></pre>
<p>go.sum记录每个依赖库的版本和对应的内容的校验和(一个哈希值)。每当增加一个依赖项时，如果go.sum中没有，则会将该依赖项的版本和内容校验和添加到go.sum中。go命令会使用这些校验和与缓存在本地的依赖包副本元信息(比如：$GOPATH/pkg/mod/cache/download/golang.org/x/text/@v下面的v0.3.0.ziphash)进行比对校验。</p>
<p>如果我修改了$GOPATH/pkg/mod/cache/download/golang.org/x/text/@v/v0.3.0.ziphash中的值，那么当我执行下面verify命令时会报错：</p>
<pre><code># go mod verify
golang.org/x/text v0.3.0: zip has been modified (/root/go/pkg/mod/cache/download/golang.org/x/text/@v/v0.3.0.zip)
golang.org/x/text v0.3.0: dir has been modified (/root/go/pkg/mod/golang.org/x/text@v0.3.0)
</code></pre>
<p>如果没有“恶意”修改，则verify会报成功：</p>
<pre><code># go mod verify
all modules verified
</code></pre>
<h3>3. 用why解释为何依赖，给出依赖路径</h3>
<p>go.mod中的依赖项由go相关命令自动生成和维护。但是如果开发人员想知道为什么会依赖某个package，可以通过go mod why命令来查询原因。go mod why命令默认会给出一个main包到要查询的packge的最短依赖路径。如果go mod why使用 -m flag，则后面的参数将被看成是module，并给出main包到每个module中每个package的最短依赖路径（如果依赖的话）：</p>
<p>下面我们通过go mod why命令查看一下gocmpp module到 golang.org/x/oauth2和golang.org/x/exp两个包是否有依赖：</p>
<pre><code># go mod why golang.org/x/oauth2 golang.org/x/exp
go: finding golang.org/x/oauth2 latest
go: finding golang.org/x/exp latest
go: downloading golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288
go: downloading golang.org/x/exp v0.0.0-20181112044915-a3060d491354
go: finding golang.org/x/net/context/ctxhttp latest
go: finding golang.org/x/net/context latest
go: finding golang.org/x/net latest
go: downloading golang.org/x/net v0.0.0-20181114220301-adae6a3d119a
# golang.org/x/oauth2
(main module does not need package golang.org/x/oauth2)

# golang.org/x/exp
(main module does not need package golang.org/x/exp)
</code></pre>
<p>通过结尾几行的输出日志，我们看到gocmpp的main package没有对golang.org/x/oauth2和golang.org/x/exp两个包产生任何依赖。</p>
<p>我们加上-m flag再来执行一遍：</p>
<pre><code># go mod why -m golang.org/x/oauth2 golang.org/x/exp
# golang.org/x/oauth2
(main module does not need module golang.org/x/oauth2)

# golang.org/x/exp
(main module does not need module golang.org/x/exp)
</code></pre>
<p>同样是没有依赖的输出结果，但是输出日志中使用的是module，而不是package字样。说明go mod why将golang.org/x/oauth2和golang.org/x/exp视为module了。</p>
<p>我们再来查询一下对golang.org/x/text的依赖：</p>
<pre><code># go mod why golang.org/x/text
# golang.org/x/text
(main module does not need package golang.org/x/text)

# go mod why -m golang.org/x/text
# golang.org/x/text
github.com/bigwhite/gocmpp/utils
golang.org/x/text/encoding/simplifiedchinese
</code></pre>
<p>我们看到，如果-m flag不开启，那么gocmpp main package没有对golang.org/x/text的依赖路径；如果-m flag开启，则golang.org/x/text被视为module，go mod why会检查gocmpp main package到module: golang.org/x/text下面所有package是否有依赖路径。这里我们看到gocmpp main package依赖了golang.org/x/text module下面的golang.org/x/text/encoding/simplifiedchinese这个package，并给出了最短依赖路径。</p>
<h3>4. 清理go.mod和go.sum中的条目：go mod tidy</h3>
<p>经过上述操作后，我们再来看看go.mod中的内容：</p>
<pre><code># cat go.mod
module github.com/bigwhite/gocmpp

require (
    github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048
    golang.org/x/net v0.0.0-20181114220301-adae6a3d119a // indirect
    golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288 // indirect
    golang.org/x/text v0.3.0
)
</code></pre>
<p>我们发现go.mod中require block增加了许多条目，显然我们的gocmpp并没有依赖到golang.org/x/oauth2和golang.org/x/net中的任何package。我们要清理一下go.mod，使其与gocmpp源码中的第三方依赖的真实情况保持一致，我们使用go mod tidy命令：</p>
<pre><code># go mod tidy
# cat go.mod
module github.com/bigwhite/gocmpp

require (
    github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048
    golang.org/x/text v0.3.0
)

# cat go.sum
github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048 h1:3O5zXlWvrRdioniMPz8pW+pGi+BNEFRtVhvj0GnknbQ=
github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

</code></pre>
<p>我们看到：执行完tidy命令后，go.mod和go.sum都变得简洁了，里面的每一个条目都是gocmpp所真实依赖的package/module的信息。</p>
<h3>5. 对依赖包的版本进行“升降级”(upgrade或downgrade)</h3>
<p>如果对go mod init初始选择的依赖包版本不甚满意，或是第三方依赖包有更新的版本发布，我们日常开发工作中都会进行对对依赖包的版本进行“升降级”(upgrade或downgrade)的操作。在go module模式下，如何来做呢？由于go.mod和go.sum是由go compiler管理的，这里不建议手工去修改go.mod中require中module的版本号。我们可以通过<a href="https://golang.org/cmd/go/#hdr-Modules__module_versions__and_more">module-aware的go get命令</a>来实现我们的目的。</p>
<p>我们先来查看一下golang.org/x/text都有哪些版本可用：</p>
<pre><code># go list -m -versions golang.org/x/text
golang.org/x/text v0.1.0 v0.2.0 v0.3.0
</code></pre>
<p>我们选择将golang.org/x/text从v0.3.0降级到v0.1.0：</p>
<pre><code># go get golang.org/x/text@v0.1.0
go: finding golang.org/x/text v0.1.0
go: downloading golang.org/x/text v0.1.0
</code></pre>
<p>降级后，我们test一下：</p>
<pre><code># go test
PASS
ok      github.com/bigwhite/gocmpp    0.003s
</code></pre>
<p>我们这时再看看go.mod和go.sum：</p>
<pre><code># cat go.mod
module github.com/bigwhite/gocmpp

require (
    github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048
    golang.org/x/text v0.1.0
)

# cat go.sum
github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048 h1:3O5zXlWvrRdioniMPz8pW+pGi+BNEFRtVhvj0GnknbQ=
github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
golang.org/x/text v0.1.0 h1:LEnmSFmpuy9xPmlp2JeGQQOYbPv3TkQbuGJU3A0HegU=
golang.org/x/text v0.1.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
</code></pre>
<p>go.mod中依赖的golang.org/x/text已经从v0.3.0自动变成了v0.1.0了。go.sum中也增加了golang.org/x/text v0.1.0的条目，不过v0.3.0的条目依旧存在。我们可以通过go mod tidy清理一下：</p>
<pre><code># go mod tidy
# cat go.sum
github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048 h1:3O5zXlWvrRdioniMPz8pW+pGi+BNEFRtVhvj0GnknbQ=
github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
golang.org/x/text v0.1.0 h1:LEnmSFmpuy9xPmlp2JeGQQOYbPv3TkQbuGJU3A0HegU=
golang.org/x/text v0.1.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
</code></pre>
<p>go 1.11中的go get也是支持两套工作模式的: 一套是传统gopath mode的；一套是module-aware的。</p>
<p>如果我们在gopath之外的路径，且该路径下没有go.mod，那么go get还是回归gopath mode:</p>
<pre><code># go get golang.org/x/text@v0.1.0
go: cannot use path@version syntax in GOPATH mode
</code></pre>
<p>而module-aware的go get在前面已经演示过了，这里就不重复演示了。</p>
<p>在module-aware模式下，go get -u会更新依赖，升级到依赖的最新minor或patch release。比如：我们在gocmpp module root path下执行：</p>
<pre><code># go get -u golang.org/x/text
# cat go.mod
module github.com/bigwhite/gocmpp

require (
    github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048
    golang.org/x/text v0.3.0 //恢复到0.3.0
)
</code></pre>
<p>我们看到刚刚降级回v0.1.0的依赖项又自动变回v0.3.0了（注意仅minor号变更）。</p>
<p>如果仅仅要升级patch号，而不升级minor号，可以使用go get -u=patch A 。比如：如果golang.org/x/text有v0.1.1版本，那么go get -u=patch golang.org/x/text会将go.mod中的text后面的版本号变为v0.1.1，而不是v0.3.0。</p>
<p>如果go get后面不接具体package，则go get仅针对于main package。</p>
<p>处于module-aware工作模式下的go get更新某个依赖(无论是升版本还是降版本)时，会自动计算并更新其间接依赖的包的版本。</p>
<h3>6. 兼容go 1.11之前版本的reproduceable build: 使用vendor</h3>
<p>处于module-aware mode下的go compiler是完全不理会vendor目录的存在的，go compiler只会使用$GOPATH/pkg/mod下(当前go mod缓存的包是放在这个位置，也许将来会更换位置)缓存的第三方包的特定版本进行编译构建。那么这样一来，对于采用go 1.11之前版本的go compiler来说，reproduceable build就失效了。</p>
<p>为此，go mod提供了vendor子命令，可以根据依赖在module顶层目录自动生成vendor目录：</p>
<pre><code># go mod vendor -v
# github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048
github.com/dvyukov/go-fuzz/gen
# golang.org/x/text v0.3.0
golang.org/x/text/encoding/simplifiedchinese
golang.org/x/text/encoding/unicode
golang.org/x/text/transform
golang.org/x/text/encoding
golang.org/x/text/encoding/internal
golang.org/x/text/encoding/internal/identifier
golang.org/x/text/internal/utf8internal
golang.org/x/text/runes
</code></pre>
<p>gopher可以将vendor目录提交到git repo，这样老版本的go compiler就可以使用vendor进行reproduceable build了。</p>
<p>当然在module-aware mode下，go 1.11 compiler也可以使用vendor进行构建，使用下面命令即可：</p>
<pre><code>go build -mod=vendor

</code></pre>
<p>注意在上述命令中，只有位于module顶层路径的vendor才会起作用。</p>
<h3>7. 国内gopher如何适应go module</h3>
<p>对于国内gopher来说，下载go get package的经历并不是总是那么愉快！尤其是get golang.org/x/xxx路径下的package的时候。以golang.org/x/text为例，在传统的gopath mode下，我们还可以通过下载github.com/golang/text，然后在本地将路径改为golang.org/x/text的方式来获取text相关包。但是在module-aware mode下，对package的下载和本地缓存管理完全由go tool自动完成，国内的gopher们该如何应对呢？</p>
<p>两种方法：<br />
1. 用go.mod中的replace语法，将golang.org/x/text指向本地另外一个目录下已经下载好的github.com/golang/text<br />
2. 使用GOPROXY</p>
<p>方法1显然具有临时性，本地改改第三方依赖库代码，用于调试还可以；第二种方法显然是正解，我们通过一个proxy来下载那些在qiang外的package。Microsoft工程师开源的<a href="https://github.com/gomods/athens">athens项目</a>正是一个用于这个用途的go proxy工具。不过限于篇幅，这里就不展开说明了。我将在后续文章详细谈谈 go proxy的，尤其是使用athens实现go proxy的详细方案。</p>
<h2>四. 对WebAssembly的支持</h2>
<h3>1. 简介</h3>
<p>由于长期在<strong>后端</strong>浸淫，对javascript、<a href="https://webassembly.org/">WebAssembly</a>等前端的技能了解不多，因此这里对Go支持WebAssembly也就能介绍个梗概。下图是对Go支持WebAssembly的一个粗浅的理解：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go1.11-webassembly.png" alt="img{512x368}" /></p>
<p>我们看到满足WebAssembly标准要求的wasm运行于browser之上，类比于一个amd64架构的binary program运行于linux操作系统之上。我们在x86-64的linux上执行go build，实质执行的是:</p>
<pre><code>GOOS=linux GOARCH=amd64 go build ...
</code></pre>
<p>因此为了将Go源码编译为wasm，我们需要执行：</p>
<pre><code>GOOS=js GOARCH=wasm go build ...
</code></pre>
<p>同时， <em>_js.go和 *_wasm.go这样的文件也和</em>_linux.go、*_amd64.go一样，会被go compiler做特殊处理。</p>
<h3>2. 一个hello world级别的WebAssembly的例子</h3>
<p>例子来自<a href="https://github.com/golang/go/wiki/WebAssembly">Go官方Wiki</a>，代码结构如下：</p>
<pre><code>/Users/tony/test/Go/wasm/hellowasm git:(master) ✗ $tree
.
├── hellowasm.go
├── index.html
└── server.go
</code></pre>
<p>hellowasm.go是最终wasm应用对应的源码：</p>
<pre><code>// hellowasm.go

package main

import "fmt"

func main() {
    fmt.Println("Hello, WebAssembly!")
}

</code></pre>
<p>我们先将其编译为wasm文件main.wasm：</p>
<pre><code>$GOOS=js GOARCH=wasm go build -o main.wasm hellowasm.go
$ls -F
hellowasm.go    index.html    main.wasm*    server.go
</code></pre>
<p>接下来我们从Goroot下面copy一个javascript支持文件wasm_exec.js：</p>
<pre><code>cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .
</code></pre>
<p>我们建立index.html，并在该文件中使用wasm_exec.js，并加载main.wasm：</p>
<pre><code>//index.html
&lt;html&gt;
        &lt;head&gt;
                &lt;meta charset="utf-8"&gt;
                &lt;script src="wasm_exec.js"&gt;&lt;/script&gt;
                &lt;script&gt;
                        const go = new Go();
                        WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) =&gt; {
                                go.run(result.instance);
                        });
                &lt;/script&gt;
        &lt;/head&gt;
        &lt;body&gt;&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>最后，我们建立server.go，这是一个File server：</p>
<pre><code>//server.go
package main

import (
    "flag"
    "log"
    "net/http"
)

var (
    listen = flag.String("listen", ":8080", "listen address")
    dir    = flag.String("dir", ".", "directory to serve")
)

func main() {
    flag.Parse()
    log.Printf("listening on %q...", *listen)
    err := http.ListenAndServe(*listen, http.FileServer(http.Dir(*dir)))
    log.Fatalln(err)
}

</code></pre>
<p>启动该server:</p>
<pre><code>$go run server.go
2018/11/19 21:19:17 listening on ":8080"...
</code></pre>
<p>打开Chrome浏览器，右键打开Chrome的“检查”页面，访问127.0.0.1:8080，我们将在console（控制台）窗口看到下面内容：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go1.11-webassembly-browser-exec-result.png" alt="img{512x368}" /></p>
<p>我们看到”Hello, WebAssembly”字样输出到console上了！</p>
<h3>3. 使用node.js执行wasm应用</h3>
<p>wasm应用除了可以运行于支持WebAssembly的浏览器上之外，还可以通过node.js运行它。</p>
<p>我的实验环境中安装的node版本是:</p>
<pre><code>$node -v
v9.11.1
</code></pre>
<p>我们删除server.go，然后执行下面命令：</p>
<pre><code>$GOOS=js GOARCH=wasm go run -exec="$(go env GOROOT)/misc/wasm/go_js_wasm_exec" .
Hello, WebAssembly!
</code></pre>
<p>我们看到通过go_js_wasm_exec命令我们成功通过node执行了main.wasm。</p>
<p>不过每次通过go run -exec来执行，命令行太长，不易记住和使用。我们将go_js_wasm_exec放到$PATH下面，然后直接执行go run：</p>
<pre><code> $export PATH=$PATH:"$(go env GOROOT)/misc/wasm"
 $which go_js_wasm_exec
/Users/tony/.bin/go1.11.2/misc/wasm/go_js_wasm_exec
$GOOS=js GOARCH=wasm go run .
Hello, WebAssembly!

</code></pre>
<p>main.wasm同样被node执行，并且这样执行main.wasm程序的命令行长度大大缩短了！</p>
<h2>五. 小结</h2>
<p>从Go 1.11版本开始，Go语言开始驶入“语言演化”的深水区。Go语言究竟该如何演化？如何在保持语言兼容性、社区不分裂的前提下，满足社区对于错误处理、泛型等语法特性的需求，是摆在Go设计者面前的一道难题。但我相信，无论Go如何演化，Go设计者都会始终遵循Go语言安身立命的那几个根本原则，也是大多数Gopher喜欢Go的根本原因：兼容、简单、可读和高效。</p>
<hr />
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网上线了，感谢小伙伴们学习支持！</p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>我的联系方式：</p>
<p>微博：https://weibo.com/bigwhite20xx<br />
微信公众号：iamtonybai<br />
博客：tonybai.com<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2018, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2018/11/19/some-changes-in-go-1-11/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>初窥Go module</title>
		<link>https://tonybai.com/2018/07/15/hello-go-module/</link>
		<comments>https://tonybai.com/2018/07/15/hello-go-module/#comments</comments>
		<pubDate>Sun, 15 Jul 2018 03:41:37 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[branch]]></category>
		<category><![CDATA[dep]]></category>
		<category><![CDATA[generics]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[glide]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-get]]></category>
		<category><![CDATA[go1.11]]></category>
		<category><![CDATA[Go1.5]]></category>
		<category><![CDATA[gobuild]]></category>
		<category><![CDATA[goget]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[govendor]]></category>
		<category><![CDATA[import]]></category>
		<category><![CDATA[KenThompson]]></category>
		<category><![CDATA[mainline]]></category>
		<category><![CDATA[Module]]></category>
		<category><![CDATA[monorepo]]></category>
		<category><![CDATA[mvs]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[RobertGriesemer]]></category>
		<category><![CDATA[RobPike]]></category>
		<category><![CDATA[RussCox]]></category>
		<category><![CDATA[SemanticImportVersioning]]></category>
		<category><![CDATA[semver]]></category>
		<category><![CDATA[tag]]></category>
		<category><![CDATA[TIOBE]]></category>
		<category><![CDATA[toolchain]]></category>
		<category><![CDATA[trunk]]></category>
		<category><![CDATA[vendor]]></category>
		<category><![CDATA[vgo]]></category>
		<category><![CDATA[传递依赖]]></category>
		<category><![CDATA[依赖管理]]></category>
		<category><![CDATA[包]]></category>
		<category><![CDATA[最小版本选择]]></category>
		<category><![CDATA[泛型]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[语义化版本]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=2625</guid>
		<description><![CDATA[自2007年“三巨头（Robert Griesemer, Rob Pike, Ken Thompson）”提出设计和实现Go语言以来，Go语言已经发展和演化了十余年了。这十余年来，Go取得了巨大的成就，先后在2009年和2016年当选TIOBE年度最佳编程语言，并在全世界范围内拥有数量庞大的拥趸。不过和其他主流编程语言一样，Go语言也不是完美的，不能满足所有开发者的“口味”。这些年来Go在“包依赖管理”和“缺少泛型”两个方面饱受诟病，它们也是Go粉们最希望Go核心Team重点完善的两个方面。 今年(2018)年初，Go核心Team的技术leader，也是Go Team最早期成员之一的Russ Cox在个人博客上连续发表了七篇文章，系统阐述了Go team解决“包依赖管理”的技术方案: vgo。vgo的主要思路包括：Semantic Import Versioning、Minimal Version Selection、引入Go module等。这七篇文章的发布引发了Go社区激烈地争论，尤其是MVS(最小版本选择)与目前主流的依赖版本选择方法的相悖让很多传统Go包管理工具的维护者“不满”，尤其是“准官方工具”：dep。vgo方案的提出也意味着dep项目的生命周期即将进入尾声。 5月份，Russ Cox的Proposal “cmd/go: add package version support to Go toolchain”被accepted，这周五早些时候Russ Cox将vgo的代码merge到Go主干，并将这套机制正式命名为“go module”。由于vgo项目本身就是一个实验原型，merge到主干后，vgo这个术语以及vgo项目的使命也就就此结束了。后续Go modules机制将直接在Go主干上继续演化。 Go modules是go team在解决包依赖管理方面的一次勇敢尝试，无论如何，对Go语言来说都是一个好事。在本篇文章中，我们就一起来看看这个新引入的go modules机制。 一. 建立试验环境 由于加入go modules experiment机制的Go 1.11版本尚未正式发布，且go 1.11 beta1版本发布在go modules merge到主干之前，因此我们要进行go module试验只能使用Go tip版本，即主干上的最新版本。我们需要通过编译Go源码包的方式获得支持go module的go编译器： 编译Go项目源码的前提是你已经安装了一个发布版，比如Go 1.10.3。然后按照下面步骤执行即可： $ git clone https://github.com/golang/go.git $ mv go [...]]]></description>
			<content:encoded><![CDATA[<p>自2007年“三巨头（<a href="https://github.com/griesemer">Robert Griesemer</a>, <a href="https://en.wikipedia.org/wiki/Rob_Pike">Rob Pike</a>, <a href="https://en.wikipedia.org/wiki/Ken_Thompson">Ken Thompson</a>）”提出设计和实现<a href="https://tonybai.com/tag/go">Go语言</a>以来，<a href="https://golang.org">Go语言</a>已经发展和演化了<a href="https://tonybai.com/2017/09/24/go-ten-years-and-climbing/">十余年</a>了。这十余年来，Go取得了巨大的成就，先后在2009年和2016年当选<a href="https://www.tiobe.com/tiobe-index/">TIOBE</a>年度最佳编程语言，并在全世界范围内拥有数量庞大的拥趸。不过和其他主流编程语言一样，Go语言也不是完美的，不能满足所有开发者的“口味”。这些年来Go在“包依赖管理”和“缺少泛型”两个方面饱受诟病，它们也是Go粉们最希望Go核心Team重点完善的两个方面。</p>
<p>今年(2018)年初，Go核心Team的技术leader，也是Go Team最早期成员之一的<a href="https://research.swtch.com/">Russ Cox</a>在<a href="https://research.swtch.com/">个人博客</a>上连续发表了<a href="https://research.swtch.com/vgo">七篇文章</a>，系统阐述了Go team解决“包依赖管理”的技术方案: <a href="https://github.com/golang/vgo">vgo</a>。vgo的主要思路包括：<a href="https://research.swtch.com/vgo-import">Semantic Import Versioning</a>、<a href="https://research.swtch.com/vgo-mvs">Minimal Version Selection</a>、<a href="https://research.swtch.com/vgo-module">引入Go module</a>等。这七篇文章的发布引发了Go社区激烈地争论，尤其是MVS(最小版本选择)与目前主流的依赖版本选择方法的相悖让很多传统Go包管理工具的维护者“不满”，尤其是“准官方工具”：<a href="https://tonybai.com/tag/dep">dep</a>。vgo方案的提出也意味着dep项目的生命周期即将进入尾声。</p>
<p>5月份，Russ Cox的<a href="https://github.com/golang/go/issues/24301">Proposal “cmd/go: add package version support to Go toolchain”</a>被accepted，这周五早些时候Russ Cox<a href="https://github.com/golang/go/commit/f7248f05946c1804b5519d0b3eb0db054dc9c5d6">将vgo的代码merge到Go主干</a>，并将这套机制正式命名为“go module”。由于vgo项目本身就是一个实验原型，merge到主干后，<strong>vgo这个术语以及vgo项目的使命也就就此结束了</strong>。后续Go modules机制将直接在Go主干上继续演化。</p>
<p>Go modules是go team在解决包依赖管理方面的一次勇敢尝试，无论如何，对Go语言来说都是一个好事。在本篇文章中，我们就一起来看看这个新引入的go modules机制。</p>
<h2>一. 建立试验环境</h2>
<p>由于加入go modules experiment机制的Go 1.11版本尚未正式发布，且<a href="https://github.com/golang/go/releases/tag/go1.11beta1">go 1.11 beta1版本</a>发布在go modules merge到主干之前，因此我们要进行go module试验只能使用Go tip版本，即主干上的最新版本。我们需要通过编译Go源码包的方式获得支持go module的go编译器：</p>
<p>编译<a href="https://github.com/golang/go">Go项目源码</a>的前提是你已经安装了一个发布版，比如<a href="https://github.com/golang/go/releases/tag/go1.10.3">Go 1.10.3</a>。然后按照下面步骤执行即可：</p>
<pre><code>$ git clone https://github.com/golang/go.git
$ mv go go-tip
$ cd go-tip
$ ./all.bash
Building Go cmd/dist using /root/.bin/go1.10.2.
Building Go toolchain1 using /root/.bin/go1.10.2.
Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1.
Building Go toolchain2 using go_bootstrap and Go toolchain1.
Building Go toolchain3 using go_bootstrap and Go toolchain2.
Building packages and commands for linux/amd64.
##### Testing packages.
ok      archive/tar    0.026s
... ...
##### API check

ALL TESTS PASSED
---
Installed Go for linux/amd64 in /root/.bin/go-tip
Installed commands in /root/.bin/go-tip/bin
*** You need to add /root/.bin/go-tip/bin to your PATH.
</code></pre>
<p>验证源码编译方式的安装结果：</p>
<pre><code># ./go version
go version devel +a241922 Fri Jul 13 00:03:31 2018 +0000 linux/amd64
</code></pre>
<p>查看有关go module的手册：</p>
<pre><code>$  ./go help mod
usage: go mod [-v] [maintenance flags]

Mod performs module maintenance operations as specified by the
following flags, which may be combined.

The -v flag enables additional output about operations performed.

The first group of operations provide low-level editing operations
for manipulating go.mod from the command line or in scripts or
other tools. They read only go.mod itself; they do not look up any
information about the modules involved.

The -init flag initializes and writes a new go.mod to the current directory,
in effect creating a new module rooted at the current directory.
The file go.mod must not already exist.
If possible, mod will guess the module path from import comments
(see 'go help importpath') or from version control configuration.
To override this guess, use the -module flag.
(Without -init, mod applies to the current module.)

The -module flag changes (or, with -init, sets) the module's path
(the go.mod file's module line).
... ...

</code></pre>
<p>无法通过编译源码的方式获取go tip版的小伙伴们也不用着急，在后续即将发布的go 1.11 beta2版本中将会包含对go modules的支持，到时候按常规方式安装beta2即可体验go modules。</p>
<h2>二. 传统Go构建以及包依赖管理的回顾</h2>
<p>Go在构建设计方面深受Google内部开发实践的影响，比如go get的设计就深受<a href="https://cacm.acm.org/magazines/2016/7/204032-why-google-stores-billions-of-lines-of-code-in-a-single-repository/pdf">Google内部单一代码仓库(single monorepo)和基于主干(trunk/mainline based)的开发模型</a>的影响：只获取Trunk/mainline代码和版本无感知。</p>
<p><img src="https://tonybai.com/wp-content/uploads/google-trunk-based-and-release-branch-dev-model.png" alt="img{512x368}" /></p>
<blockquote>
<p>Google内部基于主干的开发模型：<br />
   &#8211; 所有开发人员基于主干trunk/mainline开发：提交到trunk或从trunk获取最新的代码（同步到本地workspace）<br />
   &#8211; 版本发布时，建立Release branch，release branch实质上就是某一个时刻主干代码的快照；<br />
   &#8211; 必须同步到release branch上的bug fix和增强改进代码也通常是先在主干上提交(commit)，然后再cherry-pick到release branch上</p>
</blockquote>
<p>我们知道go get获取的代码会放在$GOPATH/src下面，而go build会在$GOROOT/src和$GOPATH/src下面按照import path去搜索package，由于go get 获取的都是各个package repo的trunk/mainline的代码，因此，<a href="https://tonybai.com/2015/07/10/some-changes-in-go-1-5/">Go 1.5</a>之前的Go compiler都是基于目标Go程序依赖包的trunk/mainline代码去编译的。这样的机制带来的问题是显而易见的，至少包括：</p>
<ul>
<li>因依赖包的trunk的变化，导致不同人获取和编译你的包/程序时得到的结果实质是不同的，即不能实现reproduceable build</li>
<li>因依赖包的trunk的变化，引入不兼容的实现，导致你的包/程序无法通过编译</li>
<li>因依赖包演进而无法通过编译，导致你的包/程序无法通过编译</li>
</ul>
<p>为了实现reporduceable build，Go 1.5引入了<a href="https://tonybai.com/2015/07/31/understand-go15-vendor/">Vendor机制</a>，Go编译器会优先在vendor下搜索依赖的第三方包，这样如果开发者将特定版本的依赖包存放在vendor下面并提交到code repo，那么所有人理论上都会得到同样的编译结果，从而实现reporduceable build。</p>
<p>在Go 1.5发布后的若干年，gopher们把注意力都集中在如何利用vendor解决包依赖问题，从手工添加依赖到vendor、手工更新依赖，到一众包依赖管理工具的诞生：比如: <a href="https://github.com/kardianos/govendor">govendor</a>、<a href="https://github.com/Masterminds/glide">glide</a>以及号称准官方工具的<a href="https://github.com/golang/dep">dep</a>，努力地尝试着按照当今主流思路解决着诸如：“钻石型依赖”等难题。</p>
<p>正当gopher认为dep将“顺理成章”地升级为go toolchain一部分的时候，vgo横空出世，并通过对“Semantic Import Versioning”和”Minimal Version Selected”的设定，在原Go tools上简单快速地实现了Go原生的包依赖管理方案 。vgo就是go module的前身。</p>
<h2>三. go modules定义、experiment开关以及“依赖管理”的工作模式</h2>
<p><strong>通常</strong>我们会在一个repo(仓库)中创建一组Go package，repo的路径比如：github.com/bigwhite/gocmpp会作为go package的导入路径(import path)，Go 1.11给这样的一组在同一repo下面的packages赋予了一个新的抽象概念: module，并启用一个新的文件go.mod记录module的元信息。</p>
<p>不过一个repo对应一个module这种说法其实并不精确也并不正确，一个repo当然可以拥有多个module，很多公司或组织是喜欢用monorepo的，这样势必有在单一的monorepo建立多个module的需求，显然go modules也是支持这种情况的。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-module-single-repo-single-mod.png" alt="img{512x368}" /><br />
图：single repo，single module</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-module-single-repo-multi-mods.png" alt="img{512x368}" /><br />
图：single monorepo，multiple modules</p>
<p>是时候上代码了！</p>
<p>我们在~/test下建立hello目录（注意：$GOPATH=~/go，显然hello目录并不在GOPATH下面）。hello.go的代码如下：</p>
<pre><code>// hello.go
package main

import "bitbucket.org/bigwhite/c"

func main() {
    c.CallC()
}
</code></pre>
<p>我们构建一下hello.go这个源码文件：</p>
<pre><code># go build hello.go
hello.go:3:8: cannot find package "bitbucket.org/bigwhite/c" in any of:
    /root/.bin/go-tip/src/bitbucket.org/bigwhite/c (from $GOROOT)
    /root/go/src/bitbucket.org/bigwhite/c (from $GOPATH)
</code></pre>
<p>构建错误！错误原因很明了：在本地的GOPATH下并没有找到bitbucket.org/bigwhite/c路径的package c。传统fix这个问题的方法是手工将package c通过go get下载到本地(并且go get会自动下载package c所依赖的package d)：</p>
<pre><code># go get bitbucket.org/bigwhite/c
# go run hello.go
call C: master branch
   --&gt; call D:
    call D: master branch
   --&gt; call D end

</code></pre>
<p>这种我们最熟悉的Go compiler从$GOPATH下(以及vendor目录下)搜索目标程序的依赖包的模式称为：<strong>“GOPATH mode”</strong>。</p>
<p>GOPATH是Go最初设计的产物，在Go语言快速发展的今天，人们日益发现GOPATH似乎不那么重要了，尤其是在引入vendor以及诸多包管理工具后。并且GOPATH的设置还会让Go语言新手感到些许困惑，提高了入门的门槛。Go core team也一直在寻求“去GOPATH”的方案，当然这一过程是循序渐进的。<a href="https://tonybai.com/2017/02/03/some-changes-in-go-1-8/">Go 1.8</a>版本中，如果开发者没有显式设置GOPATH，Go会赋予GOPATH一个默认值（在linux上为$HOME/go）。虽说不用再设置GOPATH，但GOPATH还是事实存在的，它在go toolchain中依旧发挥着至关重要的作用。</p>
<p>Go module的引入在Go 1.8版本上更进了一步，它引入了一种新的依赖管理mode：“module-aware mode”。在该mode下，某源码树(通常是一个repo)的顶层目录下会放置一个go.mod文件，每个go.mod文件定义了一个module，而放置go.mod文件的目录被称为module root目录（通常对应一个repo的root目录，但不是必须的）。module root目录以及其子目录下的所有Go package均归属于该module，除了那些自身包含go.mod文件的子目录。</p>
<p>在“module-aware mode”下，go编译器将不再在GOPATH下面以及vendor下面搜索目标程序依赖的第三方Go packages。我们来看一下在“module-aware mode”下hello.go的构建过程：</p>
<p>我们首先在~/test/hello下创建go.mod:</p>
<pre><code>// go.mod
module hello

</code></pre>
<p>然后构建hello.go</p>
<pre><code># go build hello.go
go: finding bitbucket.org/bigwhite/d v0.0.0-20180714005150-3e3f9af80a02
go: finding bitbucket.org/bigwhite/c v0.0.0-20180714063616-861b08fcd24b
go: downloading bitbucket.org/bigwhite/c v0.0.0-20180714063616-861b08fcd24b
go: downloading bitbucket.org/bigwhite/d v0.0.0-20180714005150-3e3f9af80a02

# ./hello
call C: master branch
   --&gt; call D:
    call D: master branch
   --&gt; call D end
</code></pre>
<p>我们看到go compiler并没有去使用之前已经下载到GOPATH下的bitbucket.org/bigwhite/c和bitbucket.org/bigwhite/d，而是主动下载了这两个包并成功编译。我们看看执行go build后go.mod文件的内容：</p>
<pre><code># cat go.mod
module hello

require (
    bitbucket.org/bigwhite/c v0.0.0-20180714063616-861b08fcd24b
    bitbucket.org/bigwhite/d v0.0.0-20180714005150-3e3f9af80a02 // indirect
)

</code></pre>
<p>我们看到go compiler分析出了hello module的依赖，将其放入go.mod的require区域。由于c、d两个package均没有版本发布(打tag)，因此go compiler使用了c、d的当前最新版，并以Pseudo-versions的形式记录之。并且我们看到：hello module并没有直接依赖d package，因此在d的记录后面通过注释形式标记了indirect，即非直接依赖，也就是传递依赖。</p>
<p>在“module-aware mode”下，go compiler将下载的依赖包缓存在$GOPATH/pkg/mod下面：</p>
<pre><code>// $GOPATH/pkg/mod
# tree -L 3
.
├── bitbucket.org
│   └── bigwhite
│       ├── c@v0.0.0-20180714063616-861b08fcd24b
│       └── d@v0.0.0-20180714005150-3e3f9af80a02
├── cache
│   ├── download
│   │   ├── bitbucket.org
│   │   ├── golang.org
│   │   └── rsc.io
│   └── vcs
│       ├── 064503657de46d4574a6ab937a7a3b88fee03aec15729f7493a3dc8e35cc6d80
│       ├── 064503657de46d4574a6ab937a7a3b88fee03aec15729f7493a3dc8e35cc6d80.info
│       ├── 0c8659d2f971b567bc9bd6644073413a1534735b75ea8a6f1d4ee4121f78fa5b
... ...
</code></pre>
<p>我们看到c、d两个package也是按照“版本”进行缓存的，便于后续在“module-aware mode”下进行包构建使用。</p>
<p>Go modules机制在go 1.11中是experiment feature，按照Go的惯例，在新的experiment feature首次加入时，都会有一个特性开关，go modules也不例外，<strong>GO111MODULE</strong>这个临时的环境变量就是go module特性的experiment开关。GO111MODULE有三个值：auto、on和off，默认值为auto。<strong>GO111MODULE</strong>的值会直接影响Go compiler的“依赖管理”模式的选择（是GOPATH mode还是module-aware mode），我们详细来看一下：</p>
<ul>
<li>
<p>当GO111MODULE的值为off时，go modules experiment feature关闭，go compiler显然会始终使用<strong>GOPATH mode</strong>，即无论要构建的源码目录是否在GOPATH路径下，go compiler都会在传统的GOPATH和vendor目录(仅支持在gopath目录下的package)下搜索目标程序依赖的go package；</p>
</li>
<li>
<p>当GO111MODULE的值为on时（export GO111MODULE=on），go modules experiment feature始终开启，与off相反，go compiler会始终使用<strong>module-aware mode</strong>，即无论要构建的源码目录是否在GOPATH路径下，go compiler都<strong>不会</strong>在传统的GOPATH和vendor目录下搜索目标程序依赖的go package，而是在go mod命令的缓存目录($GOPATH/pkg/mod）下搜索对应版本的依赖package；</p>
</li>
<li>
<p>当GO111MODULE的值为auto时(不显式设置即为auto)，也就是我们在上面的例子中所展现的那样：使用GOPATH mode还是module-aware mode，取决于要构建的源码目录所在位置以及是否包含go.mod文件。如果要构建的源码目录不在以GOPATH/src为根的目录体系下，且包含go.mod文件(两个条件缺一不可)，那么使用module-aware mode；否则使用传统的GOPATH mode。</p>
</li>
</ul>
<h2>四. go modules的依赖版本选择</h2>
<h3>1. build list和main module</h3>
<p>go.mod文件一旦创建后，它的内容将会被go toolchain全面掌控。go toolchain会在各类命令执行时，比如go get、go build、go mod等修改和维护go.mod文件。</p>
<p>之前的例子中，hello module依赖的c、d(indirect)两个包均没有显式的版本信息（比如: v1.x.x），因此go mod使用Pseudo-versions机制来生成和记录c, d的“版本”，我们可以通过下面命令查看到这些信息：</p>
<pre><code># go list -m -json all
{
    "Path": "hello",
    "Main": true,
    "Dir": "/root/test/hello"
}
{
    "Path": "bitbucket.org/bigwhite/c",
    "Version": "v0.0.0-20180714063616-861b08fcd24b",
    "Time": "2018-07-14T06:36:16Z",
    "Dir": "/root/go/pkg/mod/bitbucket.org/bigwhite/c@v0.0.0-20180714063616-861b08fcd24b"
}
{
    "Path": "bitbucket.org/bigwhite/d",
    "Version": "v0.0.0-20180714005150-3e3f9af80a02",
    "Time": "2018-07-14T00:51:50Z",
    "Indirect": true,
    "Dir": "/root/go/pkg/mod/bitbucket.org/bigwhite/d@v0.0.0-20180714005150-3e3f9af80a02"
}
</code></pre>
<p>go list -m输出的信息被称为<strong>build list</strong>，也就是构建当前module所要构建的所有相关package（及版本）的列表。在输出信息中我们看到 “Main”: true这一信息，标识当前的module为<strong>“main module”</strong>。所谓main module，即是go build命令执行时所在当前目录所归属的那个module，go命令会在当前目录、当前目录的父目录、父目录的父目录&#8230;等下面寻找go.mod文件，所找到的第一个go.mod文件对应的module即为main module。如果没有找到go.mod，go命令会提示下面错误信息：</p>
<pre><code># go build test/hello/hello.go
go: cannot find main module root; see 'go help modules'
</code></pre>
<p>当然我们也可以使用下面命令简略输出build list：</p>
<pre><code># go list -m all
hello
bitbucket.org/bigwhite/c v0.0.0-20180714063616-861b08fcd24b
bitbucket.org/bigwhite/d v0.0.0-20180714005150-3e3f9af80a02
</code></pre>
<h3>2. module requirement</h3>
<p>现在我们给c、d两个package打上版本信息：</p>
<pre><code>package c:
v1.0.0
v1.1.0
v1.2.0

package d:
v1.0.0
v1.1.0
v1.2.0
v1.3.0
</code></pre>
<p>然后清除掉$GOPATH/pkg/mod目录，并将hello.mod重新置为初始状态（只包含module字段）。接下来，我们再来构建一次hello.go:</p>
<pre><code>// ~/test/hello目录下

# go build hello.go
go: finding bitbucket.org/bigwhite/c v1.2.0
go: downloading bitbucket.org/bigwhite/c v1.2.0
go: finding bitbucket.org/bigwhite/d v1.3.0
go: downloading bitbucket.org/bigwhite/d v1.3.0

# ./hello
call C: v1.2.0
   --&gt; call D:
    call D: v1.3.0
   --&gt; call D end

# cat go.mod
module hello

require (
    bitbucket.org/bigwhite/c v1.2.0 // indirect (c package被标记为indirect，这似乎是当前版本的一个bug)
    bitbucket.org/bigwhite/d v1.3.0 // indirect
)
</code></pre>
<p>我们看到，再一次初始构建hello module时，Go compiler不再用最新的commit revision所对应的Pseudo-version，而是使用了c、d两个package的最新发布版（c:v1.2.0，d: v1.3.0）。</p>
<p>如果我们对使用的c、d版本有特殊约束，比如：我们使用package c的v1.0.0，package d的v1.1.0版本，我们可以通过go mod -require来操作go.mod文件，更新go.mod文件中的require段的信息：</p>
<pre><code># go mod -require=bitbucket.org/bigwhite/c@v1.0.0
# go mod -require=bitbucket.org/bigwhite/d@v1.1.0

# cat go.mod
module hello

require (
    bitbucket.org/bigwhite/c v1.0.0 // indirect
    bitbucket.org/bigwhite/d v1.1.0 // indirect
)

# go build hello.go
go: finding bitbucket.org/bigwhite/d v1.1.0
go: finding bitbucket.org/bigwhite/c v1.0.0
go: downloading bitbucket.org/bigwhite/c v1.0.0
go: downloading bitbucket.org/bigwhite/d v1.1.0

# ./hello
call C: v1.0.0
   --&gt; call D:
    call D: v1.1.0
   --&gt; call D end
</code></pre>
<p>我们看到由于我们显式地修改了对package c、d两个包的版本依赖约束，go build构建时会去下载package c的v1.0.0和package d的v1.1.0版本并完成构建。</p>
<h3>3. module query</h3>
<p>除了通过传入package@version给go mod -requirement来精确“指示”module依赖之外，go mod还支持query表达式，比如：</p>
<pre><code># go mod -require='bitbucket.org/bigwhite/c@&gt;=v1.1.0'
</code></pre>
<p>go mod会对query表达式做求值，得出build list使用的package c的版本:</p>
<pre><code># cat go.mod
module hello

require (
    bitbucket.org/bigwhite/c v1.1.0
    bitbucket.org/bigwhite/d v1.1.0 // indirect
)

# go build hello.go
go: downloading bitbucket.org/bigwhite/c v1.1.0
# ./hello
call C: v1.1.0
   --&gt; call D:
    call D: v1.1.0
   --&gt; call D end

</code></pre>
<p>go mod对module query进行求值的算法是“选择最接近于比较目标的版本(tagged version)”。以上面例子为例：</p>
<pre><code>query text: &gt;=v1.1.0
比较的目标版本为v1.1.0
比较形式：&gt;=
</code></pre>
<p>因此，满足这一query的最接近于比较目标的版本(tagged version)就是v1.1.0。</p>
<p>如果我们给package d增加一个约束“小于v1.3.0”，我们再来看看go mod的选择：</p>
<pre><code># go mod -require='bitbucket.org/bigwhite/d@&lt;v1.3.0'
# cat go.mod
module hello

require (
    bitbucket.org/bigwhite/c v1.1.0 // indirect
    bitbucket.org/bigwhite/d &lt;v1.3.0
)

# go build hello.go
go: finding bitbucket.org/bigwhite/d v1.2.0
go: downloading bitbucket.org/bigwhite/d v1.2.0

# ./hello
call C: v1.1.0
   --&gt; call D:
    call D: v1.2.0
   --&gt; call D end
</code></pre>
<p>我们看到go mod选择了package d的v1.2.0版本，根据module query的求值算法，v1.2.0恰是最接近于“小于v1.3.0”的tagged version。</p>
<p>用下面这幅示意图来呈现这一算法更为直观一些：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-mod-query-pkg-version-select.png" alt="img{512x368}" /></p>
<h3>4. minimal version selection(mvs)</h3>
<p>到目前为止，我们所使用的example都是最最简单的，hello module所依赖的package c和package d并没有自己的go.mod，也没有定义自己的requirements。对于复杂的包依赖场景，Russ Cox在<a href="https://research.swtch.com/vgo-mvs">“Minimal Version Selection”</a>一文中给过形象的算法解释(注意：这个算法仅是便于人类理解，但是性能低下，真正的实现并非按照这个算法实现)：</p>
<p><img src="https://tonybai.com/wp-content/uploads/rsc-mvs-version-select-1.png" alt="img{512x368}" /><br />
例子情景</p>
<p><img src="https://tonybai.com/wp-content/uploads/rsc-mvs-version-select-list.png" alt="img{512x368}" /><br />
算法的形象解释</p>
<p>MVS以build list为中心，从一个空的build list集合开始，先加入main module(A1)，然后递归计算main module的build list，我们看到在这个过程中，先得到C 1.2的build list，然后是B 1.2的build list，去重合并后形成A1的rough build list，选择集合中每个module的最新version，最终形成A1的build list。</p>
<p>我们改造一下我们的例子，让它变得复杂些！</p>
<p>首先，我们为package c添加go.mod文件，并为其打一个新版本：v1.3.0：</p>
<pre><code>//bitbucket.org/bigwhite/c/go.mod
module bitbucket.org/bigwhite/c

require (
        bitbucket.org/bigwhite/d v1.2.0
)

</code></pre>
<p>在module bitbucket.org/bigwhite/c的module文件中，我们为其添加一个requirment: bitbucket.org/bigwhite/d@v1.2.0。</p>
<p>接下来，我们将hello module重置为初始状态，并删除$GOPATH/pkg/mod目录。我们修改一下hello module的hello.go如下：</p>
<pre><code>package main

import "bitbucket.org/bigwhite/c"
import "bitbucket.org/bigwhite/d"

func main() {
    c.CallC()
    d.CallD()
}

</code></pre>
<p>我们让hello module也直接调用package d，并且我们在初始情况下，给hello module添加一个requirement:</p>
<pre><code>module hello

require (
    bitbucket.org/bigwhite/d v1.3.0
)
</code></pre>
<p>好了，这次我们再来构建一下hello module：</p>
<pre><code># go build hello.go
go: finding bitbucket.org/bigwhite/d v1.3.0
go: downloading bitbucket.org/bigwhite/d v1.3.0
go: finding bitbucket.org/bigwhite/c v1.3.0
go: downloading bitbucket.org/bigwhite/c v1.3.0
go: finding bitbucket.org/bigwhite/d v1.2.0
# cat go.mod
module hello

require (
    bitbucket.org/bigwhite/c v1.3.0 // indirect
    bitbucket.org/bigwhite/d v1.3.0 // indirect
)

# ./hello
call C: v1.3.0
   --&gt; call D:
    call D: v1.3.0
   --&gt; call D end
call D: v1.3.0

</code></pre>
<p>我们看到经过mvs算法后，go compiler最终选择了d v1.3.0版本。这里也模仿Russ Cox的图解给出hello module的mvs解析示意图(不过我这个例子还是比较simple)：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-modules-hello-module-mvs.png" alt="img{512x368}" /></p>
<h3>5. 使用package d的v2版本</h3>
<p>按照语义化版本规范，当出现不兼容性的变化时，需要升级版本中的major值，而go modules允许在import path中出现v2这样的带有major版本号的路径，表示所用的package为v2版本下的实现。我们甚至可以同时使用一个package的v0/v1和v2两个版本的实现。我们依旧使用上面的例子来实操一下如何在hello module中使用package d的两个版本的代码。</p>
<p>我们首先需要为package d建立module文件：go.mod，并标识出当前的module为：bitbucket.org/bigwhite/d/v2（为了保持与v0/v1各自独立演进，可通过branch的方式来实现），然后基于该版本打v2.0.0 tag。</p>
<pre><code>// bitbucket.org/bigwhite/d
#cat go.mod
module bitbucket.org/bigwhite/d/v2

</code></pre>
<p>改造一下hello module，import d的v2版本：</p>
<pre><code>// hello.go
package main

import "bitbucket.org/bigwhite/c"
import "bitbucket.org/bigwhite/d/v2"

func main() {
    c.CallC()
    d.CallD()
}
</code></pre>
<p>清理hello module的go.mod，仅保留对package c的requirement:</p>
<pre><code>module hello

require (
    bitbucket.org/bigwhite/c v1.3.0
)

</code></pre>
<p>清理$GOPATH/pkg/mod目录，然后重新构建hello module：</p>
<pre><code># go build hello.go
go: finding bitbucket.org/bigwhite/c v1.3.0
go: finding bitbucket.org/bigwhite/d v1.2.0
go: downloading bitbucket.org/bigwhite/c v1.3.0
go: downloading bitbucket.org/bigwhite/d v1.2.0
go: finding bitbucket.org/bigwhite/d/v2 v2.0.0
go: downloading bitbucket.org/bigwhite/d/v2 v2.0.0

# cat go.mod
module hello

require (
    bitbucket.org/bigwhite/c v1.3.0 // indirect
    bitbucket.org/bigwhite/d/v2 v2.0.0 // indirect
)

# ./hello
call C: v1.3.0
   --&gt; call D:
    call D: v1.2.0
   --&gt; call D end
call D: v2.0.0
</code></pre>
<p>我们看到c package依然使用的是d的v1.2.0版本，而main中使用的package d已经是v2.0.0版本了。</p>
<h2>五. go modules与vendor</h2>
<p>在最初的设计中，Russ Cox是想彻底废除掉<a href="http://tonybai.com/2015/07/31/understand-go15-vendor/">vendor</a>的，但在社区的反馈下，vendor得以保留，这也是为了兼容Go 1.11之前的版本。</p>
<p>Go modules支持通过下面命令将某个module的所有依赖保存一份copy到root module dir的vendor下:</p>
<pre><code># go mod -vendor
# ls
go.mod    go.sum  hello.go  vendor/
# cd vendor
# ls
bitbucket.org/    modules.txt
# cat modules.txt
# bitbucket.org/bigwhite/c v1.3.0
bitbucket.org/bigwhite/c
# bitbucket.org/bigwhite/d v1.2.0
bitbucket.org/bigwhite/d
# bitbucket.org/bigwhite/d/v2 v2.0.0
bitbucket.org/bigwhite/d/v2

# tree .
.
├── bitbucket.org
│   └── bigwhite
│       ├── c
│       │   ├── c.go
│       │   ├── go.mod
│       │   └── README.md
│       └── d
│           ├── d.go
│           ├── README.md
│           └── v2
│               ├── d.go
│               ├── go.mod
│               └── README.md
└── modules.txt

5 directories, 9 files

</code></pre>
<p>这样即便在go modules的<strong>module-aware mode</strong>模式下，我们依然可以只用vendor下的package来构建hello module。比如：我们先删除掉$GOPATH/pkg/mod目录，然后执行：</p>
<pre><code># go build -getmode=vendor hello.go
# ./hello
call C: v1.3.0
   --&gt; call D:
    call D: v1.2.0
   --&gt; call D end
call D: v2.0.0
</code></pre>
<p>当然生成的vendor目录还可以兼容go 1.11之前的go compiler。不过由于go 1.11之前的go compiler不支持在GOPATH之外使用vendor机制，因此我们需要将hello目录copy到$GOPATH/src下面，再用go 1.10.2版本的compiler编译它：</p>
<pre><code># go version
go version go1.10.2 linux/amd64
~/test/hello# go build hello.go
hello.go:3:8: cannot find package "bitbucket.org/bigwhite/c" in any of:
    /root/.bin/go1.10.2/src/bitbucket.org/bigwhite/c (from $GOROOT)
    /root/go/src/bitbucket.org/bigwhite/c (from $GOPATH)
hello.go:4:8: cannot find package "bitbucket.org/bigwhite/d/v2" in any of:
    /root/.bin/go1.10.2/src/bitbucket.org/bigwhite/d/v2 (from $GOROOT)
    /root/go/src/bitbucket.org/bigwhite/d/v2 (from $GOPATH)

# cp -r hello ~/go/src
# cd ~/go/src/hello
# go build hello.go
# ./hello
call C: v1.3.0
   --&gt; call D:
    call D: v1.2.0
   --&gt; call D end
call D: v2.0.0

</code></pre>
<p>编译输出和程序的执行结果均符合预期。</p>
<h2>六. 小结</h2>
<p>go modules刚刚merge到go trunk中，问题还会有很多。merge后很多gopher也提出了诸多问题，可以在<a href="https://github.com/golang/go/issues?q=is%3Aissue+is%3Aopen+label%3Amodules">这里</a>查到。当然哪位朋友如果也遇到了go modules的问题，也可以在go官方issue上提出来，帮助go team尽快更好地完善go 1.11的go modules机制。</p>
<p>go module的加入应该算是go 1.11版本最大的变化，go module的内容很多，短时间内我的理解也可能存在偏差和错误，欢迎广大gopher们交流指正。</p>
<p>参考资料：</p>
<ul>
<li><a href="https://groups.google.com/forum/#!msg/golang-dev/a5PqQuBljF4/61QK4JdtBgAJ">go modules have landed</a> 需科学上网访问</li>
<li><a href="https://research.swtch.com/vgo">Go &amp; Versioning</a></li>
<li>go help mod</li>
</ul>
<hr />
<p><a href="https://tonybai.com/">51短信平台</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>我的联系方式：</p>
<p>微博：https://weibo.com/bigwhite20xx<br />
微信公众号：iamtonybai<br />
博客：tonybai.com<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2018, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2018/07/15/hello-go-module/feed/</wfw:commentRss>
		<slash:comments>14</slash:comments>
		</item>
		<item>
		<title>写Go代码时遇到的那些问题[第3期]</title>
		<link>https://tonybai.com/2018/04/06/the-problems-i-encountered-when-writing-go-code-issue-3rd/</link>
		<comments>https://tonybai.com/2018/04/06/the-problems-i-encountered-when-writing-go-code-issue-3rd/#comments</comments>
		<pubDate>Fri, 06 Apr 2018 02:15:53 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Array]]></category>
		<category><![CDATA[AustinClements]]></category>
		<category><![CDATA[Channel]]></category>
		<category><![CDATA[comsumer]]></category>
		<category><![CDATA[dep]]></category>
		<category><![CDATA[GB18030]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[generics]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Go1.10]]></category>
		<category><![CDATA[go1.11]]></category>
		<category><![CDATA[go1.12]]></category>
		<category><![CDATA[Go2]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[golang-dev]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[gps]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[len]]></category>
		<category><![CDATA[lock]]></category>
		<category><![CDATA[logging]]></category>
		<category><![CDATA[logrus]]></category>
		<category><![CDATA[lumberjack]]></category>
		<category><![CDATA[manifest]]></category>
		<category><![CDATA[map]]></category>
		<category><![CDATA[network]]></category>
		<category><![CDATA[nodejs]]></category>
		<category><![CDATA[perl6]]></category>
		<category><![CDATA[producer]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[python3]]></category>
		<category><![CDATA[reddit]]></category>
		<category><![CDATA[Ruby]]></category>
		<category><![CDATA[RussCox]]></category>
		<category><![CDATA[select]]></category>
		<category><![CDATA[semver]]></category>
		<category><![CDATA[SetReadDeadline]]></category>
		<category><![CDATA[Shell]]></category>
		<category><![CDATA[Slice]]></category>
		<category><![CDATA[Socket]]></category>
		<category><![CDATA[strftime]]></category>
		<category><![CDATA[String]]></category>
		<category><![CDATA[timeout]]></category>
		<category><![CDATA[twitter]]></category>
		<category><![CDATA[Unicode]]></category>
		<category><![CDATA[utf16]]></category>
		<category><![CDATA[utf7]]></category>
		<category><![CDATA[vendor]]></category>
		<category><![CDATA[versioned-go]]></category>
		<category><![CDATA[vgo]]></category>
		<category><![CDATA[wasm]]></category>
		<category><![CDATA[WebAssembly]]></category>
		<category><![CDATA[包管理]]></category>
		<category><![CDATA[垃圾回收]]></category>
		<category><![CDATA[字符集编码]]></category>
		<category><![CDATA[抢占式调度]]></category>
		<category><![CDATA[最小版本选择]]></category>
		<category><![CDATA[标准库]]></category>
		<category><![CDATA[泛型]]></category>
		<category><![CDATA[网络编程]]></category>
		<category><![CDATA[语义化]]></category>
		<category><![CDATA[超时]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2581</guid>
		<description><![CDATA[我有一个习惯，那就是随时记录下编程过程中遇到的问题（包括问题现场、问题起因以及对问题的分析），并喜欢阶段性的对一段时间内的编码过程的得与失进行回顾和总结。内容可以包括：对编程语法的新认知、遇坑填坑的经历、一些让自己豁然开朗的小tip/小实践等。记录和总结的多了，感觉有价值的，就成文发在博客上的；一些小的点，或是还没有想清楚的事情，或思路没法结构化统一的，就放在资料库里备用。“写Go代码时遇到的那些问题”这个系列也是基于这个思路做的。 在这一篇中，我把“所遇到的问题”划分为三类：语言类、库与工具类、实践类，这样应该更便于大家分类阅读和理解。另外借这篇文章，我们先来看一下Go语言当前的State，资料来自于twitter、reddit、golang-dev forum、github上golang项目的issue/cl以及各种gophercon的talk资料。 零. Go语言当前状态 1. vgo Go 1.10在中国农历春节期间正式发布。随后Go team进入了Go 1.11的开发周期。 在2017年的Go语言用户调查报告结果中，缺少良好的包管理工具以及Generics依然是Gopher面临的最为棘手的挑战和难题的Top2，Go team也终于开始认真对待这两个问题了，尤其是包依赖管理的问题。在今年2月末，Russ Cox在自己的博客上连续发表了七篇博文，详细阐述了vgo &#8211; 带版本感知和支持的Go命令行工具的设计思路和实现方案，并在3月末正式提交了”versioned-go proposal“。 目前相对成熟的包管理方案是: "语义化版本" +manifest文件(手工维护的依赖约束描述文件) +lock文件(工具自动生成的传递依赖描述文件) +版本选择引擎工具（比如dep中的gps - Go Packaging Solver） 与之相比，vgo既有继承，更有创新。继承的是对语义化版本的支持，创新的则是semantic import versioning、最小版本选择minimal version selection等新机制，不变的则是对Go1语法的兼容。按照Russ Cox的计划，Go 1.11很可能会提供一个试验性的vgo实现（当然vgo所呈现的形式估计是merge到go tools中），让广大gopher试用和反馈，然后会像vendor机制那样，在后续Go版本中逐渐成为默认选项。 2. wasm porting 知名开源项目gopherjs的作者Richard Musiol上个月提交了一个proposal: WebAssembly architecture for Go，主旨在于让Gopher也可以用Go编写前端代码，让Go编写的代码可以在浏览器中运行。当然这并不是真的让Go能像js那样直接运行于浏览器或nodejs上，而是将Go编译为WebAssembly，wasm中间字节码，再在浏览器或nodejs初始化的运行环境中运行。这里根据自己的理解粗略画了一幅二进制机器码的go app与中间码的wasm的运行层次对比图，希望对大家有用： wasm porting已经完成了第一次commit ，很大可能会随着go1.11一并发布第一个版本。 3. 非协作式的goroutine抢占式调度 当前goroutine的“抢占式”调度依靠的是compiler在函数中自动插入的“cooperative preemption point”来实现的，但这种方式在使用过程中依然有各种各样的问题，比如：检查点的性能损耗、诡异的全面延迟问题以及调试上的困难。近期负责go runtime gc设计与实现的Austin Clements提出了一个proposal：non-cooperative [...]]]></description>
			<content:encoded><![CDATA[<p>我有一个习惯，那就是随时记录下编程过程中遇到的问题（包括问题现场、问题起因以及对问题的分析），并喜欢阶段性的对一段时间内的<strong>编码过程的得与失</strong>进行回顾和总结。内容可以包括：对编程语法的新认知、遇坑填坑的经历、一些让自己豁然开朗的小tip/小实践等。记录和总结的多了，感觉有价值的，就成文发在博客上的；一些小的点，或是还没有想清楚的事情，或思路没法结构化统一的，就放在资料库里备用。“写Go代码时遇到的那些问题”这个系列也是基于这个思路做的。</p>
<p>在这一篇中，我把“所遇到的问题”划分为三类：语言类、库与工具类、实践类，这样应该更便于大家分类阅读和理解。另外借这篇文章，我们先来看一下<a href="https://tonybai.com/tag/golang">Go语言</a>当前的State，资料来自于twitter、<a href="https://www.reddit.com/r/golang">reddit</a>、<a href="https://groups.google.com/forum/#!forum/golang-dev">golang-dev forum</a>、github上<a href="https://github.com/golang">golang项目</a>的issue/cl以及各种gophercon的talk资料。</p>
<h2>零. Go语言当前状态</h2>
<h3>1. vgo</h3>
<p><a href="http://tonybai.com/2018/02/17/some-changes-in-go-1-10/">Go 1.10</a>在中国农历春节期间正式发布。随后Go team进入了Go 1.11的<a href="https://github.com/golang/go/wiki/Go-Release-Cycle">开发周期</a>。</p>
<p>在2017年的<a href="https://blog.golang.org/survey2017-results">Go语言用户调查报告</a>结果中，缺少良好的<a href="https://tonybai.com/2017/06/08/first-glimpse-of-dep/">包管理工具</a>以及Generics依然是Gopher面临的最为棘手的挑战和难题的Top2，Go team也终于开始认真对待这两个问题了，尤其是包依赖管理的问题。在今年2月末，<a href="https://swtch.com/~rsc/">Russ Cox</a>在自己的博客上连续发表了<a href="https://research.swtch.com/vgo">七篇博文</a>，详细阐述了<a href="https://github.com/golang/vgo">vgo</a> &#8211; 带版本感知和支持的Go命令行工具的设计思路和实现方案，并在3月末正式提交了”<a href="https://github.com/golang/proposal/blob/master/design/24301-versioned-go.md">versioned-go proposal</a>“。</p>
<p>目前相对成熟的包管理方案是:</p>
<pre><code>"语义化版本"
+manifest文件(手工维护的依赖约束描述文件)
+lock文件(工具自动生成的传递依赖描述文件)
+版本选择引擎工具（比如dep中的gps - Go Packaging Solver）
</code></pre>
<p>与之相比，vgo既有继承，更有创新。继承的是对<a href="http://semver.org/lang/zh-CN/">语义化版本</a>的支持，创新的则是<a href="https://research.swtch.com/vgo-import">semantic import versioning</a>、<a href="https://research.swtch.com/vgo-mvs">最小版本选择minimal version selection</a>等新机制，不变的则是对<a href="https://golang.org/doc/go1compat">Go1语法</a>的兼容。按照Russ Cox的计划，Go 1.11很可能会提供一个试验性的vgo实现（当然vgo所呈现的形式估计是merge到go tools中），让广大gopher试用和反馈，然后会像<a href="http://tonybai.com/2015/07/31/understand-go15-vendor/">vendor机制</a>那样，在后续Go版本中逐渐成为默认选项。</p>
<h3>2. wasm porting</h3>
<p>知名开源项目<a href="https://github.com/gopherjs/gopherjs">gopherjs</a>的作者<a href="https://github.com/neelance">Richard Musiol</a>上个月提交了一个proposal: <a href="https://docs.google.com/document/d/131vjr4DH6JFnb-blm_uRdaC0_Nv3OUwjEY5qVCxCup4/preview#heading=h.mjo1bish3xni">WebAssembly architecture for Go</a>，主旨在于让Gopher也可以用Go编写前端代码，让Go编写的代码可以在浏览器中运行。当然这并不是真的让Go能像js那样直接运行于浏览器或nodejs上，而是将Go编译为<a href="http://webassembly.org/">WebAssembly，wasm</a>中间字节码，再在浏览器或nodejs初始化的运行环境中运行。这里根据自己的理解粗略画了一幅二进制机器码的go app与中间码的wasm的运行层次对比图，希望对大家有用：</p>
<p><img src="http://tonybai.com/wp-content/uploads/writing-go-code-issues/3rd-issue/go-wasm-porting.png" alt="img{512x368}" /></p>
<p>wasm porting已经完成了<a href="https://go-review.googlesource.com/c/go/+/102835">第一次commit</a> ，很大可能会随着go1.11一并发布第一个版本。</p>
<h3>3. 非协作式的goroutine抢占式调度</h3>
<p>当前<a href="http://tonybai.com/2017/06/23/an-intro-about-goroutine-scheduler/">goroutine</a>的<a href="http://tonybai.com/2017/11/23/the-simple-analysis-of-goroutine-schedule-examples/">“抢占式”调度</a>依靠的是compiler在函数中自动插入的“cooperative preemption point”来实现的，但这种方式在使用过程中依然有各种各样的问题，比如：检查点的性能损耗、诡异的全面延迟问题以及调试上的困难。近期负责go runtime gc设计与实现的<a href="https://github.com/aclements">Austin Clements</a>提出了一个proposal：<a href="https://github.com/golang/go/issues/24543">non-cooperative goroutine preemption</a> ，该proposal将去除cooperative preemption point，而改为利用构建和记录每条指令的stack和register map的方式实现goroutine的抢占， 该proposal预计将在go 1.12中实现。</p>
<h3>4. Go的历史与未来</h3>
<p>在<a href="https://www.gophercon-russia.ru/en">GopherConRu 2018大会</a>上，来自Go team的核心成员Brad Fitzpatrick做了<a href="https://github.com/GopherConRu/talks/blob/master/2018/Go%20-%20looking%20back%20and%20looking%20forward%20-%20Brad%20Fitzpatrick.pdf">“Go的历史与未来”</a>的主题演讲 ，Bradfitz“爆料”了关于Go2的几个可能，考虑到Bradfitz在Go team中的位置，这些可能性还是具有很大可信度的：</p>
<pre><code>1). 绝不像Perl6和Python3那样分裂社区
2). Go1的包可以import Go2的package
3). Go2很可能加入Generics，Ian Lance Taylor应该在主导该Proposal
4). Go2在error handling方面会有改进，但不会是try--catch那种形式
5). 相比于Go1，Go2仅会在1-3个方面做出重大变化
6). Go2可能会有一个新的标准库，并且该标准库会比现有的标准库更小，很多功能放到标准库外面
7). 但Go2会在标准库外面给出最流行、推荐的、可能认证的常用包列表，这些在标准库外面的包可以持续更新，而不像那些在标准库中的包，只能半年更新一次。
</code></pre>
<h2>一. 语言篇</h2>
<h3>1. len(channel)的使用</h3>
<p>len是Go语言的一个<a href="https://golang.org/ref/spec#Length_and_capacity">built-in函数</a>，它支持接受array、slice、map、string、channel类型的参数，并返回对应类型的”长度” &#8211; 一个整型值：</p>
<pre><code>len(s)   

如果s是string，len(s)返回字符串中的字节个数
如何s是[n]T, *[n]T的数组类型，len(s)返回数组的长度n
如果s是[]T的Slice类型，len(s)返回slice的当前长度
如果s是map[K]T的map类型，len(s)返回map中的已定义的key的个数
如果s是chan T类型，那么len(s)返回当前在buffered channel中排队（尚未读取）的元素个数
</code></pre>
<p>不过我们在代码经常见到的是len函数针对数组、slice、string类型的调用，而len与channel的联合使用却很少。那是不是说len(channel)就不可用了呢？我们先来看看len(channel)的语义。</p>
<ul>
<li>当channel为unbuffered channel时，len(channel)总是返回0；</li>
<li>当channel为buffered channel时，len(channel)返回当前channel中尚未被读取的元素个数。</li>
</ul>
<p>这样一来，所谓len(channel)中的<a href="https://tonybai.com/2014/09/29/a-channel-compendium-for-golang/">channel</a>就是针对buffered channel。len(channel)从语义上来说一般会被用来做“判满”、”判有”和”判空”逻辑：</p>
<pre><code>// 判空

if len(channel) == 0 {
    // 这时：channel 空了 ?
}

// 判有

if len(channel) &gt; 0 {
    // 这时：channel 有数据了 ?
}

// 判满
if len(channel) == cap(channel) {
    // 这时:   channel 满了 ?
}

</code></pre>
<p>大家看到了，我在上面代码中注释：“空了”、“有数据了”和“满了”的后面打上了问号！channel多用于多个goroutine间的通讯，一旦多个goroutine共同读写channel，len(channel)就会在多个goroutine间形成”竞态条件”，单存的依靠len(channel)来判断队列状态，不能保证在后续真正读写channel的时候channel状态是不变的。以判空为例：</p>
<p><img src="http://tonybai.com/wp-content/uploads/writing-go-code-issues/3rd-issue/go-len-channel-1.png" alt="img{512x368}" /></p>
<p>从上图可以看到，当goroutine1使用len(channel)判空后，便尝试从channel中读取数据。但在真正从Channel读数据前，另外一个goroutine2已经将数据读了出去，goroutine1后面的<strong>读取将阻塞在channel上</strong>，导致后面逻辑的失效。因此，<strong>为了不阻塞在channel上</strong>，常见的方法是将“判空与读取”放在一起做、将”判满与写入”一起做，通过select实现操作的“事务性”：</p>
<pre><code>//writing-go-code-issues/3rd-issue/channel_len.go/channel_len.go.go
func readFromChan(ch &lt;-chan int) (int, bool) {
    select {
    case i := &lt;-ch:
        return i, true
    default:
        return 0, false // channel is empty
    }
}

func writeToChan(ch chan&lt;- int, i int) bool {
    select {
    case ch &lt;- i:
        return true
    default:
        return false // channel is full
    }
}

</code></pre>
<p>我们看到由于用到了Select-default的trick，当channel空的时候，readFromChan不会阻塞；当channel满的时候，writeToChan也不会阻塞。这种方法也许适合大多数的场合，但是这种方法有一个“问题”，那就是“改变了channel的状态”：读出了一个元素或写入了一个元素。有些时候，我们不想这么做，我们想在不改变channel状态下单纯地侦测channel状态！很遗憾，目前没有哪种方法可以适用于所有场合。但是在特定的场景下，我们可以用len(channel)实现。比如下面这个场景：</p>
<p><img src="http://tonybai.com/wp-content/uploads/writing-go-code-issues/3rd-issue/go-len-channel-2.png" alt="img{512x368}" /></p>
<p>这是一个“多producer + 1 consumer”的场景。controller是一个总控协程，初始情况下，它来判断channel中是否有消息。如果有消息，它本身不消费“消息”，而是创建一个consumer来消费消息，直到consumer因某种情况退出，控制权再回到controller，controller不会立即创建new consumer，而是等待channel下一次有消息时才创建。在这样一个场景中，我们就可以使用len(channel)来判断是否有消息。</p>
<h3>2. 时间的格式化输出</h3>
<p>时间的格式化输出是日常编程中经常遇到的“题目”。以前使用<a href="https://tonybai.com/tag/c">C语言编程</a>时，用的是strftime。我们来回忆一下c的代码：</p>
<pre><code>// writing-go-code-issues/3rd-issue/time-format/strftime_in_c.c
#include &lt;stdio.h&gt;
#include &lt;time.h&gt;

int main() {
        time_t now = time(NULL);

        struct tm *localTm;
        localTm = localtime(&amp;now);

        char strTime[100];
        strftime(strTime, sizeof(strTime),  "%Y-%m-%d %H:%M:%S", localTm);
        printf("%s\n", strTime);

        return 0;
}

</code></pre>
<p>这段c代码输出结果是：</p>
<pre><code>2018-04-04 16:07:00
</code></pre>
<p>我们看到strftime采用“字符化”的占位符(诸如：%Y、%m等)“拼”出时间的目标输出格式布局（如：”%Y-%m-%d %H:%M:%S”），这种方式不仅在C中采用，很多其他主流编程语言也采用了该方案，比如:shell、<a href="https://tonybai.com/tag/python">python</a>、<a href="https://tonybai.com/tag/ruby">ruby</a>、<a href="https://tonybai.com/tag/java">java</a>等，这似乎已经成为了各种编程语言在时间格式化输出的标准。这些占位符对应的字符（比如Y、M、H）是对应英文单词的头母，因此相对来说较为容易记忆。</p>
<p>但是如果你在Go中使用strftime的这套“标准”，看到输出结果的那一刻，你肯定要“骂娘”！</p>
<pre><code>// writing-go-code-issues/3rd-issue/time-format/timeformat_in_c_way.go
package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println(time.Now().Format("%Y-%m-%d %H:%M:%S"))
}
</code></pre>
<p>上述go代码输出结果如下：</p>
<pre><code>%Y-%m-%d %H:%M:%S
</code></pre>
<p>Go居然将“时间格式占位符字符串”原封不动的输出了!</p>
<p>这是因为Go另辟了蹊径，采用了不同于strftime的时间格式化输出的方案。Go的设计者主要出于这样的考虑：虽然strftime的单个占位符使用了对应单词的首字母的形式，但是但真正写起代码来，不打开strftime函数的manual或查看<a href="http://strftime.org/">网页版的strftime助记符说明</a>，很难真的拼出一个复杂的时间格式。并且对于一个”%Y-%m-%d %H:%M:%S”的格式串，不对照文档，很难在大脑中准确给出格式化后的时间结果，比如%Y和%y有何不同、%M和%m又有何差别呢？</p>
<p>Go语言采用了更为直观的“参考时间(reference time)”替代strftime的各种标准占位符，使用“参考时间”构造出来的“时间格式串”与最终输出串是“一模一样”的，这就省去了程序员再次在大脑中对格式串进行解析的过程：</p>
<pre><code>格式串："2006年01月02日 15时04分05秒"

=&gt;

输出结果：2018年04月04日 18时13分08秒

</code></pre>
<p>标准的参考时间如下：</p>
<pre><code>2006-01-02 15:04:05 PM -07:00 Jan Mon MST
</code></pre>
<p>这个绝对时间本身并没有什么实际意义，仅是出于“好记”的考虑，我们将这个参考时间换为另外一种时间输出格式：</p>
<pre><code>01/02 03:04:05PM '06 -0700
</code></pre>
<p>我们看出Go设计者的“用心良苦”，这个时间其实恰好是<strong>将助记符从小到大排序(从01到07)的结果</strong>，可以理解为：01对应的是%M, 02对应的是%d等等。下面这幅图形象地展示了“参考时间”、“格式串”与最终格式化的输出结果之间的关系：</p>
<p><img src="http://tonybai.com/wp-content/uploads/writing-go-code-issues/3rd-issue/go-time-format-demo.png" alt="img{512x368}" /></p>
<p>就我个人使用go的经历来看，我在做时间格式化输出时，尤其是构建略微复杂的时间格式输出时，也还是要go doc time包或打开time包的web手册的。从社区的反馈来看，很多Gopher也都有类似经历，尤其是那些已经用惯了strftime格式的gopher。甚至有人专门做了<a href="http://fuckinggodateformat.com/">“Fucking Go Date Format”</a>页面，来帮助自动将strftime使用的格式转换为go time的格式。</p>
<p>下面这幅cheatsheet也能提供一些帮助(由writing-go-code-issues/3rd-issue/time-format/timeformat_cheatsheet.go输出生成)：</p>
<p><img src="http://tonybai.com/wp-content/uploads/writing-go-code-issues/3rd-issue/go-time-format-cheatsheet.png" alt="img{512x368}" /></p>
<h2>二. 库与工具篇</h2>
<h3>1. golang.org/x/text/encoding/unicode遇坑一则</h3>
<p>在<a href="https://github.com/bigwhite/gocmpp">gocmpp</a>这个项目中，我用到了unicode<a href="https://tonybai.com/2007/11/03/also-talk-about-char-encoding/">字符集转换</a>：将utf8转换为ucs2(utf16)、ucs2转换为utf8、utf8转为GB18030等。这些转换功能，我是借助golang.org/x/text这个项目下的encoding/unicode和transform实现的。x/text是golang官方维护的text处理的工具包，其中包含了对unicode字符集的相关操作。</p>
<p>要实现一个utf8到ucs2(utf16)的字符集转换，只需像如下这样实现即可（这也是我的最初实现）：</p>
<pre><code>func Utf8ToUcs2(in string) (string, error) {
    if !utf8.ValidString(in) {
        return "", ErrInvalidUtf8Rune
    }

    r := bytes.NewReader([]byte(in))

    //UTF-16 bigendian, no-bom
    t := transform.NewReader(r, unicode.All[1].NewEncoder())
    out, err := ioutil.ReadAll(t)
    if err != nil {
        return "", err
    }
    return string(out), nil
}
</code></pre>
<p>这里要注意是unicode.All这个切片保存着UTF-16的所有格式：</p>
<pre><code>var All = []encoding.Encoding{
    UTF16(BigEndian, UseBOM),
    UTF16(BigEndian, IgnoreBOM),
    UTF16(LittleEndian, IgnoreBOM),
}
</code></pre>
<p>这里我最初我用的是All[1]，即UTF16(BigEndian, IgnoreBOM)，一切都是正常的。</p>
<p>但就在年前，我将text项目更新到最新版本，然后发现单元测试无法通过：</p>
<pre><code>--- FAIL: TestUtf8ToUcs2 (0.00s)
    utils_test.go:58: The first char is fe, not equal to expected 6c
FAIL
FAIL    github.com/bigwhite/gocmpp/utils    0.008s
</code></pre>
<p>经查找发现：text项目的golang.org/x/text/encoding/unicode包做了不兼容的修改，上面那个unicode.All切片变成了下面这个样子：</p>
<pre><code>// All lists a configuration for each IANA-defined UTF-16 variant.
var All = []encoding.Encoding{
    UTF8,
    UTF16(BigEndian, UseBOM),
    UTF16(BigEndian, IgnoreBOM),
    UTF16(LittleEndian, IgnoreBOM),
}
</code></pre>
<p>All切片在最前面插入了一个UTF8元素，这样导致我的代码中原本使用的 UTF16(BigEndian, IgnoreBOM)变成了UTF16(BigEndian, UseBOM)，test不过也就情有可原了。</p>
<p>如何改呢？这回儿我直接使用UTF16(BigEndian, IgnoreBOM)，而不再使用All切片了：</p>
<pre><code>func Utf8ToUcs2(in string) (string, error) {
    if !utf8.ValidString(in) {
        return "", ErrInvalidUtf8Rune
    }

    r := bytes.NewReader([]byte(in))
    //UTF-16 bigendian, no-bom
    t := transform.NewReader(r,
            unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM).NewEncoder())
    out, err := ioutil.ReadAll(t)
    if err != nil {
        return "", err
    }
    return string(out), nil
}

</code></pre>
<p>这样即便All切片再有什么变动，我的代码也不会受到什么影响了。</p>
<h3>2. logrus的非结构化日志定制输出</h3>
<p>在该系列的<a href="http://tonybai.com/2018/01/13/the-problems-i-encountered-when-writing-go-code-issue-1st/">第一篇文章</a>中，我提到过使用<a href="https://github.com/sirupsen/logrus">logrus</a>+<a href="https://github.com/natefinch/lumberjack">lumberjack</a>来实现支持rotate的logging。</p>
<p>默认情况下日志的输出格式是这样的（writing-go-code-issues/3rd-issue/logrus/logrus2lumberjack_default.go）：</p>
<pre><code>time="2018-04-05T06:08:53+08:00" level=info msg="logrus log to lumberjack in normal text formatter"
</code></pre>
<p>这样相对结构化的日志比较适合后续的集中日志分析。但是日志携带的“元信息(time、level、msg)”过多，并不是所有场合都倾向于这种日志，于是我们期望以普通的非结构化的日志输出，我们定制formatter：</p>
<pre><code>// writing-go-code-issues/3rd-issue/logrus/logrus2lumberjack.go
func main() {
    customFormatter := &amp;logrus.TextFormatter{
        FullTimestamp:   true,
        TimestampFormat: "2006-01-02 15:04:05",
    }
    logger := logrus.New()
    logger.Formatter = customFormatter

    rotateLogger := &amp;lumberjack.Logger{
        Filename: "./foo.log",
    }
    logger.Out = rotateLogger
    logger.Info("logrus log to lumberjack in normal text formatter")
}
</code></pre>
<p>我们使用textformatter，并定制了时间戳的格式，输出结果如下：</p>
<pre><code>time="2018-04-05 06:22:57" level=info msg="logrus log to lumberjack in normal text formatter"
</code></pre>
<p>日志仍然不是我们想要的那种。但同样的customFormatter如果输出到terminal，结果却是我们想要的：</p>
<pre><code>//writing-go-code-issues/3rd-issue/logrus/logrus2tty.go

INFO[2018-04-05 06:26:16] logrus log to tty in normal text formatter
</code></pre>
<p>到底如何设置TextFormatter的属性才能让我们输出到lumberjack中的日志格式是我们想要的这种呢？无奈下只能挖logrus的源码了，我们找到了这段代码：</p>
<pre><code>//github.com/sirupsen/logrus/text_formatter.go

// Format renders a single log entry
func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
    ... ...
    isColored := (f.ForceColors || f.isTerminal) &amp;&amp; !f.DisableColors

    timestampFormat := f.TimestampFormat
    if timestampFormat == "" {
        timestampFormat = defaultTimestampFormat
    }
    if isColored {
        f.printColored(b, entry, keys, timestampFormat)
    } else {
        if !f.DisableTimestamp {
            f.appendKeyValue(b, "time", entry.Time.Format(timestampFormat))
        }
        f.appendKeyValue(b, "level", entry.Level.String())
        if entry.Message != "" {
            f.appendKeyValue(b, "msg", entry.Message)
        }
        for _, key := range keys {
            f.appendKeyValue(b, key, entry.Data[key])
        }
    }

    b.WriteByte('\n')
    return b.Bytes(), nil
}
</code></pre>
<p>我们看到如果isColored为false，输出的就是带有time, msg, level的结构化日志；只有isColored为true才能输出我们想要的普通日志。isColored的值与三个属性有关：ForceColors 、isTerminal和DisableColors。我们按照让isColored为true的条件组合重新设置一下这三个属性，因为输出到file，因此isTerminal自动为false。</p>
<pre><code>//writing-go-code-issues/3rd-issue/logrus/logrus2lumberjack_normal.go
func main() {
    //    isColored := (f.ForceColors || f.isTerminal) &amp;&amp; !f.DisableColors
    customFormatter := &amp;logrus.TextFormatter{
        FullTimestamp:   true,
        TimestampFormat: "2006-01-02 15:04:05",
        ForceColors:     true,
    }
    logger := logrus.New()
    logger.Formatter = customFormatter

    rotateLogger := &amp;lumberjack.Logger{
        Filename: "./foo.log",
    }
    logger.Out = rotateLogger
    logger.Info("logrus log to lumberjack in normal text formatter")
}

</code></pre>
<p>我们设置ForceColors为true后，在foo.log中得到了我们期望的输出结果：</p>
<pre><code>INFO[2018-04-05 06:33:22] logrus log to lumberjack in normal text formatter
</code></pre>
<h2>三. 实践篇</h2>
<h3>1. 说说网络数据读取timeout的处理 &#8211; 以SetReadDeadline为例</h3>
<p>Go天生适合于<a href="https://tonybai.com/2015/11/17/tcp-programming-in-golang/">网络编程</a>，但网络编程的复杂性也是有目共睹的、要写出稳定、高效的网络端程序，需要的考虑的因素有很多。比如其中之一的：从socket读取数据超时的问题。</p>
<p>Go语言标准网络库并没有实现epoll实现的那样的<strong>“idle timeout”</strong>，而是提供了Deadline机制，我们用一副图来对比一下两个机制的不同：</p>
<p><img src="http://tonybai.com/wp-content/uploads/writing-go-code-issues/3rd-issue/go-idle-timeout-vs-deadline.png" alt="img{512x368}" /></p>
<p>看上图a)和b)展示了”idle timeout”机制，所谓idle timeout就是指这个timeout是真正在没有data ready的情况的timeout（如图中a)，如果有数据ready可读(如图中b)，那么timeout机制暂停，直到数据读完后，再次进入数据等待的时候，idle timeout再次启动。</p>
<p>而deadline(以read deadline为例)机制，则是无论是否有数据ready以及数据读取活动，都会在到达时间（deadline）后的再次read时返回timeout error，并且后续的所有network read operation也都会返回timeout（如图中d），除非重新调用SetReadDeadline(time.Time{})取消Deadline或在再次读取动作前重新重新设定deadline实现<strong>续时</strong>的目的。Go网络编程一般是“阻塞模型”，那为什么还要有SetReadDeadline呢，这是因为有时候，我们要给调用者“感知”其他“异常情况”的机会，比如是否收到了main goroutine发送过来的<strong>退出通知信息</strong>。</p>
<p>Deadline机制在使用起来很容易出错，这里列举两个曾经遇到的出错状况：</p>
<p>a) 以为SetReadDeadline后，后续每次Read都可能实现idle timeout</p>
<p><img src="http://tonybai.com/wp-content/uploads/writing-go-code-issues/3rd-issue/go-setreaddeadline-once.png" alt="img{512x368}" /></p>
<p>在上图中，我们看到这个流程是读取一个完整业务包的过程，业务包的读取使用了三次Read调用，但是只在第一次Read前调用了SetReadDeadline。这种使用方式仅仅在Read A时实现了足额的“idle timeout”，且仅当A数据始终未ready时会timeout；一旦A数据ready并已经被Read，当Read B和Read C时，如果还期望足额的“idle timeout”那就误解了SetReadDeadline的真正含义了。因此要想在每次Read时都实现“足额的idle timeout”，需要在每次Read前都重新设定deadline。</p>
<p>b) 一个完整“业务包”分多次读取的异常情况的处理</p>
<p><img src="http://tonybai.com/wp-content/uploads/writing-go-code-issues/3rd-issue/go-setreaddeadline-before-each-read.png" alt="img{512x368}" /></p>
<p>在这幅图中，每个Read前都重新设定了deadline，那么这样就一定ok了么？对于在一个过程中读取一个“完整业务包”的业务逻辑来说，我们还要考虑对每次读取异常情况的处理，尤其是timeout发生。在该例子中，有三个Read位置需要考虑异常处理。</p>
<p>如果Read A始终没有读到数据，deadline到期，返回timeout，这里是最容易处理的，因为此时前一个完整数据包已经被读完，新的完整数据包还没有到来，外层控制逻辑收到timeout后，重启再次启动该读流程即可。</p>
<p>如果Read B或Read C处没有读到数据，deadline到期，这时异常处理就棘手一些，因为一个完整数据包的部分数据（A）已经从流中被读出，剩余的数据并不是一个完整的业务数据包，不能简单地再在外层控制逻辑中重新启动该过程。我们要么在Read B或Read C处尝试多次重读，直到将完整数据包读取完整后返回；要么认为在B或C处出现timeout是不合理的，返回区别于A处的错误码给外层控制逻辑，让外层逻辑决定是否是连接存在异常。</p>
<p>注：本文所涉及的示例代码，请到<a href="https://github.com/bigwhite/experiments/tree/master/writing-go-code-issues/3rd-issue">这里</a>下载。</p>
<hr />
<p>微博：<a href="http://weibo.com/bigwhite20xx">@tonybai_cn</a><br />
微信公众号：iamtonybai<br />
github.com: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="http://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p style='text-align:left'>&copy; 2018, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2018/04/06/the-problems-i-encountered-when-writing-go-code-issue-3rd/feed/</wfw:commentRss>
		<slash:comments>5</slash:comments>
		</item>
	</channel>
</rss>
