<?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; svn</title>
	<atom:link href="http://tonybai.com/tag/svn/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Mon, 20 Apr 2026 23:16:50 +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/2024/01/07/what-we-got-right-what-we-got-wrong/</link>
		<comments>https://tonybai.com/2024/01/07/what-we-got-right-what-we-got-wrong/#comments</comments>
		<pubDate>Sun, 07 Jan 2024 11:33:46 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Channel]]></category>
		<category><![CDATA[Compiler]]></category>
		<category><![CDATA[CSP]]></category>
		<category><![CDATA[generics]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go.dev]]></category>
		<category><![CDATA[gofmt]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[GopherCon]]></category>
		<category><![CDATA[GopherConAu]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[IDE]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[LLVM]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[plan9]]></category>
		<category><![CDATA[Pthread]]></category>
		<category><![CDATA[RobPike]]></category>
		<category><![CDATA[RussCox]]></category>
		<category><![CDATA[select]]></category>
		<category><![CDATA[Specification]]></category>
		<category><![CDATA[SSA]]></category>
		<category><![CDATA[svn]]></category>
		<category><![CDATA[typeparameter]]></category>
		<category><![CDATA[vet]]></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>

		<guid isPermaLink="false">https://tonybai.com/?p=4103</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/01/07/what-we-got-right-what-we-got-wrong 在《2023年Go语言盘点：稳中求新，稳中求变》和《Go测试的20个实用建议》两篇文章中，我都提到过已经退居二线的Go语言之父Rob Pike在Go开源14周年的那天亲自在GopherCon Australia 2023上发表了“What We Got Right, What We Got Wrong”的主题演讲来回顾Go诞生以来的得与失。近期Rob Pike终于将这次演进的文字稿发布了出来！GopherCon Australia也在油管上发布了这个演进的视频。Rob Pike的观点对所有Gopher都是极具参考价值的，因此在这篇博文中，我将Rob Pike的这次演讲稿翻译成中文，供大家参考(结合文字稿和视频)，我们一起来领略和学习大师的观点。 这是2023年11月10日我在悉尼GopherConAU 2023会议上的闭幕演讲（视频），那一天也是Go开源14周年的日子。本文中穿插着演示文稿中使用的幻灯片。 介绍 大家好！ 首先，我要感谢Katie和Chewy让我有幸为此次GopherConAU大会做闭幕演讲。 2009年11月10日 今天是2023年11月10日，Go作为开源项目推出14周年的纪念日。 2009年11月10日那天，加州时间下午3点（如果没记错的话），Ken Thompson、Robert Griesemer、Russ Cox、Ian Taylor、Adam Langley、Jini Kim和我满怀期待地看着网站上线。之后，全世界都知道我们在做什么了。 14年后的今天，有很多事情值得回顾。我想借此机会谈谈自那一天以来学到的一些重要经验。即使是最成功的项目，在反思之后，也会发现一些事情本可以做得更好。当然，也有一些事情事后看来似乎是成功的关键所在。 首先，我必须明确的是，这里的观点只代表我个人，不代表Go团队和Google。无论是过去还是现在，Go都是由一支专注的团队和庞大的社区付出巨大努力的结果。所以，如果你同意我的任何说法，请感谢他们。如果你不同意，请责怪我，但请保留你的意见。 鉴于本次演讲的题目，许多人可能期待我会分析语言中的优点和缺点。当然，我会做一些分析，但还会有更多内容，原因有几个。 首先，编程语言的好坏很大程度上取决于观点而不是事实，尽管许多人对Go或任何其他语言的最微不足道的功能都存在争论。 另外，关于换行符的位置、nil的工作方式、导出的大小写表示法、垃圾回收、错误处理等话题已经有了大量的讨论。这些话题肯定有值得讨论的地方，但几乎没什么是还没有被讨论过的。 但我要讨论的不仅仅是语言本身的真正原因是，语言并不是整个项目的全部。我们最初的目标不是创造一种新的编程语言，而是创造一种更好的编写软件的方式。我们对所使用的语言有意见——无论使用什么语言，每个人都是如此——但是我们遇到的基本问题与这些语言的特性没有太大关系，而是与在谷歌使用这些语言构建软件的过程有关。 T恤上的第一只Gopher 新语言的创建提供了探索其他想法的新路径，但这只是一个推动因素，而不是真正的重点。如果当时我正在工作的二进制文件不需要45分钟来构建 ，Go语言就不会出现。但那45分钟不是因为编译器慢(因为它不慢)，也不是因为它所用的语言不好(因为它也不差)。缓慢是由其他因素造成的。 我们想解决的就是这些因素：构建现代服务器软件的复杂性：控制依赖性、与人员不断变化的大型团队一起编程、可维护性、高效测试、多核CPU和网络的有效利用等等。 简而言之，Go不仅仅是一种编程语言。当然，它是一种编程语言，这是它的定义。但它的目的是帮助提供一种更好的方式来开发高质量的软件，至少与14多年前的我们的环境相比。 时至今日，这仍然是它的宗旨。Go是一个使构建生产软件更容易、更高效的项目。 几周前，当我开始准备这次演讲时，我只有一个题目，除此之外别无其他。为了激发我的思路，我在Mastodon上向人们征求意见。不少人给予了回复。我注意到了一种趋势：人们认为我们做错的事情都在语言本身，而我们做对的事情都在语言周边，比如gofmt、部署和测试等。事实上，我觉得这令人鼓舞。我们试图做的事情似乎已经产生了效果。 但值得承认的是，我们在早期并没有明确真正的目标。我们可能觉得这些目标是不言自明的。为了弥补这一缺陷，我在2013年的SPLASH会议上发表了一场题为《谷歌的Go语言：面向软件工程的语言设计》的演讲。 Go at Google 那场演讲和相关的博客文章可能是对Go语言为何而生的最好诠释。 今天的演讲是SPLASH演讲的后续，回顾了我们在构建语言之后所学到的经验教训，并且可以更广泛地应用于更大的图景。 那么&#8230;&#8230;来谈谈一些教训。 首先，当然，我们有： The Gopher [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/01/07/what-we-got-right-what-we-got-wrong">本文永久链接</a> &#8211; https://tonybai.com/2024/01/07/what-we-got-right-what-we-got-wrong</p>
<p>在《<a href="https://tonybai.com/2023/12/31/the-2023-review-of-go-programming-language/">2023年Go语言盘点：稳中求新，稳中求变</a>》和《<a href="https://tonybai.com/2024/01/01/go-testing-by-example/">Go测试的20个实用建议</a>》两篇文章中，我都提到过已经退居二线的<a href="https://tonybai.com/2023/12/11/simplicity/">Go语言之父Rob Pike</a>在<a href="https://tonybai.com/2023/11/11/go-opensource-14-years/">Go开源14周年</a>的那天亲自在GopherCon Australia 2023上发表了“What We Got Right, What We Got Wrong”的主题演讲来回顾Go诞生以来的得与失。近期<a href="https://commandcenter.blogspot.com/2024/01/what-we-got-right-what-we-got-wrong.html">Rob Pike终于将这次演进的文字稿发布了出来</a>！<a href="https://www.youtube.com/watch?v=yE5Tpp2BSGw">GopherCon Australia也在油管上发布了这个演进的视频</a>。Rob Pike的观点对所有Gopher都是极具参考价值的，因此在这篇博文中，我将Rob Pike的这次演讲稿翻译成中文，供大家参考(结合文字稿和视频)，我们一起来领略和学习大师的观点。</p>
<hr />
<p>这是2023年11月10日我<a href="https://www.youtube.com/watch?v=yE5Tpp2BSGw">在悉尼GopherConAU 2023会议上的闭幕演讲（视频）</a>，那一天也是<a href="https://tonybai.com/2023/11/11/go-opensource-14-years">Go开源14周年</a>的日子。本文中穿插着演示文稿中使用的幻灯片。</p>
<h2>介绍</h2>
<p>大家好！</p>
<p>首先，我要感谢Katie和Chewy让我有幸为此次GopherConAU大会做闭幕演讲。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-2.png" alt="" /><br />
<center>2009年11月10日</center></p>
<p>今天是2023年11月10日，Go作为开源项目推出14周年的纪念日。</p>
<p>2009年11月10日那天，加州时间下午3点（如果没记错的话），Ken Thompson、Robert Griesemer、Russ Cox、Ian Taylor、Adam Langley、Jini Kim和我满怀期待地看着网站上线。之后，全世界都知道我们在做什么了。</p>
<p>14年后的今天，有很多事情值得回顾。我想借此机会谈谈自那一天以来学到的一些重要经验。即使是最成功的项目，在反思之后，也会发现一些事情本可以做得更好。当然，也有一些事情事后看来似乎是成功的关键所在。</p>
<p>首先，我必须明确的是，这里的观点只代表我个人，不代表Go团队和Google。无论是过去还是现在，Go都是由一支专注的团队和庞大的社区付出巨大努力的结果。所以，如果你同意我的任何说法，请感谢他们。如果你不同意，请责怪我，但请保留你的意见。</p>
<p>鉴于本次演讲的题目，许多人可能期待我会分析语言中的优点和缺点。当然，我会做一些分析，但还会有更多内容，原因有几个。</p>
<p>首先，编程语言的好坏很大程度上取决于观点而不是事实，尽管许多人对Go或任何其他语言的最微不足道的功能都存在争论。</p>
<p>另外，关于换行符的位置、nil的工作方式、导出的大小写表示法、垃圾回收、错误处理等话题已经有了大量的讨论。这些话题肯定有值得讨论的地方，但几乎没什么是还没有被讨论过的。</p>
<p>但我要讨论的不仅仅是语言本身的真正原因是，语言并不是整个项目的全部。我们最初的目标不是创造一种新的编程语言，而是<a href="https://tonybai.com/2022/05/04/the-paper-of-go-programming-language-and-environment">创造一种更好的编写软件的方式</a>。我们对所使用的语言有意见——无论使用什么语言，每个人都是如此——但是我们遇到的基本问题与这些语言的特性没有太大关系，而是与在谷歌使用这些语言构建软件的过程有关。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-3.png" alt="" /><br />
<center>T恤上的第一只Gopher</center></p>
<p>新语言的创建提供了探索其他想法的新路径，但这只是一个推动因素，而不是真正的重点。如果当时我正在工作的二进制文件不需要45分钟来构建<br />
，Go语言就不会出现。但那45分钟不是因为编译器慢(因为它不慢)，也不是因为它所用的语言不好(因为它也不差)。缓慢是由其他因素造成的。</p>
<p>我们想解决的就是这些因素：构建现代服务器软件的复杂性：控制依赖性、与人员不断变化的大型团队一起编程、可维护性、高效测试、多核CPU和网络的有效利用等等。</p>
<p>简而言之，Go不仅仅是一种编程语言。当然，它是一种编程语言，这是它的定义。但它的目的是帮助提供一种更好的方式来开发高质量的软件，至少与14多年前的我们的环境相比。</p>
<p>时至今日，这仍然是它的宗旨。Go是一个使构建生产软件更容易、更高效的项目。</p>
<p>几周前，当我开始准备这次演讲时，我只有一个题目，除此之外别无其他。为了激发我的思路，我在Mastodon上向人们征求意见。不少人给予了回复。我注意到了一种趋势：人们认为我们做错的事情都在语言本身，而我们做对的事情都在语言周边，比如gofmt、部署和测试等。事实上，我觉得这令人鼓舞。我们试图做的事情似乎已经产生了效果。</p>
<p>但值得承认的是，我们在早期并没有明确真正的目标。我们可能觉得这些目标是不言自明的。为了弥补这一缺陷，我在2013年的SPLASH会议上发表了一场题为《<a href="https://go.dev/talks/2012/splash.article">谷歌的Go语言：面向软件工程的语言设计</a>》的演讲。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-4.png" alt="" /><br />
<center>Go at Google</center></p>
<p>那场演讲和相关的博客文章可能是对Go语言为何而生的最好诠释。</p>
<p>今天的演讲是SPLASH演讲的后续，回顾了我们在构建语言之后所学到的经验教训，并且可以更广泛地应用于更大的图景。</p>
<p>那么&#8230;&#8230;来谈谈一些教训。</p>
<p>首先，当然，我们有：</p>
<h2>The Gopher</h2>
<p>以Go Gopher吉祥物开始可能看起来是一个奇怪的起点，但Go gopher是Go成功的最早因素之一。在发布Go之前，我们就知道我们想要一个吉祥物来装饰周边商品——每个项目都需要周边商品——Renee French主动提出为我们制作一个这样的吉祥物。在这一点上，我们做得非常正确。</p>
<p>下面最早的Gopher毛绒玩具的图片：</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-5.jpeg" alt="" /><br />
<center>The Gopher</center></p>
<p>这是Gopher的照片，它的第一个原型不太成功。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-6.jpeg" alt="" /><br />
<center>Gopher和它进化程度较低的祖先</center></p>
<p>Gopher是一个吉祥物，它也是荣誉徽章，甚至是世界各地Go程序员的身份标志。此时此刻，你正在参加一个名为GopherCon的会议，这是众多GopherCon会议中的一个。拥有一个从第一天就准备好分享信息的容易识别、有趣的生物，对Go的成长至关重要。它天真又聪明——它可以构建任何东西!</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-7.jpeg" alt="" /><br />
<center>Gopher建造机器人（Renee French 绘图）</center></p>
<p>它为社区参与该项目奠定了基调，这是卓越的技术与真正的乐趣相结合的基调。最重要的是，Gopher是社区的一面旗帜，一面团结起来的旗帜，尤其是在早期，当Go还是编程界的新贵时。</p>
<p>这是几年前Gopher参加巴黎会议的照片，看看他们多兴奋！</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-8.png" alt="" /><br />
<center>巴黎的Gopher观众（Brad Fitzpatrick摄）</center></p>
<p>尽管如此，在知识共享署名许可(Creative Commons Attribution license)下发布Gopher的设计也许不是最好的选择。一方面，它鼓励人们以有趣的方式重新组合他，这反过来又有助于培养社区精神。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-9.png" alt="" /><br />
<center>Gopher model sheet</center></p>
<p>Renee创建了一个“模型表”来帮助艺术家在保持其精神原貌的同时进行艺术创作。</p>
<p>一些艺术家利用这些特征制作了自己版本的Gopher并获得了乐趣；Renee和我最喜欢的版本是日本设计师@tottie的和游戏程序员@tenntennen的：</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-10.jpeg" alt="" /><br />
<center>@tottie的Gopher</center></p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-11.png" alt="" /><br />
<center>@tenntennen 的gopher</center></p>
<p>但许可证的“归属”部分常常会导致令人沮丧的争论，或者导致Renee的创作不属于她，也不符合原作的精神。而且，说实话，这种归属往往只是不情愿地得到尊重，或者根本没有得到尊重。例如，我怀疑@tenntennen是否因他的Gopher插图被使用而获得补偿或是得到承认。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-12.png" alt="" /><br />
<center>gophervans.com: Boo!</center></p>
<p>因此，如果让我们重来一次，我们会认真思考确保吉祥物忠于其理想的最佳方法。维护吉祥物是一件很难的事，而且解决方案仍然难以捉摸。</p>
<p>但更多的是技术性的事情。</p>
<h2>做的对的事情</h2>
<p>这里有一份我认为我们在客观上做对了的事情的清单，特别是在回顾的时候。并不是每一个编程语言项目都做了这些事情，但清单中的每一件对Go的最终成功都至关重要。我会试着言简意赅，因为这些话题都已为人所熟知。</p>
<h3>1. 语言规范(Specification)</h3>
<p>我们从正式的语言规范开始。这不仅可以在编写编译器时锁定行为，还可以使多个编译器实现共存并就该行为达成一致。编译器本身并不是一个规范。你测试编译器的依据是什么？</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-13.png" alt="" /><br />
<center>Web上的Go语言规范</center></p>
<p>哦，顺便说一句，该规范的初稿是在这里编写的，位于悉尼达令港一栋建筑的18层。我们正在Go的家乡庆祝Go的生日。</p>
<h3>2. 多种实现</h3>
<p>Go有多个编译器实现，它们都实现相同的语言规范。有了规范就可以更容易地实现这一点。</p>
<p>有一天，伊恩·泰勒（Ian Taylor）发邮件通知我们，在阅读了我们的语言规范草案后，他自己编写了一个编译器，这让我们感到惊讶！</p>
<pre><code>Subject: A gcc frontend for Go
From: Ian Lance Taylor
Date: Sat, Jun 7, 2008 at 7:06 PM
To: Robert Griesemer, Rob Pike, Ken Thompson

One of my office-mates pointed me at http://.../go_lang.html .  It
seems like an interesting language, and I threw together a gcc
frontend for it.  It's missing a lot of features, of course, but it
does compile the prime sieve code on the web page.
</code></pre>
<p>这的确令人兴奋，但更多的编译器实现也随之而来了，所有这些都因正式规范的存在而成为可能。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-14.jpeg" alt="" /><br />
<center>很多编译器</center></p>
<p>拥有多个编译器帮助我们改进了语言并完善了规范，并为那些不太喜欢我们类似Plan-9的业务方式的其他人提供了替代环境。稍后会详细介绍。如今有很多兼容的实现，这很棒！</p>
<h3>3. 可移植性</h3>
<p>我们使Go应用的交叉编译变得轻而易举，程序员可以在他们喜欢的任何平台上工作，并交付到任何需要的平台。使用Go可能比使用任何其他语言更容易达成这一点。很容易将编译器视为运行它的机器的本地编译器，但没有理由这么认为。打破这个假设具有重要意义，这对许多开发者来说都是新鲜事。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-15.png" alt="" /><br />
<center>可移植性</center></p>
<h3>4. 兼容性</h3>
<p>我们努力使语言达到1.0版本的标准，然后通过兼容性保证将其固定下来，这对Go的采用产生了非常明显的影响！我不理解为什么大多数其他项目一直在抵制这样做。是的，保持强大兼容性的确需要付出成本，但它可以阻止功能特性停滞，而在这个几乎没有其他东西保持稳定的世界里，不必担心新版本的Go会破坏你的项目，这足以令人感到欣喜！</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-16.png" alt="" /><br />
<center>Go兼容性承诺</center></p>
<h3>5. 标准库</h3>
<p>尽管它的增长在某种程度上是偶然的，因为在一开始没有其他地方可以安装Go代码，但拥有一个坚实、制作精良的标准库，其中包含编写21世纪服务器代码所需的大部分内容，这是一个重大资产。在我们积累了足够的经验来理解还应该提供什么之前，它使整个社区都使用相同的工具包。这非常有效，并有助于防止出现不同版本的库，从而帮助统一社区。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-17.jpeg" alt="" /><br />
<center>标准库</center></p>
<h3>6. 工具</h3>
<p>我们确保该语言易于解析，从而支持工具构建。起初我们认为Go需要一个IDE，但易于构建工具意味着，随着时间的推移，IDE将会出现在Go上。他们和gopls一起做到了，而且他们非常棒。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-18.jpeg" alt="" /><br />
<center>工具</center></p>
<p>我们还为编译器提供了一套辅助工具，例如自动化测试、覆盖率和代码审查(code vetting)。当然还有go命令，它集成了整个构建过程，也是许多项目构建和维护其Go代码所需的一切。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-19.jpeg" alt="" /><br />
<center>快速构建</center></p>
<p>此外，Go获得了快速构建的声誉，这也没有什么坏处。</p>
<h3>7. Gofmt</h3>
<p>我将gofmt作为一个单独的项目从工具中拿出来，因为它是一个不仅在Go上而且在整个编程社区上留下了印记的工具。在Robert编写gofmt之前（顺便说一句，他从一开始就坚持这样做），自动格式化程序的质量不高，因此大多未被使用。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-20.png" alt="" /><br />
<center>gofmt谚语</center></p>
<p>gofmt的成功表明了代码自动格式化可以做得很好，今天几乎每种值得使用的编程语言都有一个标准格式化程序。我们不再为空格和换行符争论，这节省了大量时间了，这也让那些花在定义标准格式和编写这段相当困难的代码实现格式自动化上的时间显得超值。</p>
<p>此外，gofmt还使无数其他工具成为可能，例如简化器、分析器甚至是代码覆盖率工具。因为gofmt的内容成为了任何人都可以使用的库，所以你可以解析程序、编辑AST，然后打印完美的字节输出，供人类和机器使用。</p>
<p>谢谢，罗伯特。</p>
<p>不过，恭喜你就够了。接下来，我们来谈谈一些更有争议的话题。</p>
<h2>并发性</h2>
<p>并发有争议吗？嗯，在我2002年加入谷歌的那年肯定有。John Ousterhout曾说过：线程很糟糕。许多人都同意他的观点，因为线程似乎非常难以使用。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-21.png" alt="" /><br />
<center>John Ousterhout不喜欢线程</center></p>
<p>谷歌的软件几乎总是避免使用它们，可以说是彻底禁止使用，而制定这一禁令的工程师引用了Ousterhout的言论。这让我很困扰。自20世纪70年代以来，我一直在做类似的并发事情，有时候甚至没有意识到，在我看来这很强大。但经过反思，很明显Ousterhout犯了两个错误。首先，他的结论超出了他有兴趣使用线程的领域，其次，他主要是在抱怨使用笨拙的低级包如pthread之类的线程，而不是抱怨这一基本思想。</p>
<p>像这样混淆解决方案和问题是世界各地工程师常犯的错误。有时，提出的解决方案比它解决的问题更难，并且很难看到有更简单的路径。但我离题了。</p>
<p>根据经验，我知道有更好的方法来使用线程，或者无论我们选择怎么称呼它们，我甚至在Go语言出现之前就曾就此发表过演讲。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-22.png" alt="" /><br />
<center>Newsqueak中的并发</center></p>
<p>但我并不孤单，其他许多语言、论文甚至书籍都表明，并发编程可以做得很好，不仅我知道这一点。它只是还没有在主流中流行起来，Go的诞生部分地就是为了解决这个问题。在那次臭名昭著的45分钟构建中，我试图向一个非线程二进制文件添加一个线程，这非常困难，因为我们使用了错误的工具。</p>
<p>回顾过去，我认为可以公平地说，Go在让编程界相信并发是一种强大工具方面发挥了重要作用，特别是在多核网络世界中，它可以比pthread做得更好。如今，大多数主流语言都对并发提供了很好地支持。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-23.png" alt="" /><br />
<center>Google 3.0</center></p>
<p>另外，Go的并发版本在导致它出现的语言线中有些新颖，因为它使goroutine变得平淡无奇。没有协程，没有任务，没有线程，没有名称，只有goroutine。我们发明了“goroutine”这个词，因为没有适合的现有术语。时至今日，我仍然希望Unix的拼写命令可以学会它。</p>
<p>顺便说一句，因为我经常被问到，让我花一分钟时间谈谈async/await。看到async/await模型及其相关风格成为许多语言选择支持并发的方式，我有点难过，但它肯定是对pthreads的巨大改进。</p>
<p>与goroutine、channel和select相比，async/await对语言实现者来说更容易也更小，可以更容易地内建或后移植到现有平台中。但它将一些复杂性推回给了程序员，通常会导致Bob Nystrom所著名的“<a href="https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/">彩色函数</a>”。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-24.png" alt="" /><br />
<center>你的函数是什么颜色的</center></p>
<p>我认为Go表明了CSP这种不同但更古老的模型可以完美地嵌入到过程化语言中，没有这种复杂性。我甚至看到它几次作为库实现。但它的实现，如果做得好，需要显著的运行时复杂性，我可以理解为什么一些人更倾向于不在他们的系统中内置它。不管你提供什么并发模型，重要的是只提供一次，因为一个环境提供多个并发实现可能会很麻烦。Go当然通过把它放在语言中而不是库中解决了这个问题。</p>
<p>关于这些问题可能要讲整场演讲，但目前就这些吧。</p>
<p>并发的另一个价值在于，它使Go看起来像是全新的东西。如我所说，一些其他语言在之前已经支持了它，但它们从未进入主流，而Go对并发的支持是吸引初学者采用的一个主要因素，它吸引了以前没有使用过并发但对其可能性感兴趣的程序员。</p>
<p>这就是我们犯下两个大错误的地方。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-25.png" alt="" /><br />
<center>耳语的Gopher(Cooperating Sequential Processes)</center></p>
<p>首先，并发很有趣，我们很高兴拥有它，但我们设想的使用案例大多是服务器相关的，意在在net/http等关键库中完成，而不是在每个程序的所有地方完成。当许多程序员使用它时，他们努力研究它如何真正帮助他们。我们应该一开始就解释清楚，语言中的并发支持真正带到桌面的是更简单的服务器软件。这个问题空间对许多人很重要，但并非所有尝试Go的人都是如此，这点指导不足是我们的责任。</p>
<p>相关的第二点是，我们用了太长时间来澄清并行和并发之间的区别——支持在多核机器上并行执行多个计算，以及一种组织代码的方式，以便很好地执行并行计算。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-26.png" alt="" /><br />
<center>并发不是并行</center></p>
<p>无数程序员试图通过使用goroutine来并行化他们的代码以使其更快，但经常对结果中的速度降低感到困惑。仅当基础问题本质上是并行的时候，例如服务HTTP请求，并发代码才会通过并行化而变快。我们在解释这一点上做得很糟糕，结果让许多程序员感到困惑，可能还赶走了一些人。</p>
<p>为了解决这个问题，我在2012年Waza上给Heroku的开发者大会做了一个题为“<a href="https://www.youtube.com/watch?v=oV9rvDllKEg">并发不是并行</a>”的演讲。这是一次很有趣的演讲，但它应该更早发生。</p>
<p>对此表示歉意。但好处仍然存在：Go帮助普及了并发性作为构建服务器软件的一种方式。</p>
<h2>接口</h2>
<p>很明显，接口与并发都是Go中与众不同的思想。它们是Go对面向对象设计的答案，采用最初关注行为的风格，尽管新来者一直在努力使结构体承担这一角色。</p>
<p>使接口动态化，无需提前宣布哪些类型实现了它们，这困扰了一些早期评论者，并且仍然恼火一小部分人，但它对Go培育的编程风格很重要。大部分标准库都是建立在它们的基础之上的，而更广泛的主题如测试和管理依赖也高度依赖于它们慷慨的“欢迎所有人”的天性。</p>
<p>我觉得接口是Go中设计最好的部分之一。</p>
<p>除了一些早期关于接口定义中是否应该包括数据的讨论之外，它们在讨论的第一天就已经成形。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-27.png" alt="" /><br />
<center>GIF 解码器：Go接口的练习（Rob Pike和Nigel Tao 2011）</center></p>
<p>在这个问题上还有一个故事要讲。</p>
<p>在Robert和我的办公室里那著名的第一天，我们讨论了关于多态性应该怎么处理的问题。Ken和我从C语言中知道qsort可以作为一个困难的测试用例，所以我们三个人开始讨论用我们这种初具雏形的语言如何实现一个类型安全的排序例程(routine)。</p>
<p>Robert和我几乎同时产生了同样的想法：在类型上使用方法来提供排序所需的操作。这个概念很快发展成了一个想法，即值类型拥有作为方法定义的行为，一组方法可以提供函数可以操作的接口。Go的接口几乎立即就出现了。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-28.png" alt="" /><br />
<center>sort.Interface</center></p>
<p>有一点没人经常提到：Go的sort函数是作为一个在接口上操作的函数实现的。这与大多数人熟悉的面向对象编程风格不同，但这是一个非常强大的想法。</p>
<p>这个想法对我们来说非常激动人心，它可能成为一个基础的编程构造，这令我们陶醉。当Russ Cox加入时，他很快指出了I/O如何完美地融入这个想法，标准库的发展非常迅速，在很大程度上依赖于三个著名的接口：空接口(interface{})、Writer和Reader，每个接口平均包含两个第三个方法。那些微小的方法对Go来说是惯用法，无处不在。</p>
<p>接口的工作方式不仅成为Go的一个显著特性，它们也成为我们思考库、泛型和组合的方式。这是让人兴奋的事情。</p>
<p>但我们在这个问题上停止讨论可能是一个错误。</p>
<p>你看，我们之所以走上这条路，至少在一定程度上是因为我们看到泛型编程太容易鼓励一种倾向于在算法之前首先关注类型的思考方式。过早抽象而不是有机设计。容器而不是函数。</p>
<p>我们在语言中正确定义了通用容器——map，切片，数组，channel——而不给程序员访问它们所包含的泛型。这可以说是一个错误。我们相信，我认为仍然正确的是，大多数简单的编程任务可以很好地由这些类型来处理。但有一些不能，语言提供的和用户可以控制的之间的障碍肯定困扰了一些人。</p>
<p>简而言之，尽管我不会改变接口的任何工作方式，但它们以需要十多年时间才能纠正的方式影响了我们的思维。Ian Taylor从一开始就推动我们面对这个问题，但在接口作为Go编程基石的情况下，这是相当困难的。</p>
<p>评论者经常抱怨我们应该使用泛型，因为它们“很简单”，在某些语言中可能确实如此，但接口的存在意味着任何新的多态形式都必须考虑到它们。找到一种可以与语言的其余部分很好地协同工作的前进方法需要多次尝试，几次中止的实现，以及许多小时、天数和周数的讨论。最终，在Phil Wadler的带领下，我们召集了一些类型理论家来提供帮助。即使在语言中有了可靠的泛型模型，作为方法集存在的接口也仍然存在一些遗留问题。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-29.png" alt="" /><br />
<center>泛型版sort</center></p>
<p>如你所知，最终的答案是设计一个可以吸收更多多态形式的接口泛化，从“方法集合”过渡到“类型集合”。这是一个微妙但深刻的举措，大多数社区似乎都可以接受，尽管我怀疑抱怨声永远不会停止。</p>
<p>有时候要花很多年的时间来弄清楚一些事情，或者甚至弄清楚你并不能完全弄明白它。但你还是要继续前进。</p>
<p>顺便说一句，我希望我们有一个比“泛型”更好的术语，它起源于表示一种不同的数据结构中心多态风格。“参数多态”是Go提供的该功能的正确术语，这是一个准确的术语，但它难听。于是我们依然说“泛型”，尽管它不太恰当。</p>
<h2>编译器</h2>
<p>困扰编程语言社区的一件事是，早期的Go编译器是用C语言编写的。在他们看来，正确的方式是使用LLVM或类似的工具包，或者用Go语言本身编写编译器，这称为自举。我们没有做这两者中的任何一种，原因有几个。</p>
<p>首先，自举一种新语言要求至少其编译器的第一步必须用现有语言完成。对我们来说，C语言是显而易见的选择，因为Ken已经编写了C编译器，并且其内部结构可以很好地作为Go编译器的基础。此外，用自己的语言编写编译器，同时开发该语言，往往会产生一种适合编写编译器的语言，但这不是我们想要的语言。</p>
<p>早期的编译器工作良好，它可以很好地引导语言。但从某种意义上说，它有点奇怪，实际上它是一个Plan 9风格的编译器，使用旧的编译器编写思想，而不是新的思想，如<a href="https://tonybai.com/2022/10/21/understand-go-ssa-by-example/">静态单一赋值(SSA)</a>。生成的代码平庸，内部不太漂亮。但它是务实高效的，编译器代码本身体积适中，对我们来说也很熟悉，这使得我们在尝试新想法时可以快速进行更改。一个关键步骤是添加自动增长的分段堆栈。这很容易添加到我们的编译器中，但是如果我们使用像LLVM这样的工具包，考虑到ABI和垃圾收集器支持所需的更改，将这种更改集成到完整的编译器套件中是不可行的。</p>
<p>另一个工作良好的区域是交叉编译，这直接来自原始Plan 9编译器套件的工作方式。</p>
<p>按照我们的方式行事，无论多么非正统，都有助于我们快速前进。有些人对这一选择感到冒犯，但这对当时的我们来说是正确的选择。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-30.png" alt="" /><br />
<center>Go 1.5之后的Go编译器架构</center></p>
<p>对于Go 1.5版本，Russ Cox编写了一个工具，可以半自动将编译器从C转换为Go。到那时，语言已经完成，编译器导向的语言设计的担忧也就无关紧要了。有一些关于这个过程的在线演讲值得一看。我在2016年的GopherCon上做了一个关于汇编器的演讲，这在我毕生追求可移植性的过程中是一个高点。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-31.png" alt="" /><br />
<center>Go汇编器设计(GopherCon 2016) </center></p>
<p>我们从C开始做了正确的事情，但最终将编译器翻译为Go，使我们能够将Go所具有的所有优势带到其开发中，包括测试、工具、自动重写、性能分析等。当前的编译器比原始编译器干净得多，并且可以生成更好的代码。但是，当然，这就是自举的工作原理。</p>
<p>请记住，我们的目标不仅仅是一种语言，而是更多。</p>
<p>我们不寻常的做法绝不是对LLVM或语言社区中任何人的侮辱。我们只是使用了最适合我们任务的工具。当然，今天有一个LLVM托管的Go编译器，以及许多其他应该有的编译器。</p>
<h2>项目管理</h2>
<p>我们从一开始就知道，要成功，Go必须是一个开源项目。但我们也知道，在弄清楚关键的思想和有一个工作的实现之前，私下开发会更高效。头两年对澄清我们在试图实现什么，而不受干扰，是必不可少的。</p>
<p>向开源的转变是一个巨大的改变，也很具教育意义。来自社区的投入是压倒性的。与社区的接触花费了大量的时间和精力，尤其是对Ian，不知怎么他找到时间来回答任何人提出的每一个问题。但它也带来了更多。我仍然惊叹在Alex Brainman的指导下，社区完全独立完成的Windows移植的速度。那很神奇。</p>
<p>我们花了很长时间来理解转向开源项目的影响，以及如何管理它。</p>
<p>特别是，公平地说，我们花了太长时间来理解与社区合作的最佳方式。本次演讲的一个主题是我们的沟通不足——即使我们认为我们正在进行良好沟通——由于误解和不匹配的期望，大量时间被浪费了。本可以做得更好。</p>
<p>但是，随着时间的推移，我们说服了社区中的至少那一部分和我们在一起的人，我们的一些想法，虽然与常见的开源方式不同，但具有价值。最重要的是<strong>我们坚持通过强制代码审查和对细节的穷尽关注来维护高质量代码</strong>。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-32.png" alt="" /><br />
<center>Mission Control (drawing by Renee French) </center></p>
<p>一些项目的工作方式不同，它们快速接受代码，然后在提交后进行清理。Go项目则相反，力图将质量放在第一位。我相信这是更有效的方式，但它将更多的工作推回社区，如果他们不理解其价值，他们就不会感到应有的欢迎。在这方面还有很多东西要学习，但我相信现在的情况已经好多了。</p>
<p>顺便说一句，有一个历史细节不是广泛为人知的。该项目使用过4个不同的内容管理系统：SVN、Perforce、Mercurial和Git。Russ Cox做了一份艰巨的工作，保留了所有历史，所以即使今天，Git仓库也包含了在SVN中做出的最早的更改。我们都认为保留历史很有价值，我要感谢他做了这项艰苦的工作。</p>
<p>还有一点。人们经常认为谷歌会告诉Go团队该做什么。这绝对不是真的。谷歌对Go的支持非常慷慨，但它不制定议程。社区的投入要大得多。谷歌内部有一个巨大的Go代码库，团队用它来测试和验证版本，但这是通过从公共仓库导入谷歌完成的，而不是反过来。简而言之，核心Go团队由谷歌支付薪水，但他们是独立的。</p>
<h2>包管理</h2>
<p>Go的包管理开发过程做得并不好。我相信，语言本身的包设计非常出色，并且在我们讨论的第一年左右的时间里消耗了大量的时间。如果你感兴趣的话，我之前提到的SPLASH演讲详细解释了它为什么会这样工作。</p>
<p>一个关键点是使用纯字符串来指定导入语句中的路径，从而提供了我们正确认为很重要的灵活性。但从只有一个“标准库”到从网络导入代码的转变是坎坷的。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-33.png" alt="" /><br />
<center>修复云（Renee French 绘制）</center></p>
<p>有两个问题。</p>
<p>首先，我们这些Go核心团队的成员很早就熟悉Google的工作方式，包括它的monorepo(单一代码仓库)和每个人都在负责构建。但是我们没有足够的经验来使用具有大量包版本的包管理器以及尝试解决依赖关系图的非常困难的问题。直到今天，很少有人真正理解技术的复杂性，但这并不能成为我们未能从一开始就解决这些问题的借口。这尤其令人尴尬，因为我曾是一个失败项目的技术负责人，为谷歌的内部构建做类似的事情，我应该意识到我们面临的是什么。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-34.png" alt="" /><br />
<center>deps.dev</center></p>
<p>我在deps.dev上的工作是一种忏悔。</p>
<p>其次，让社区参与帮助解决依赖管理问题的初衷是好的，但当最终设计出来时，即使有大量的文档和有关理论的文章，社区中的许多人仍然感到受到了轻视。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-35.png" alt="" /><br />
<center>pkg.go.dev</center></p>
<p>这次失败给团队上了一课，让他们知道如何真正与社区互动，并且自此取得了很大的进步。</p>
<p>不过，现在事情已经解决了，新的设计在技术上非常出色，并且似乎对大多数用户来说效果很好。只是时间太长，而且道路崎岖不平。</p>
<h2>文档和示例</h2>
<p>我们事先没有得到的另一件事是文档。我们写了很多文档，并认为我们做得很好，但很快就发现社区想要的文档级别与我们的预期不同。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-36.png" alt="" /><br />
<center>修理图灵机的Gopher（Renee French 绘图）</center></p>
<p>关键缺失的一部分是最简单函数的示例。我们曾以为只需说明某个东西的功能就足够了，但我们花费了太长时间才接受到展示如何使用它的价值更大。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-37.png" alt="" /><br />
<center>可执行的例子</center></p>
<p>不过，我们已经吸取了教训。现在文档中有很多示例，大部分是由开源贡献者提供的。我们很早就做的一件事就是让它们在网络上可执行。我在2012年的Google I/O大会上做了一次演讲，展示了并发的实际应用，Andrew Gerrand 编写了一段可爱的Web goo，使得直接从浏览器运行代码片段成为可能。我怀疑这是第一次这样做，但Go是一种编译语言，很多观众以前从未见过这个技巧。然后该技术被部署到博客和在线包文档中。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-38.png" alt="" /><br />
<center>Go playground</center></p>
<p>也许更重要的是我们对Go Playground的支持，这是一个免费的开放沙箱，供人们尝试，甚至开发代码。</p>
<h2>结论</h2>
<p>我们已经走了很长一段路。</p>
<p>回顾过去，很明显很多事情都做得对，并且它们都帮助Go取得了成功。但还有很多事情可以做得更好，重要的是要承认这些问题并从中学习。对于任何托管重要开源项目的人来说，双方都有教训。</p>
<p>我希望我对这些教训及其原因的历史回顾会有所帮助，也许可以作为对那些反对我们正在做的事情和我们如何做的人的一种道歉/解释。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-39.png" alt="" /><br />
<center>GopherConAU 2023 吉祥物，作者：Renee French</center></p>
<p>但在推出 14 年后，我们终于来了。公平地说，总的来说这是一个非常好的地方。</p>
<p>很大程度上是因为通过设计和开发Go作为一种编写软件的方式（而不仅仅是作为一种编程语言）做出的决定，我们已经到达了一个新的地方。</p>
<p>我们到达这里的部分原因包括：</p>
<ul>
<li>一个强大的标准库，可实现服务器代码所需的大部分基础知识</li>
<li>并发作为该语言的“一等公民”</li>
<li>基于组合而不是继承的方法</li>
<li>澄清依赖管理的打包模型</li>
<li>集成的快速构建和测试工具</li>
<li>严格一致的代码格式</li>
<li>注重可读性而非聪明性</li>
<li>兼容性保证</li>
</ul>
<p>最重要的是，得益于令人难以置信的乐于助人且多元化的Gophers社区的支持。</p>
<p><img src="https://tonybai.com/wp-content/uploads/what-we-got-right-what-we-got-wrong-40.png" alt="" /><br />
<center>多元化的社区（@tenntennen 绘图）</center></p>
<p>也许这些问题最有趣的结果是，无论是谁编写的Go代码的外观和工作原理都是一样的，基本上没有使用该语言的不同子集的派系，并且保证随着时间的推移代码可继续编译和运行。对于主要编程语言来说，这可能是第一次。</p>
<p>我们绝对做对了。</p>
<p>谢谢。</p>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2024年，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; 2024, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2024/01/07/what-we-got-right-what-we-got-wrong/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>小厂内部私有Go module拉取方案</title>
		<link>https://tonybai.com/2021/09/03/the-approach-to-go-get-private-go-module-in-house/</link>
		<comments>https://tonybai.com/2021/09/03/the-approach-to-go-get-private-go-module-in-house/#comments</comments>
		<pubDate>Fri, 03 Sep 2021 10:29:41 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[athens]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[gitlab]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-get]]></category>
		<category><![CDATA[go-install]]></category>
		<category><![CDATA[go1.11]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[GONOSUMDB]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[GOPRIVATE]]></category>
		<category><![CDATA[GOPROXY]]></category>
		<category><![CDATA[goproxy.cn]]></category>
		<category><![CDATA[goproxy.io]]></category>
		<category><![CDATA[govanityurls]]></category>
		<category><![CDATA[hg]]></category>
		<category><![CDATA[nginx]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[proxy.golang.org]]></category>
		<category><![CDATA[svn]]></category>
		<category><![CDATA[vcs]]></category>
		<category><![CDATA[yaml]]></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=3277</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2021/09/03/the-approach-to-go-get-private-go-module-in-house 1. 问题来由 Go 1.11版本引入Go module后，Go命令拉取依赖的公共go module不再是“痛点”。如下图所示： 图：从公司内部经由公共GOPROXY服务拉取公共go module 我们在公司/组织内部仅需要为环境变量GOPROXY配置一个公共GOPROXY服务即可轻松拉取所有公共go module(公共module即开源module)。 但随着公司内Go使用者增多以及Go项目的增多，“代码重复”问题就出现了。抽取公共代码放入一个独立的、可被复用的内部私有仓库成为必然。这样我们便有了拉取私有go module的需求！ 一些公司或组织的所有代码都放在公共vcs托管服务商那里(比如github.com)，私有go module则直接放在对应的公共vcs服务的private repository(私有仓库)中。如果你的公司也是如此，那么拉取托管在公共vcs私有仓库中的私有go module也很容易，见下图： 图：从公司内部直接拉取托管在公共vcs服务上的私有go module 当然这个方案的一个前提是：每个开发人员都需要具有访问公共vcs服务上的私有go module仓库的权限，凭证的形式不限，可以是basic auth的user和password，也可以是personal access token(类似github那种)，只要按照公共vcs的身份认证要求提供即可。 但是如果私有go module放在公司内部的vcs服务器上，就像下面图中所示： 图：私有go module放在组织/公司内部的vcs服务器上 那么我们该如何让Go命令自动拉取内部服务器上的私有go module呢？ 一些gopher会说：“这很简单啊! 这和拉取托管在公共vcs服务上的私有go module没有什么分别啊”。持这种观点的gopher多半来自大厂。大厂内部有完备的IT基础设施供开发使用，大厂内部的vcs服务器都可以通过域名访问(比如git.bat.com/user/repo)，因此大厂内部员工可以像访问公共vcs服务那样访问内部vcs服务器上的私有go module，就像下面图中所示： 图：大厂方案：直接拉取内部vcs仓库上的私有go module 我们看到：在上面这个方案中，公司搭建了一个内部goproxy服务(即上图中的in-house goproxy)，这样的目的一来是为那些无法直接访问外网的开发机器以及ci机器提供拉取外部go module的途径，二来由于in-house goproxy的cache的存在，还可以加速公共go module的拉取效率。对于私有go module，开发机将其配置到GOPRIVATE环境变量中，这样Go命令在拉取私有go module时不会再走GOPROXY，而会采用直接访问vcs(如上图中的git.bat.com)的方式拉取私有go module。 当然大厂还可能采用下图所示方案将外部go module与私有go module都交给内部统一的Goproxy服务去处理： 图：大厂方案: 统一代理方案 在这种方案中，开发者仅需要将GOPROXY配置为in-house goproxy便可以统一拉取外部go module与私有go module。但由于go命令默认会对所有通过goproxy拉取的go [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/the-approach-to-go-get-private-go-module-in-house-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2021/09/03/the-approach-to-go-get-private-go-module-in-house">本文永久链接</a> &#8211; https://tonybai.com/2021/09/03/the-approach-to-go-get-private-go-module-in-house</p>
<h3>1. 问题来由</h3>
<p><a href="https://mp.weixin.qq.com/s?__biz=MzIyNzM0MDk0Mg==&amp;mid=100000482&amp;idx=1&amp;sn=b5a588b8b4cd63ac57b29ee6e64438aa&amp;chksm=6863e5035f146c152ae2a7460dea924df4b14a56bbcbee1966934abed3fcfd492bc6f56928b2#rd">Go 1.11版本</a>引入<a href="https://tonybai.com/tag/gomodule">Go module</a>后，Go命令拉取依赖的公共go module不再是“痛点”。如下图所示：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-approach-to-go-get-private-go-module-in-house-2.png" alt="" /><br />
<center>图：从公司内部经由公共GOPROXY服务拉取公共go module</center></p>
<p>我们在公司/组织内部仅需要为环境变量GOPROXY配置一个公共GOPROXY服务即可轻松拉取所有公共go module(公共module即开源module)。</p>
<p>但随着公司内Go使用者增多以及Go项目的增多，“代码重复”问题就出现了。抽取公共代码放入一个独立的、可被复用的内部私有仓库成为必然。这样我们便<strong>有了拉取私有go module的需求！</strong></p>
<p>一些公司或组织的所有代码都放在公共vcs托管服务商那里(比如github.com)，私有go module则直接放在对应的公共vcs服务的private repository(私有仓库)中。如果你的公司也是如此，那么拉取托管在公共vcs私有仓库中的私有go module也很容易，见下图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-approach-to-go-get-private-go-module-in-house-3.png" alt="" /><br />
<center>图：从公司内部直接拉取托管在公共vcs服务上的私有go module</center></p>
<p>当然这个方案的一个前提是：每个开发人员都需要具有访问公共vcs服务上的私有go module仓库的权限，凭证的形式不限，可以是basic auth的user和password，也可以是personal access token(类似github那种)，只要按照公共vcs的身份认证要求提供即可。</p>
<p>但是如果私有go module放在公司内部的vcs服务器上，就像下面图中所示：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-approach-to-go-get-private-go-module-in-house-4.png" alt="" /><br />
<center>图：私有go module放在组织/公司内部的vcs服务器上</center></p>
<p>那么我们该如何让Go命令自动拉取内部服务器上的私有go module呢？</p>
<p>一些gopher会说：“<strong>这很简单啊! 这和拉取托管在公共vcs服务上的私有go module没有什么分别啊</strong>”。持这种观点的gopher多半来自大厂。大厂内部有完备的IT基础设施供开发使用，大厂内部的vcs服务器都可以通过域名访问(比如git.bat.com/user/repo)，因此大厂内部员工可以像访问公共vcs服务那样访问内部vcs服务器上的私有go module，就像下面图中所示：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-approach-to-go-get-private-go-module-in-house-5.png" alt="" /><br />
<center>图：大厂方案：直接拉取内部vcs仓库上的私有go module</center></p>
<p>我们看到：在上面这个方案中，公司搭建了一个内部goproxy服务(即上图中的in-house goproxy)，这样的目的一来是为那些无法直接访问外网的开发机器以及ci机器提供拉取外部go module的途径，二来由于in-house goproxy的cache的存在，还可以加速公共go module的拉取效率。对于私有go module，开发机将其配置到GOPRIVATE环境变量中，这样Go命令在拉取私有go module时不会再走GOPROXY，而会采用直接访问vcs(如上图中的git.bat.com)的方式拉取私有go module。</p>
<p>当然大厂还可能采用下图所示方案将外部go module与私有go module都交给内部统一的Goproxy服务去处理：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-approach-to-go-get-private-go-module-in-house-6.png" alt="" /><br />
<center>图：大厂方案: 统一代理方案 </center></p>
<p>在这种方案中，开发者仅需要将GOPROXY配置为in-house goproxy便可以统一拉取外部go module与私有go module。但由于go命令默认会对所有通过goproxy拉取的go module进行sum校验（到sum.golang.org)，而我们的私有go module在公共sum验证server中没有数据记录，因此，开发者需要将私有go module填到GONOSUMDB环境变量中，这样go命令就不会对其进行sum校验了。不过这种方案有一处要注意：那就是in-house goproxy需要拥有对所有private module所在repo的访问权限，这样才能保证每个私有go module的拉取成功！</p>
<p>好了，问题来了！对于那些没有完备内部IT基础设施，还想将私有go module放在公司内部的vcs服务器上的小厂应该如何实现私有go module的拉取方案呢？</p>
<h3>2. 可供小厂参考的一个解决方案</h3>
<p>小厂虽小，但目标不能低。小厂虽然IT基础设施薄弱或不够灵活，但也不能因此给开发人员带去太多额外的“负担”。因此，对比了上面的两个大厂可能采用的方案，我们更倾向于后者。这样，我们就可以<strong>将所有复杂性都交给in-house goproxy这个节点，开发人员就可以做的足够简单</strong>。但小厂没有DNS，无法用域名&#8230;，我们该怎么实现这个方案呢？在这一节中，我们就实现这个方案。</p>
<h4>0. 方案示例环境拓扑</h4>
<p>我们先为后续的方案实现准备一个示例环境，其拓扑如下图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-approach-to-go-get-private-go-module-in-house-7.png" alt="" /></p>
<h4>1. 选择一个goproxy实现</h4>
<p><a href="https://pkg.go.dev/cmd/go@master#hdr-Module_proxy_protocol">Go module proxy协议规范</a>发布后，Go社区出现了很多成熟的Goproxy开源实现。从最初的<a href="https://tonybai.com/2018/11/26/hello-go-module-proxy/">athens</a>，再到国内的两个优秀的开源实现：<a href="https://github.com/goproxy/goproxy">goproxy.cn</a>和<a href="https://github.com/goproxyio/goproxy">goproxy.io</a>。其中，goproxy.io在官方站点给出了<a href="https://goproxy.io/zh/docs/enterprise.html">企业内部部署的方法</a>，基于这一点，我们就基于goproxy.io来实现我们的方案（其余的goproxy实现应该也都可以实现)。</p>
<p>我们在上图中的in-house goproxy节点上执行下面步骤安装goproxy：</p>
<pre><code>$mkdir ~/.bin/goproxy
$cd ~/.bin/goproxy
$git clone https://github.com/goproxyio/goproxy.git
$cd goproxy
$make
</code></pre>
<p>编译后，会在当前的bin目录(~/.bin/goproxy/goproxy/bin)下看到名为goproxy的可执行文件。</p>
<p>建立goproxy cache目录：</p>
<pre><code>$mkdir /root/.bin/goproxy/goproxy/bin/cache
</code></pre>
<p>启动goproxy：</p>
<pre><code>$./goproxy -listen=0.0.0.0:8081 -cacheDir=/root/.bin/goproxy/goproxy/bin/cache -proxy https://goproxy.io
goproxy.io: ProxyHost https://goproxy.io
</code></pre>
<p>启动后goproxy在8081端口监听(即便不指定，goproxy的默认端口也是8081)，指定的上游goproxy服务为goproxy.io。</p>
<blockquote>
<p>注意：goproxy的这个启动参数并不是最终版本的，这里仅仅想验证一下goproxy是否能按预期工作。</p>
</blockquote>
<p>接下来，我们来验证一下goproxy的工作是否如我们预期。</p>
<p>我们在开发机上配置GOPROXY环境变量指向10.10.20.20:8081：</p>
<pre><code>// .bashrc
export GOPROXY=http://10.10.20.20:8081
</code></pre>
<p>生效环境变量后，执行下面命令：</p>
<pre><code>$go get github.com/pkg/errors
</code></pre>
<p>结果如预期，开发机顺利下载了github.com/pkg/errors包。</p>
<p>在goproxy侧，我们看到了下面日志：</p>
<pre><code>goproxy.io: ------ --- /github.com/pkg/@v/list [proxy]
goproxy.io: ------ --- /github.com/pkg/errors/@v/list [proxy]
goproxy.io: ------ --- /github.com/@v/list [proxy]
goproxy.io: 0.146s 404 /github.com/@v/list
goproxy.io: 0.156s 404 /github.com/pkg/@v/list
goproxy.io: 0.157s 200 /github.com/pkg/errors/@v/list
</code></pre>
<p>并且在goproxy的cache目录下，我们也看到了下载并缓存的github.com/pkg/errors包：</p>
<pre><code>$cd /root/.bin/goproxy/goproxy/bin/cache
$tree
.
└── pkg
    └── mod
        └── cache
            └── download
                └── github.com
                    └── pkg
                        └── errors
                            └── @v
                                └── list

8 directories, 1 file
</code></pre>
<h4>2. 自定义包导入路径并将其映射到内部的vcs仓库</h4>
<p>小厂可能没有为vcs服务器分配域名，我们也不能在Go私有包的导入路径中放入ip地址，因此我们需要给我们的私有go module自定义一个路径，比如：mycompany.com/go/module1。我们统一将私有go module放在mycompany.com/go下面的代码仓库中。</p>
<p>接下来的问题是，当goproxy去拉取mycompany.com/go/module1时，应该得到mycompany.com/go/module1对应的内部vcs上module1 仓库的地址，这样goproxy才能从内部vcs代码服务器上下载到module1对应的代码。</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-approach-to-go-get-private-go-module-in-house-8.png" alt="" /><br />
<center>图：goproxy如何得到mycompany.com/go/module1所对应的vcs仓库地址呢？</center></p>
<p>其实<a href="https://tonybai.com/2020/11/15/another-approach-to-customize-package-import-path">方案不止一种</a>。这里我们使用一个名为<a href="https://tonybai.com/2017/06/30/go-get-go-packages-in-private-code-repo-by-govanityurls">govanityurls</a>的工具，这个工具在<a href="https://tonybai.com/2017/06/28/set-custom-go-get-import-path-for-go-package">我以前的文章</a>中曾提到过。</p>
<p>结合govanityurls和nginx，我们就可以将私有go module的导入路径映射为其在vcs上的代码仓库的真实地址。下面的图解释了具体原理：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-approach-to-go-get-private-go-module-in-house-9.png" alt="" /></p>
<p>首先，goproxy要想将收到的拉取私有go module(mycompany.com/go/module1)的请求不转发给公共代理，需要在其启动参数上做一些手脚，如下面修改后的goproxy启动命令：</p>
<pre><code>$./goproxy -listen=0.0.0.0:8081 -cacheDir=/root/.bin/goproxy/goproxy/bin/cache -proxy https://goproxy.io -exclude "mycompany.com/go"
</code></pre>
<p>这样凡是与-exclude后面的值匹配的go module拉取请求，goproxy都不会转给goproxy.io，而是直接请求go module的“源站”。而上面图中要做的就是将这个“源站”的地址转换为企业内部vcs服务中的一个仓库地址。由于mycompany.com这个域名并不存在，从图中我们看到：我们在goproxy所在节点的/etc/hosts中加了这样一条记录：</p>
<pre><code>127.0.0.1 mycompany.com
</code></pre>
<p>这样goproxy发出的到mycompany.com的请求实则是发向了本机。而上图中所示，监听本机80端口的正是nginx，nginx关于mycompany.com这一主机的配置如下：</p>
<pre><code>// /etc/nginx/conf.d/gomodule.conf

server {
        listen 80;
        server_name mycompany.com;

        location /go {
                proxy_pass http://127.0.0.1:8080;
                proxy_redirect off;
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

                proxy_http_version 1.1;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection "upgrade";
        }
}
</code></pre>
<p>我们看到对于路径为mycompany.com/go/xxx的请求，nginx将请求转发给了127.0.0.1:8080，而这个服务地址恰是<a href="https://github.com/GoogleCloudPlatform/govanityurls">govanityurls</a>工具监听的地址。</p>
<p>govanityurls这个工具是前Go核心开发团队成员<a href="https://rakyll.org">Jaana B.Dogan</a>开源的一个工具，这个工具可以帮助gopher快速<a href="https://tonybai.com/2017/06/30/go-get-go-packages-in-private-code-repo-by-govanityurls/">实现自定义Go包的go get导入路径</a>。</p>
<p>govanityurls本身就好比一个“导航”服务器。当go命令向自定义包地址发起请求时，实则是将请求发送给了govanityurls服务，之后govanityurls将请求中的包所在仓库的真实地址(从vanity.yaml配置文件中读取)返回给go命令，后续go命令再从真实的仓库地址获取包数据。</p>
<blockquote>
<p>注：govanityurls的安装方法很简单，直接go install/go get github.com/GoogleCloudPlatform/govanityurls即可。</p>
</blockquote>
<p>在我们的示例中，vanity.yaml的配置如下：</p>
<pre><code>host: mycompany.com

paths:
  /go/module1:
      repo: ssh://admin@10.10.30.30/module1
      vcs: git
</code></pre>
<p>也就是说当govanityurls收到nginx转发的请求后，会将请求与vanity.yaml中配置的module路径相匹配，如果匹配ok，则会将该module的真实repo地址通过go命令期望的应答格式予以返回。在这里我们看到，module1对应的真实vcs上的仓库地址为：ssh://admin@10.10.30.30/module1。</p>
<p>于是goproxy会收到这个地址，并再次向这个真实地址发起请求，并最终将module1缓存到本地cache并返回给客户端。</p>
<blockquote>
<p>注意：由于这个方案与大厂的第二个方案是一样的，因此goproxy需要有访问mycompany.com/go下面所有go module对应的真实vcs仓库的权限。</p>
</blockquote>
<h4>3. 开发机(客户端)的设置</h4>
<p>前面示例中，我们已经将开发机的GOPROXY环境变量设置为goproxy的服务地址。但我们说过凡是通过GOPROXY拉取的go module，go命令默认都会将其sum值到公共GOSUM服务器上去校验。但我们实质上拉取的是私有go module，GOSUM服务器上并没有我们的go module的sum数据。这样会导致go build命令报错，无法继续构建过程。</p>
<p>因此，开发机客户端还需将mycompany.com/go作为一个值设置到GONOSUMDB环境变量中，这就告诉go命令，凡是与mycompany.com/go匹配的go module，都无需做sum校验了。</p>
<h4>4. 方案的“不足”</h4>
<p>当然上述方案也不是完美的，它也有自己的不足的地方：</p>
<ul>
<li>开发者还是需要额外配置GONOSUMDB变量</li>
</ul>
<p>由于Go命令默认会对从GOPROXY拉取的go module进行sum校验，因此我们需要将私有go module配置到GONOSUMDB环境变量中，这给开发者带来了一个小小的“负担”。</p>
<p>缓解措施：小厂可以将私有go项目都放在一个特定域名下，这样就无需为每个go私有项目单独增加GONOSUMDB配置了，只需要配置一次即可。</p>
<ul>
<li>新增私有go module，vanity.yaml需要手工同步更新</li>
</ul>
<p>这个是这个方案最不灵活的地方了，由于目前govanityurls功能有限，我们针对每个私有go module可能都需要单独配置其对应的vcs仓库地址以及获取方式(git, svn or hg)。</p>
<p>缓解方案：在一个vcs仓库中管理多个私有go module，就像<a href="https://github.com/etcd-io/etcd">etcd</a>那样。相比于最初go官方建议的一个repo只管理一个module，新版本的go在<a href="https://golang.google.cn/doc/modules/managing-source#multiple-module-source">一个repo管理多个go module</a>方面已经有了长足的进步。</p>
<p>不过对于小厂来说，这点额外工作与得到的收益相比，应该也不算什么！^_^</p>
<ul>
<li>无法划分权限</li>
</ul>
<p>在上面的方案说明时也提到过，goproxy所在节点需要具备访问所有私有go module所在vcs repo的权限，但又无法对go开发者端做出有差别授权，这样只要是goproxy能拉取到的私有go module，go开发者都能拉取到。</p>
<p>不过对于多数小厂而言，内部所有源码原则上都是企业内部公开的，这个问题似乎也不大。如果觉得这是个问题，那么只能使用上面的大厂的第一个方案了。</p>
<h3>3. 小结</h3>
<p>无论大厂小厂，当对Go的使用逐渐深入后，接纳的人增多，开发的项目增多且越来越复杂后，拉取私有go module这样的问题肯定会摆到桌面上来。</p>
<p>对于大厂的gopher来说，这可能不是问题，甚至对他们都是透明的。但对于小厂等内部IT基础设施不完备的组织而言，的确需要自己动手解决。</p>
<p>这篇文章为小厂搭建Go私有库以及从私有库拉取私有go module提供了一个思路以及一个参考实现。</p>
<p>如果觉得上面的安装配置步骤有些繁琐，有兴趣深入的朋友可以将上述几个程序(goproxy, nginx, govanityurls)打到一个容器镜像中，实现一键安装设置。</p>
<hr />
<p><a href="https://mp.weixin.qq.com/s/jUqAL7hf2GmMun64BJufEA">“Gopher部落”知识星球</a>正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：</p>
<ul>
<li>Go技术书籍的书摘和读书体会系列</li>
<li>Go与eBPF系列</li>
</ul>
<p>欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/202103/gopher-tribe-zsxq-card.png" alt="" /></p>
<p>Go技术专栏“<a href="https://www.imooc.com/read/87">改善Go语⾔编程质量的50个有效实践</a>”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订<br />
阅！</p>
<p><img src="https://tonybai.com/wp-content/uploads/imooc-go-column-pgo-with-qr.jpg" 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/imooc-k8s-practice-with-qr.jpg" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。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; 2021, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2021/09/03/the-approach-to-go-get-private-go-module-in-house/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Go 1.16中值得关注的几个变化</title>
		<link>https://tonybai.com/2021/02/25/some-changes-in-go-1-16/</link>
		<comments>https://tonybai.com/2021/02/25/some-changes-in-go-1-16/#comments</comments>
		<pubDate>Thu, 25 Feb 2021 02:49:56 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[.NET]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[bitbucket]]></category>
		<category><![CDATA[Compiler]]></category>
		<category><![CDATA[embed]]></category>
		<category><![CDATA[generics]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-get]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[go.sum]]></category>
		<category><![CDATA[Go1]]></category>
		<category><![CDATA[go1.13]]></category>
		<category><![CDATA[go1.16]]></category>
		<category><![CDATA[go1兼容性]]></category>
		<category><![CDATA[GODEBUG]]></category>
		<category><![CDATA[godoc]]></category>
		<category><![CDATA[goinstall]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[GOPATH]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[GOVCS]]></category>
		<category><![CDATA[Go泛型]]></category>
		<category><![CDATA[hg]]></category>
		<category><![CDATA[inittrace]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[io/fs]]></category>
		<category><![CDATA[iOS]]></category>
		<category><![CDATA[linker]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[M1]]></category>
		<category><![CDATA[macos]]></category>
		<category><![CDATA[MADV_DONTNEED]]></category>
		<category><![CDATA[retract]]></category>
		<category><![CDATA[RobertGriesemer]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[RussCox]]></category>
		<category><![CDATA[Subversion]]></category>
		<category><![CDATA[svn]]></category>
		<category><![CDATA[vfs]]></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=3109</guid>
		<description><![CDATA[辛丑牛年初七开工大吉的日子(2021.2.18)，Go核心开发团队为中国Gopher们献上了大礼 &#8211; Go 1.16版本正式发布了！国内Gopher可以在Go中国官网上下载到Go 1.16在各个平台的安装包： 2020年双12，Go 1.16进入freeze状态，即不再接受新feature，仅fix bug、编写文档和接受安全更新等，那时我曾写过一篇名为《Go 1.16新功能特性不完全前瞻》的文章。当时Go 1.16的发布说明尚处于早期草稿阶段，要了解Go 1.16功能特性都有哪些变化，只能结合当时的release note以及从Go 1.16里程碑中的issue列表中挖掘。 如今Go 1.16版本正式发布了，和当时相比，Go 1.16又有哪些变化呢？在这篇文章中，我们就来一起详细分析一下Go 1.16中那些值得关注的重要变化！ 一. 语言规范 如果你是Go语言新手，想必你一定很期待一个大版本的发布会带来许多让人激动人心的语言特性。但是Go语言在这方面肯定会让你“失望”的。伴随着Go 1.0版本一起发布的Go1兼容性承诺给Go语言的规范加了一个“框框”，从Go 1.0到Go 1.15版本，Go语言对语言规范的变更屈指可数，因此资深Gopher在阅读Go版本的release notes时总是很自然的略过这一章节，因为这一章节通常都是如下面这样的描述： 这就是Go的设计哲学：简单！绝不轻易向语言中添加新语法元素增加语言的复杂性。除非是那些社区呼声很高并且是Go核心团队认可的。我们也可以将Go从1.0到Go 1.16这段时间称为“Go憋大招”的阶段，因为就在Go团队发布1.16版本之前不久，Go泛型提案正式被Go核心团队接受(Accepted)： 这意味着什么呢？这意味着在2022年2月份(Go 1.18)，Gopher们将迎来Go有史以来最大一次语言语法变更并且这种变更依然是符合Go1兼容性承诺的，这将避免Go社区出现Python3给Python社区带去的那种“割裂”。不过就像《“能力越大，责任越大” &#8211; Go语言之父详解将于Go 1.18发布的Go泛型》一文中Go语言之父Robert Griesemer所说的那样：泛型引入了抽象，但滥用抽象而没有解决实际问题将带来不必要的复杂性，请三思而后行! 离泛型的落地还有一年时间，就让我们耐心等待吧！ 二. Go对各平台/OS支持的变更 Go语言具有良好的可移植性，对各主流平台和OS的支持十分全面和及时，Go官博曾发布过一篇文章，简要列出了自Go1以来对各主流平台和OS的支持情况： Go1（2012年3月）支持原始系统(译注：上面提到的两种操作系统和三种架构)以及64位和32位x86上的FreeBSD、NetBSD和OpenBSD，以及32位x86上的Plan9。 Go 1.3（2014年6月）增加了对64位x86上Solaris的支持。 Go 1.4（2014年12月）增加了对32位ARM上Android和64位x86上Plan9的支持。 Go 1.5（2015年8月）增加了对64位ARM和64位PowerPC上的Linux以及32位和64位ARM上的iOS的支持。 Go 1.6（2016年2月）增加了对64位MIPS上的Linux，以及32位x86上的Android的支持。它还增加了32位ARM上的Linux官方二进制下载，主要用于RaspberryPi系统。 Go 1.7（2016年8月）增加了对的z系统（S390x）上Linux和32位x86上Plan9的支持。 Go 1.8（2017年2月）增加了对32位MIPS上Linux的支持，并且它增加了64位PowerPC和z系统上Linux的官方二进制下载。 Go 1.9（2017年8月）增加了对64位ARM上Linux的官方二进制下载。 Go 1.12（2018年2月）增加了对32位ARM上Windows10 IoT Core的支持，如RaspberryPi3。它还增加了对64位PowerPC上AIX的支持。 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/go-1.16-1.png" alt="img{512x368}" /></p>
<p>辛丑牛年初七开工大吉的日子(2021.2.18)，Go核心开发团队为中国Gopher们献上了大礼 &#8211; <a href="https://mp.weixin.qq.com/s/7tYi-61teL0kBmWz7q2SGw">Go 1.16版本正式发布了</a>！国内Gopher可以在<a href="https://golang.google.cn/dl/">Go中国官网上</a>下载到Go 1.16在各个平台的安装包：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-1.16-2.png" alt="img{512x368}" /></p>
<p>2020年双12，Go 1.16进入freeze状态，即不再接受新feature，仅fix bug、编写文档和接受安全更新等，那时我曾写过一篇名为<a href="https://mp.weixin.qq.com/s/JzAQ3r9lDBad8PO6iAerqw">《Go 1.16新功能特性不完全前瞻》</a>的文章。当时Go 1.16的<a href="https://tip.golang.org/doc/go1.16">发布说明</a>尚处于早期草稿阶段，要了解Go 1.16功能特性都有哪些变化，只能结合当时的release note以及从<a href="https://github.com/golang/go/issues?q=is%3Aissue+milestone%3AGo1.16+is%3Aclosed">Go 1.16里程碑</a>中的issue列表中挖掘。</p>
<p>如今Go 1.16版本正式发布了，和当时相比，Go 1.16又有哪些变化呢？在这篇文章中，我们就来一起详细分析一下Go 1.16中那些值得关注的重要变化！</p>
<h3>一. 语言规范</h3>
<p>如果你是Go语言新手，想必你一定很期待一个大版本的发布会带来许多让人激动人心的语言特性。但是Go语言在这方面肯定会让你“失望”的。伴随着Go 1.0版本一起发布的<a href="https://tip.golang.org/doc/go1compat">Go1兼容性承诺</a>给Go语言的规范加了一个“框框”，从Go 1.0到<a href="https://mp.weixin.qq.com/s/B5onfyP7BPYCh_rMSBtfcQ">Go 1.15</a>版本，Go语言对语言规范的变更屈指可数，因此资深Gopher在阅读Go版本的release notes时总是很自然的略过这一章节，因为这一章节通常都是如下面这样的描述：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-1.16-3.png" alt="img{512x368}" /></p>
<p>这就是<a href="https://www.imooc.com/read/87/article/2321">Go的设计哲学：简单</a>！绝不轻易向语言中添加新语法元素增加语言的复杂性。除非是那些社区呼声很高并且是Go核心团队认可的。我们也可以将Go从1.0到Go 1.16这段时间称为“Go憋大招”的阶段，因为就在Go团队发布1.16版本之前不久，<a href="https://mp.weixin.qq.com/s/Cnko3hrrcFKpsfdlj3yXdQ">Go泛型提案</a>正式被Go核心团队接受(Accepted)：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-1.16-4.png" alt="img{512x368}" /></p>
<p>这意味着什么呢？这意味着在2022年2月份(Go 1.18)，Gopher们将迎来Go有史以来最大一次语言语法变更并且<strong>这种变更依然是符合Go1兼容性承诺的</strong>，这将避免Go社区出现Python3给Python社区带去的那种“割裂”。不过就像<a href="https://mp.weixin.qq.com/s/SMT40557JgQ9FjUkswznlA">《“能力越大，责任越大” &#8211; Go语言之父详解将于Go 1.18发布的Go泛型》</a>一文中Go语言之父<a href="https://github.com/griesemer">Robert Griesemer</a>所说的那样：<strong>泛型引入了抽象，但滥用抽象而没有解决实际问题将带来不必要的复杂性，请三思而后行</strong>! 离泛型的落地还有一年时间，就让我们耐心等待吧！</p>
<h3>二. Go对各平台/OS支持的变更</h3>
<p><strong>Go语言具有良好的<a href="https://tonybai.com/2017/06/27/an-intro-about-go-portability">可移植性</a>，对各主流平台和OS的支持十分全面和及时</strong>，Go官博曾<a href="https://mp.weixin.qq.com/s/FQWMwZnT8xmCGe1Ing_luQ">发布过一篇文章</a>，简要列出了自Go1以来对各主流平台和OS的支持情况：</p>
<ul>
<li>Go1（2012年3月）支持原始系统(译注：上面提到的两种操作系统和三种架构)以及64位和32位x86上的FreeBSD、NetBSD和OpenBSD，以及32位x86上的Plan9。</li>
<li>Go 1.3（2014年6月）增加了对64位x86上Solaris的支持。</li>
<li><a href="https://tonybai.com/2014/11/04/some-changes-in-go-1-4/">Go 1.4</a>（2014年12月）增加了对32位ARM上Android和64位x86上Plan9的支持。</li>
<li><a href="https://tonybai.com/2015/07/10/some-changes-in-go-1-5/">Go 1.5</a>（2015年8月）增加了对64位ARM和64位PowerPC上的Linux以及32位和64位ARM上的iOS的支持。</li>
<li><a href="https://tonybai.com/2016/02/21/some-changes-in-go-1-6/">Go 1.6</a>（2016年2月）增加了对64位MIPS上的Linux，以及32位x86上的Android的支持。它还增加了32位ARM上的Linux官方二进制下载，主要用于RaspberryPi系统。</li>
<li><a href="https://tonybai.com/2016/06/21/some-changes-in-go-1-7/">Go 1.7</a>（2016年8月）增加了对的z系统（S390x）上Linux和32位x86上Plan9的支持。</li>
<li><a href="https://tonybai.com/2017/02/03/some-changes-in-go-1-8/">Go 1.8</a>（2017年2月）增加了对32位MIPS上Linux的支持，并且它增加了64位PowerPC和z系统上Linux的官方二进制下载。</li>
<li><a href="https://tonybai.com/2017/07/14/some-changes-in-go-1-9/">Go 1.9</a>（2017年8月）增加了对64位ARM上Linux的官方二进制下载。</li>
<li><a href="https://tonybai.com/2019/03/02/some-changes-in-go-1-12">Go 1.12</a>（2018年2月）增加了对32位ARM上Windows10 IoT Core的支持，如RaspberryPi3。它还增加了对64位PowerPC上AIX的支持。</li>
<li><a href="https://tonybai.com/2020/03/08/some-changes-in-go-1-14">Go 1.14</a>（2019年2月）增加了对64位RISC-V上Linux的支持。</li>
</ul>
<p><a href="https://tonybai.com/2016/06/21/some-changes-in-go-1-7/">Go 1.7版本</a>中新增的<strong>go tool dist list</strong>命令还可以帮助我们快速了解各个版本究竟支持哪些平台以及OS的组合。下面是Go 1.16版本该命令的输出：</p>
<pre><code>$go tool dist list
aix/ppc64
android/386
android/amd64
android/arm
android/arm64
darwin/amd64
darwin/arm64
dragonfly/amd64
freebsd/386
freebsd/amd64
freebsd/arm
freebsd/arm64
illumos/amd64
ios/amd64
ios/arm64
js/wasm
linux/386
linux/amd64
linux/arm
linux/arm64
linux/mips
linux/mips64
linux/mips64le
linux/mipsle
linux/ppc64
linux/ppc64le
linux/riscv64
linux/s390x
netbsd/386
netbsd/amd64
netbsd/arm
netbsd/arm64
openbsd/386
openbsd/amd64
openbsd/arm
openbsd/arm64
openbsd/mips64
plan9/386
plan9/amd64
plan9/arm
solaris/amd64
windows/386
windows/amd64
windows/arm
</code></pre>
<p>通常我不太会过多关注每次Go版本发布时关于可移植性方面的内容，这次将可移植性单独作为章节主要是因为Go 1.16发布之前的<strong>Apple M1芯片事件</strong>！</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-1.16-5.jpg" alt="img{512x368}" /></p>
<p>苹果公司再次放弃Intel x86芯片而改用自造的基于Arm64的M1芯片引发业界激烈争论。但现实是搭载Arm64 M1芯片的苹果笔记本已经大量上市，对于编程语言开发团队来说，能做的只有尽快支持这一平台。因此，Go团队给出了在Go 1.16版本中增加对Mac M1的原生支持。</p>
<p>在Go 1.16版本之前，Go也支持darwin/arm64的组合，但那更多是为了构建在iOS上运行的Go应用(利用<a href="https://github.com/golang/mobile">gomobile</a>)。</p>
<p>Go 1.16做了进一步的细分：将darwin/arm64组合改为apple M1专用；而构建在iOS上运行的Go应用则使用ios/arm64。同时，Go 1.16还增加了ios/amd64组合用于支持在MacOS(amd64)上运行的<a href="https://tip.golang.org/misc/ios/README">iOS模拟器中运行Go应用</a>。</p>
<p>另外还值得一提的是在OpenBSD上，Go应用的系统调用需要通过libc发起，而不能再绕过libc而直接使用汇编指令了，这是出于对未来OpenBSD的一些兼容性要求考虑才做出的决定。</p>
<h3>三. Go module-aware模式成为默认！</h3>
<p>在泛型落地前，Go module依旧是这些年Go语言改进的重点(虽不是语言规范特性)。在Go 1.16版本中，Go module-aware模式成为了默认模式(另一种则是传统的gopath模式)。module-aware模式成为默认意味着什么呢？意味着GO111MODULE的值默认为on了。</p>
<p>自从Go 1.11加入go module，不同go版本在GO111MODULE为不同值的情况下开启的构建模式几经变化，上一次go module-aware模式的行为有较大变更还是在<a href="https://mp.weixin.qq.com/s/Txqvanb17LYQYgohNiUHig">Go 1.13版本</a>中。这里将Go 1.13版本之前、Go 1.13版本以及Go 1.16版本在GO111MODULE为不同值的情况下的行为做一下对比，这样我们可以更好的理解go 1.16中module-aware模式下的行为特性，下面我们就来做一下比对：</p>
<table>
<thead>
<tr>
<th>GO111MODULE</th>
<th>&lt; Go 1.13</th>
<th>Go 1.13</th>
<th>Go 1.16</th>
</tr>
</thead>
<tbody>
<tr>
<td>on</td>
<td>任何路径下都开启module-aware模式</td>
<td>任何路径下都开启module-aware模式</td>
<td>【默认值】：任何路径下都开启module-aware模式</td>
</tr>
<tr>
<td>auto</td>
<td>【默认值】：使用GOPATH mode还是module-aware mode，取决于要构建的源码目录所在位置以及是否包含go.mod文件。如果要构建的源码目录不在以GOPATH/src为根的目录体系下，且包含go.mod文件(两个条件缺一不可)，那么使用module-aware mode；否则使用传统的GOPATH mode。</td>
<td>【默认值】：只要当前目录或父目录下有go.mod文件时，就开启module-aware模式，无论源码目录是否在GOPATH外面</td>
<td>只有当前目录或父目录下有go.mod文件时，就开启module-aware模式，无论源码目录是否在GOPATH外面</td>
</tr>
<tr>
<td>off</td>
<td>gopath模式</td>
<td>gopath模式</td>
<td>gopath模式</td>
</tr>
</tbody>
</table>
<p>我们看到在Go 1.16模式下，依然可以回归到gopath模式。但Go核心团队已经决定拒绝<a href="https://github.com/golang/go/issues/37755">“继续保留GOPATH mode”的提案</a>，并计划在Go 1.17版本中彻底取消gopath mode，仅保留go module-aware mode：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-1.16-6.png" alt="img{512x368}" /></p>
<p>虽然目前仍有项目没有转换到go module下，但根据调查，大多数项目已经选择拥抱go module并完成了转换工作，因此笔者认为即便Go 1.17真的取消了GOPATH mode，对整个Go社区的影响也不会太大了。</p>
<p>Go 1.16中，go module机制还有其他几个变化，这里逐一来看一下：</p>
<h4>1. go build/run命令不再自动更新go.mod和go.sum了</h4>
<p>为了能更清晰看出Go 1.16与之前版本的差异，我们准备了一个小程序：</p>
<pre><code>// github.com/bigwhite/experiments/blob/master/go1.16-examples/go-modules/helloworld/go.mod
module github.com/bigwhite/helloworld

go 1.16

// github.com/bigwhite/experiments/blob/master/go1.16-examples/go-modules/helloworld/helloworld.go
package main

import "github.com/sirupsen/logrus"

func main() {
    logrus.Println("Hello, World")
}
</code></pre>
<p>我们使用<a href="https://mp.weixin.qq.com/s/B5onfyP7BPYCh_rMSBtfcQ">go 1.15版本</a>构建一下该程序：</p>
<pre><code>$go build
go: finding module for package github.com/sirupsen/logrus
go: downloading github.com/sirupsen/logrus v1.8.0
go: found github.com/sirupsen/logrus in github.com/sirupsen/logrus v1.8.0

$cat go.mod
module github.com/bigwhite/helloworld

go 1.16

require github.com/sirupsen/logrus v1.8.0

$cat go.sum
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.8.0 h1:nfhvjKcUMhBMVqbKHJlk5RPrrfYr/NMo3692g0dwfWU=
github.com/sirupsen/logrus v1.8.0/go.mod h1:4GuYW9TZmE769R5STWrRakJc4UqQ3+QQ95fyz7ENv1A=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
</code></pre>
<p>在Go 1.15版本中，go build会自动分析源码中的依赖，如果go.mod中没有对该依赖的require，则会自动添加require，同时会将go.sum中将相关包(特定版本)的校验信息写入。</p>
<p>我们将上述helloworld恢复到初始状态，再用go 1.16来build一次：</p>
<pre><code>$go build
helloworld.go:3:8: no required module provides package github.com/sirupsen/logrus; to add it:
    go get github.com/sirupsen/logrus
</code></pre>
<p>我们看到go build没有成功，而是给出错误：go.mod中没有对logrus的require，并给出添加对logrus的require的方法(go get github.com/sirupsen/logrus)。</p>
<p>我们就按照go build给出的提示执行go get：</p>
<pre><code>$go get github.com/sirupsen/logrus
go: downloading github.com/magefile/mage v1.10.0
go get: added github.com/sirupsen/logrus v1.8.0

$cat go.mod
module github.com/bigwhite/helloworld

go 1.16

require github.com/sirupsen/logrus v1.8.0 // indirect

$cat go.sum
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/magefile/mage v1.10.0 h1:3HiXzCUY12kh9bIuyXShaVe529fJfyqoVM42o/uom2g=
github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.8.0 h1:nfhvjKcUMhBMVqbKHJlk5RPrrfYr/NMo3692g0dwfWU=
github.com/sirupsen/logrus v1.8.0/go.mod h1:4GuYW9TZmE769R5STWrRakJc4UqQ3+QQ95fyz7ENv1A=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

$go build
//ok
</code></pre>
<p>我们看到go build并不会向go 1.15及之前版本那样做出有“副作用”的动作：自动修改go.mod和go.sum，而是提示开发人员显式通过go get来添加缺少的包/module，即便是<a href="https://tonybai.com/2019/06/03/the-practice-of-upgrading-major-version-under-go-module/">依赖包major版本升级</a>亦是如此。</p>
<p>从自动更新go.mod，到通过提供-mod=readonly选项来避免自动更新go.mod，再到Go 1.16的禁止自动更新go.mod，笔者认为这个变化是Go不喜“隐式转型”的一种延续，即尽量不支持任何可能让开发者产生疑惑或surprise的隐式行为（就像隐式转型），取而代之的是要用一种显式的方式去完成(就像必须显式转型那样)。</p>
<p>我们也看到在go 1.16中，添加或更新go.mod中的依赖，只有显式使用go get。go mod tidy依旧会执行对go.mod的清理，即也可以修改go.mod。</p>
<h4>2. 推荐使用go install安装Go可执行文件</h4>
<p>在gopath mode下，go install基本“隐身”了，它能做的事情基本都被go get“越俎代庖”了。在go module时代初期，go install更是没有了地位。但Go团队现在想逐步恢复go install的角色：安装Go可执行文件！在Go 1.16中，当go install后面的包携带特定版本号时，go install将忽略当前go.mod中的依赖信息而直接编译安装可执行文件：</p>
<pre><code>// go install回将gopls v0.6.5安装到GOBIN下
$go install golang.org/x/tools/gopls@v0.6.5
</code></pre>
<p>并且后续，Go团队会让go get将专注于分析依赖，并获取go包/module，更新go.mod/go.sum，而<a href="https://github.com/golang/go/issues/43684">不再具有安装可执行Go程序的行为能力</a>，这样go get和go install就会各司其职，Gopher们也不会再被两者的重叠行为所迷惑了。现在如果不想go get编译安装，可使用go get -d。</p>
<h4>3. 作废module的特定版本</h4>
<p>在<a href="https://mp.weixin.qq.com/s/fg84g4OzSgoHDGbvVDbMKg">《如何作废一个已发布的Go module版本，我来告诉你！》</a>一文中，我曾详细探讨了Go引入module后如何作废一个已发布的go module版本。当时已经知晓Go 1.16会在go.mod中增加<a href="https://github.com/golang/mod/commit/c0d644d00ab849f4506f17a98a5740bf0feff020">retract指示符</a>，因此也给出了在Go 1.16下retract一个module版本的原理和例子(基于当时的go tip)。</p>
<p>Go 1.16正式版在工具的输出提示方面做了进一步的优化，让开发人员体验更为友好。我们还是以一个简单的例子来看看在Go 1.16中作废一个module版本的过程吧。</p>
<p>在我的bitbucket账户下有一个名为m2的Go module(https://bitbucket.org/bigwhite/m2/)，当前它的版本为v1.0.0：</p>
<pre><code>// bitbucket.org/bigwhite/m2
$cat go.mod
module bitbucket.org/bigwhite/m2

go 1.15

$cat m2.go
package m2

import "fmt"

func M2() {
    fmt.Println("This is m2.M2 - v1.0.0")
}
</code></pre>
<p>我们在本地建立一个m2的消费者：</p>
<pre><code>// github.com/bigwhite/experiments/blob/master/go1.16-examples/go-modules/retract

$cat go.mod
module github.com/bigwhite/retractdemo

go 1.16

$cat main.go
package main

import "bitbucket.org/bigwhite/m2"

func main() {
    m2.M2()
}
</code></pre>
<p>运行这个消费者：</p>
<pre><code>$go run main.go
main.go:3:8: no required module provides package bitbucket.org/bigwhite/m2; to add it:
    go get bitbucket.org/bigwhite/m2
</code></pre>
<p>由于上面提到的原因，go run不会隐式修改go.mod，因此我们需要手工go get m2：</p>
<pre><code>$go get bitbucket.org/bigwhite/m2
go: downloading bitbucket.org/bigwhite/m2 v1.0.0
go get: added bitbucket.org/bigwhite/m2 v1.0.0
</code></pre>
<p>再来运行消费者，我们将看到以下运行成功的结果：</p>
<pre><code>$go run main.go
This is m2.M2 - v1.0.0
</code></pre>
<p>现在m2的作者对m2打了小补丁，版本升级到了v1.0.1。这时消费者通过go list命令可以看到m2的最新版本(前提：go proxy server上已经cache了最新的v1.0.1)：</p>
<pre><code>$go list -m -u all
github.com/bigwhite/retractdemo
bitbucket.org/bigwhite/m2 v1.0.0 [v1.0.1]
</code></pre>
<p>消费者可以通过go get将对m2的依赖升级到最新的v1.0.1：</p>
<pre><code>$go get bitbucket.org/bigwhite/m2@v1.0.1

go get: upgraded bitbucket.org/bigwhite/m2 v1.0.0 =&gt; v1.0.1
$go run main.go
This is m2.M2 - v1.0.1
</code></pre>
<p>m2作者收到issue，有人指出v1.0.1版本有安全漏洞，m2作者确认了该漏洞，但此时v1.0.1版已经发布并被缓存到各大go proxy server上，已经无法撤回。m2作者便想到了Go 1.16中引入的retract指示符，于是它在m2的go.mod用retract指示符做了如下更新：</p>
<pre><code>$cat go.mod
module bitbucket.org/bigwhite/m2

// 存在安全漏洞
retract v1.0.1

go 1.15
</code></pre>
<p>并将此次更新作为v1.0.2发布了出去！</p>
<p>之后，当消费者使用go list查看m2是否有最新更新时，便会看到retract提示：(前提：go proxy server上已经cache了最新的v1.0.2)</p>
<pre><code>$go list -m -u all
github.com/bigwhite/retractdemo
bitbucket.org/bigwhite/m2 v1.0.1 (retracted) [v1.0.2]
</code></pre>
<p>执行go get会收到带有更详尽信息的retract提示和问题解决建议：</p>
<pre><code>$go get .
go: warning: bitbucket.org/bigwhite/m2@v1.0.1: retracted by module author: 存在安全漏洞
go: to switch to the latest unretracted version, run:
    go get bitbucket.org/bigwhite/m2@latest
</code></pre>
<p>于是消费者按照提示执行go get bitbucket.org/bigwhite/m2@latest：</p>
<pre><code>$go get bitbucket.org/bigwhite/m2@latest
go get: upgraded bitbucket.org/bigwhite/m2 v1.0.1 =&gt; v1.0.2

$cat go.mod
module github.com/bigwhite/retractdemo

go 1.16

require bitbucket.org/bigwhite/m2 v1.0.2

$go run main.go
This is m2.M2 - v1.0.2
</code></pre>
<p>到此，retract的使命终于完成了！</p>
<h4>4. 引入GOVCS环境变量，控制module源码获取所使用的版本控制工具</h4>
<p>出于安全考虑，Go 1.16引入GOVCS环境变量，用于在go命令直接从代码托管站点获取源码时对所使用的版本控制工具进行约束，如果是从go proxy server获取源码，那么GOVCS将不起作用，因为go工具与go proxy server之间使用的是<a href="https://tip.golang.org/ref/mod#goproxy-protocol">GOPROXY协议</a>。</p>
<p>GOVCS的默认值为public:git|hg,private:all，即对所有公共module允许采用git或hg获取源码，而对私有module则不限制版本控制工具的使用。</p>
<p>如果要允许使用所有工具，可像下面这样设置GOVCS：</p>
<pre><code>GOVCS=*:all
</code></pre>
<p>如果要禁止使用任何版本控制工具去直接获取源码（不通过go proxy），那么可以像下面这样设置GOVCS:</p>
<pre><code>GOVCS=*:off
</code></pre>
<h4>5. 有关go module的文档更新</h4>
<p>自打<a href="https://mp.weixin.qq.com/s/PVxdtvSXgNpiD65TUo-TCg">Go 1.14版本</a>宣布go module生产可用后，Go核心团队在说服和帮助Go社区全面拥抱go module的方面不可谓不努力。在文档方面亦是如此，最初有关go module的文档仅局限于go build命令相关以及<a href="https://github.com/golang/go/wiki/Modules">有关go module的wiki</a>。随着go module日益成熟，go.mod格式的日益稳定，Go团队在1.16版本中还将go module相关文档升级到go reference的层次，与go language ref等并列：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-1.16-7.png" alt="img{512x368}" /></p>
<p>我们看到有关go module的ref文档包括：</p>
<ul>
<li><a href="https://tip.golang.org/ref/mod">Go Modules Reference</a> https://tip.golang.org/ref/mod</li>
<li><a href="https://tip.golang.org/doc/modules/gomod-ref">go.mod file reference</a> https://tip.golang.org/doc/modules/gomod-ref</li>
</ul>
<p>官方还编写了<a href="https://tip.golang.org/doc/#developing-modules">详细的Go module日常开发时的使用方法</a>，包括：开发与发布module、module发布与版本管理工作流、升级major号等。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-1.16-8.png" alt="img{512x368}" /></p>
<p><strong>建议每个gopher都要将这些文档仔细阅读一遍，以更为深入了解和使用go module</strong>。</p>
<h3>四. 编译器与运行时</h3>
<h4>1. runtime/metrics包</h4>
<p>在<a href="https://mp.weixin.qq.com/s/JzAQ3r9lDBad8PO6iAerqw">《Go 1.16新功能特性不完全前瞻》</a>一文中，我们提到过：Go 1.16 新增了runtime/metrics包，以替代runtime.ReadMemStats和debug.ReadGCStats输出runtime的各种度量数据，这个包更通用稳定，性能也更好。限于篇幅这里不展开，后续可能会以单独的文章讲解这个新包。</p>
<h4>2. GODEBUG环境变量支持跟踪包init函数的消耗</h4>
<p>GODEBUG=inittrace=1这个特性也保留在了Go 1.16正式版当中了。当GODEBUG环境变量包含inittrace=1时，Go运行时将会报告各个源代码文件中的init函数的执行时间和内存开辟消耗情况。我们用上面的helloworld示例(github.com/bigwhite/experiments/blob/master/go1.16-examples/go-modules/helloworld)来看看该特性的效果：</p>
<pre><code>$go build
$GODEBUG=inittrace=1 ./helloworld
init internal/bytealg @0.006 ms, 0 ms clock, 0 bytes, 0 allocs
init runtime @0.037 ms, 0.031 ms clock, 0 bytes, 0 allocs
init errors @0.29 ms, 0.005 ms clock, 0 bytes, 0 allocs
init math @0.31 ms, 0 ms clock, 0 bytes, 0 allocs
init strconv @0.33 ms, 0.002 ms clock, 32 bytes, 2 allocs
init sync @0.35 ms, 0.003 ms clock, 16 bytes, 1 allocs
init unicode @0.37 ms, 0.10 ms clock, 24568 bytes, 30 allocs
init reflect @0.49 ms, 0.002 ms clock, 0 bytes, 0 allocs
init io @0.51 ms, 0.003 ms clock, 144 bytes, 9 allocs
init internal/oserror @0.53 ms, 0 ms clock, 80 bytes, 5 allocs
init syscall @0.55 ms, 0.010 ms clock, 752 bytes, 2 allocs
init time @0.58 ms, 0.010 ms clock, 384 bytes, 8 allocs
init path @0.60 ms, 0 ms clock, 16 bytes, 1 allocs
init io/fs @0.62 ms, 0.002 ms clock, 16 bytes, 1 allocs
init internal/poll @0.63 ms, 0.001 ms clock, 64 bytes, 4 allocs
init os @0.65 ms, 0.089 ms clock, 4472 bytes, 20 allocs
init fmt @0.77 ms, 0.006 ms clock, 32 bytes, 2 allocs
init bytes @0.84 ms, 0.004 ms clock, 48 bytes, 3 allocs
init context @0.87 ms, 0 ms clock, 128 bytes, 4 allocs
init encoding/binary @0.89 ms, 0.002 ms clock, 16 bytes, 1 allocs
init encoding/base64 @0.90 ms, 0.015 ms clock, 1408 bytes, 4 allocs
init encoding/json @0.93 ms, 0.002 ms clock, 32 bytes, 2 allocs
init log @0.95 ms, 0 ms clock, 80 bytes, 1 allocs
init golang.org/x/sys/unix @0.96 ms, 0.002 ms clock, 48 bytes, 1 allocs
init bufio @0.98 ms, 0 ms clock, 176 bytes, 11 allocs
init github.com/sirupsen/logrus @0.99 ms, 0.009 ms clock, 312 bytes, 5 allocs
INFO[0000] Hello, World
</code></pre>
<p>以下面这行为例：</p>
<pre><code>init fmt @0.77 ms, 0.006 ms clock, 32 bytes, 2 allocs
</code></pre>
<ul>
<li>0.77ms表示的是自从程序启动后到fmt包init执行所过去的时间(以ms为单位)</li>
<li>0.006 ms clock表示fmt包init函数执行的时间(以ms为单位)</li>
<li>312 bytes表示fmt包init函数在heap上分配的内存大小；</li>
<li>5 allocs表示的是fmt包init函数在heap上执行内存分配操作的次数。</li>
</ul>
<h4>3. Go runtime默认使用MADV_DONTNEED</h4>
<p>Go 1.15版本时，我们可以通过GODEBUG=madvdontneed=1让Go runtime使用MADV_DONTNEED替代MADV_FREE达到更积极的将不用的内存释放给OS的效果(如果使用MADV_FREE，只有OS内存压力很大时，才会真正回收内存)，这将使得通过top查看到的常驻系统内存(RSS或RES)指标更实时也更真实反映当前Go进程对os内存的实际占用情况(仅使用linux)。</p>
<p>在Go 1.16版本中，Go runtime将MADV_DONTNEED作为默认值了，我们可以用一个小例子来对比一下这种变化：</p>
<pre><code>// github.com/bigwhite/experiments/blob/master/go1.16-examples/runtime/memalloc.go
package main

import "time"

func allocMem() []byte {
    b := make([]byte, 1024*1024*1) //1M
    return b
}

func main() {
    for i := 0; i &lt; 100000; i++ {
        _ = allocMem()
        time.Sleep(500 * time.Millisecond)
    }
}
</code></pre>
<p>我们在linux上使用go 1.16版本编译该程序，考虑到优化和inline的作用，我们在编译时关闭优化和内联：</p>
<pre><code>$go build -gcflags "-l -N" memalloc.go
</code></pre>
<p>接下来，我们分两次运行该程序，并使用top监控其RES指标值：</p>
<pre><code>$./memalloc
$ top -p 9273
  PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND
 9273 root      20   0  704264   5840    856 S  0.0  0.3   0:00.03 memalloc
 9273 root      20   0  704264   3728    856 S  0.0  0.2   0:00.05 memalloc
 ... ...

$GODEBUG=madvdontneed=0 ./memalloc
$ top -p 9415

  PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND
 9415 root      20   0  704264   5624    856 S  0.0  0.3   0:00.03 memalloc
 9415 root      20   0  704264   5624    856 S  0.0  0.3   0:00.05 memalloc
</code></pre>
<p>我们看到默认运行的memalloc(开启MADV_DONTNEED)，RES很积极的变化，当上一次显示5840，下一秒内存就被归还给OS，RES变为3728。而关闭MADV_DONTNEED（GODEBUG=madvdontneed=0）的memalloc，OS就会很lazy的回收内存，RES一直显示5624这个值。</p>
<h4>4. Go链接器的进一步进行<a href="https://golang.org/s/better-linker">现代化改造</a></h4>
<p>新一代Go链接器的更新计划从Go 1.15版本开始，在Go 1.15版本链接器的性能、资源占用、最终二进制文件大小等方面都有了一定幅度的优化提升。Go 1.16版本延续了这一势头：相比于Go 1.15，官方宣称(在linux上)性能有20%-25%的提升，资源占用下降5%-15%。更为直观的是编译出的二进制文件的size，我实测了一下文件大小下降10%以上：</p>
<pre><code>-rwxr-xr-x   1 tonybai  staff    22M  2 21 23:03 my-large-app-demo*
-rwxr-xr-x   1 tonybai  staff    25M  2 21 23:02 my-large-app-demo-go1.15*
</code></pre>
<p>并且和Go 1.15的链接器优化仅针对amd64平台和基于ELF格式的OS不同，<strong>这次的链接器优化已经扩展到所有平台和os组合上</strong>。</p>
<h3>五. 标准库</h3>
<h4>1. io/fs包</h4>
<p>Go 1.16标准库新增io/fs包，并定义了一个fs.File接口用于表示一个只读文件树(tree of file)的抽象。之所以要<a href="https://github.com/golang/go/issues/41190">加入io/fs包并新增fs.File接口</a>源于对<a href="https://github.com/golang/go/issues/41191">嵌入静态资源文件(embed static asset)的实现需求</a>。虽说实现embed功能特性是直接原因，但io/fs的加入也不是“临时起意”，早在很多年前的godoc实现时，对一个抽象的文件系统接口的需求就已经被提了出来并给出了实现：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-1.16-9.png" alt="" /></p>
<p>最终这份实现以godoc工具的<a href="https://github.com/golang/tools/tree/master/godoc/vfs">vfs包</a>的形式一直长期存在着。虽然它的实现有些复杂，抽象程度不够，但却对<a href="https://github.com/golang/proposal/blob/master/design/draft-iofs.md">io/fs包的设计</a>有着重要的参考价值。同时也部分弥补了<a href="https://github.com/golang/go/issues/14106">Rob Pike老爷子当年没有将os.File设计为interface的遗憾</a>，<a href="https://github.com/golang/go/issues/5636">Ian Lance Taylor 2013年提出的增加VFS层的想法</a>也一并得以实现。</p>
<p>io/fs包的两个最重要的接口如下：</p>
<pre><code>// $GOROOT/src/io/fs/fs.go

// An FS provides access to a hierarchical file system.
//
// The FS interface is the minimum implementation required of the file system.
// A file system may implement additional interfaces,
// such as ReadFileFS, to provide additional or optimized functionality.
type FS interface {
        // Open opens the named file.
        //
        // When Open returns an error, it should be of type *PathError
        // with the Op field set to "open", the Path field set to name,
        // and the Err field describing the problem.
        //
        // Open should reject attempts to open names that do not satisfy
        // ValidPath(name), returning a *PathError with Err set to
        // ErrInvalid or ErrNotExist.
        Open(name string) (File, error)
}

// A File provides access to a single file.
// The File interface is the minimum implementation required of the file.
// A file may implement additional interfaces, such as
// ReadDirFile, ReaderAt, or Seeker, to provide additional or optimized functionality.
type File interface {
        Stat() (FileInfo, error)
        Read([]byte) (int, error)
        Close() error
}
</code></pre>
<p>FS接口代表虚拟文件系统的最小抽象，File接口则是虚拟文件的最小抽象，我们可以基于这两个接口进行扩展以及对接现有的一些实现。io/fs包也给出了一些扩展FS的“样例”：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-1.16-10.png" alt="" /></p>
<p>这两个接口的设计也是“Go秉持定义小接口惯例”的延续(更多关于这方面的内容，可以参考我的专栏文章<a href="https://www.imooc.com/read/87/article/2425">《定义小接口是Go惯例》</a>)。</p>
<p>io/fs包的加入也契合了Go社区对vfs的需求，在Go团队决定加入io/fs并提交实现后，社区做出了积极的反应，在github上我们能看到好多为各类对象提供针对io/fs.FS接口实现的项目：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-1.16-11.png" alt="" /></p>
<p>io/fs.FS和File接口在后续Go演进过程中会像io.Writer和io.Reader一样成为Gopher们在操作类文件树时最爱的接口。</p>
<h4>2. embed包</h4>
<p>在<a href="https://mp.weixin.qq.com/s/JzAQ3r9lDBad8PO6iAerqw">《Go 1.16新功能特性不完全前瞻》</a>一文中我们曾重点说了Go 1.16将支持在Go二进制文件中嵌入静态文件并给出了一个在webserver中嵌入文本文件的例子：</p>
<pre><code>// github.com/bigwhite/experiments/blob/master/go1.16-examples/stdlib/embed/webserver/hello.txt
hello, go 1.16

// github.com/bigwhite/experiments/blob/master/go1.16-examples/stdlib/embed/webserver/main.go
package main

import (
         _  "embed"
    "net/http"
)

//go:embed hello.txt
var s string

func main() {
    http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte(s))
    }))
    http.ListenAndServe(":8080", nil)
}
</code></pre>
<p>我们看到在这个例子，通过//go:embed hello.txt，我们可以轻易地将hello.txt的内容存储在包级变量s中，而s将作为每个http request的应答返回给客户端。</p>
<p><a href="https://github.com/golang/go/issues/41191">在Go二进制文件中嵌入静态资源文件</a>是Go核心团队对社区广泛需求的积极回应。在go 1.16以前，Go社区开源的类嵌入静态文件的项目不下十多个，在Russ Cox<a href="https://github.com/golang/proposal/blob/master/design/draft-embed.md">关于embed的设计草案</a>中，他就列了十多个：</p>
<ul>
<li>github.com/jteeuwen/go-bindata(主流实现)</li>
<li>github.com/alecthomas/gobundle</li>
<li>github.com/GeertJohan/go.rice</li>
<li>github.com/go-playground/statics</li>
<li>github.com/gobuffalo/packr</li>
<li>github.com/knadh/stuffbin</li>
<li>github.com/mjibson/esc</li>
<li>github.com/omeid/go-resources</li>
<li>github.com/phogolabs/parcello</li>
<li>github.com/pyros2097/go-embed</li>
<li>github.com/rakyll/statik</li>
<li>github.com/shurcooL/vfsgen</li>
<li>github.com/UnnoTed/fileb0x</li>
<li>github.com/wlbr/templify</li>
<li>perkeep.org/pkg/fileembed</li>
</ul>
<p>Go1.16原生支持嵌入并且给出一种开发者体验良好的实现方案，这对Go社区是一种极大的鼓励，也是Go团队重视社区声音的重要表现。</p>
<p>笔者认为embed机制是Go 1.16中玩法最多的一种机制，也是极具新玩法挖掘潜力的机制。在embed加入Go tip不久，很多Gopher就已经“脑洞大开”：</p>
<p>有通过embed嵌入版本号的：</p>
<pre><code>// github.com/bigwhite/experiments/blob/master/go1.16-examples/stdlib/embed/version/main.go
package main

import (
    _ "embed"
    "fmt"
    "strings"
)

var (
    Version string = strings.TrimSpace(version)
    //go:embed version.txt
    version string
)

func main() {
    fmt.Printf("Version %q\n", Version)
}

// github.com/bigwhite/experiments/blob/master/go1.16-examples/stdlib/embed/version/version.txt
v1.0.1
</code></pre>
<p>有通过embed打印自身源码的：</p>
<pre><code>// github.com/bigwhite/experiments/blob/master/go1.16-examples/stdlib/embed/printself/main.go
package main

import (
        _ "embed"
        "fmt"
)

//go:embed main.go
var src string

func main() {
        fmt.Print(src)
}
</code></pre>
<p>更是有<a href="https://blog.lawrencejones.dev/golang-embed/">将一个完整的、复杂的带有js支持的web站点直接嵌入到go二进制文件中的示例</a>，鉴于篇幅，这里就不一一列举了。</p>
<p>Go擅长于Web服务，而embed机制的引入粗略来看，可以大大简化web服务中资源文件的部署，估计这也是之前社区青睐各种静态资源文件嵌入项目的原因。embed估计也会成为Go 1.16中最被gopher们喜爱的功能特性。</p>
<p>不过embed机制的实现目前有如下一些局限：</p>
<ul>
<li>仅支持在包级变量前使用//go:embed指示符，还不支持在函数/方法内的局部变量上应用embed指示符（当然我们可以通过将包级变量赋值给局部变量来过渡一下）；</li>
<li>使用//go:embed指示符的包必须以空导入的方式导入embed包，二者是成对出现的，缺一不可；</li>
</ul>
<h4>3. net包的变化</h4>
<p>在Go 1.16之前，我们检测在一个已关闭的网络上进行I/O操作或在I/O完成前网络被关闭的情况，只能通过匹配字符串”use of closed network connection”的方式来进行。之前的版本没有针对这个错误定义“哨兵错误变量”(更多关于哨兵错误变量的内容，可以参考我的专栏文章<a href="https://www.imooc.com/read/87/article/2433">《别笑！这就是 Go 的错误处理哲学》</a>)，Go 1.16增加了ErrClosed这个“哨兵错误变量”，我们可以通过errors.Is(err, net.ErrClosed)来检测是否是上述错误情况。</p>
<h3>六. 小结</h3>
<p>从Go 1.16版本变更的功能特性中，我看到了Go团队更加重视社区的声音，这也是Go团队一直持续努力的目标。在最新的Go proposal review meeting的结论中，我们还看到了这样的一个<a href="https://github.com/golang/go/issues/43931">proposal</a>被accept：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-1.16-12.png" alt="" /></p>
<p>要知道这个proposal的提议是将在Go 1.18才会落地的泛型实现分支merge到Go项目master分支，也就是说在Go 1.17中就会包含“不会发布的”泛型部分实现，这在之前是不可能实现的(之前，新proposal必须有原型实现的分支，实现并经过社区测试与Go核心委员会评估后才会在特定版本merge到master分支)。虽说泛型的开发有其特殊情况，但能被accept，这恰证明了Go社区的声音在Go核心团队日益受到重视。</p>
<p><strong>如果你还没有升级到Go 1.16，那么现在正是时候</strong>。</p>
<p>本文中涉及的代码可以在<a href="https://github.com/bigwhite/experiments/tree/master/go1.16-examples">这里</a>下载。https://github.com/bigwhite/experiments/tree/master/go1.16-examples</p>
<hr />
<p><a href="https://mp.weixin.qq.com/s/jUqAL7hf2GmMun64BJufEA">“Gopher部落”知识星球</a>正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：</p>
<ul>
<li>Go技术书籍的书摘和读书体会系列</li>
<li>Go与eBPF系列</li>
</ul>
<p>考虑到部落尚处于推广期，这里仍然为大家准备了新人优惠券，虽然优惠幅度有所下降，但依然物超所值，早到早享哦！</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条有效实践建议，上线后收到一致好评！欢迎大家订阅！目前该技术专栏正在新春促销！关注我的个人公众号“iamtonybai”，发送“go专栏活动”即可获取专栏专属优惠码，可在订阅专栏时抵扣20元哦(2021.2月末前有效)。</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/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。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; 2021, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2021/02/25/some-changes-in-go-1-16/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>没有VPS搭建govanityurls服务？别急！你依然可以自定义Go包导入路径</title>
		<link>https://tonybai.com/2020/11/15/another-approach-to-customize-package-import-path/</link>
		<comments>https://tonybai.com/2020/11/15/another-approach-to-customize-package-import-path/#comments</comments>
		<pubDate>Sun, 15 Nov 2020 10:42:21 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[github-pages]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-get]]></category>
		<category><![CDATA[go-module]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[golang.org]]></category>
		<category><![CDATA[GOPATH]]></category>
		<category><![CDATA[govanityurls]]></category>
		<category><![CDATA[hg]]></category>
		<category><![CDATA[import]]></category>
		<category><![CDATA[letsencrypt]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[svn]]></category>
		<category><![CDATA[vanity-urls]]></category>
		<category><![CDATA[vps]]></category>
		<category><![CDATA[包导入路径]]></category>
		<category><![CDATA[权威路径]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=2992</guid>
		<description><![CDATA[我们见到的Go包的导入路径常常以github.com、bitbucket.org等代码托管站点的域名为前缀，这样的包导入路径有一个问题，那就是当Go包的托管站点发生变更时(比如从github.om迁移到bitbucket.org或gitlab.com)，该包的使用者需要更新包的导入路径。当然，在支持go module+GOPROXY的情况下，如果使用者不再升级包版本，他/她完全可以继续使用原包导入路径，但这仅是特例。 还有一些包的导入路径并非以知名代码托管站点域名作为前缀，比如：Go官方扩展包text，它的包导入路径是golang.org/x/text，这种包导入路径被称为vanity import path，字面义是虚荣心导入路径，即以个人或组织官方域名作为前缀的包导入路径。采用vanity import path的包避免了包迁移对包使用者的影响。包使用者完全无需关心包的实际存储位置是在github上还是在bitbucket上或是私有服务器上。同时将vanity import path作为包的权威路径(canonical import path)，也方便go get等对包权威路径的检查，避免包路径变更的前后不一致。 之前笔者曾经写过两篇文章介绍了利用govanityurls这个工具实现自定义包导入路径的方法。不过这种方法有一个约束条件，那就是你需要有一台VPS主机来部署运行govanityurls。虽然现在的云主机很便宜，但是购买和自建毕竟还是要付出一定成本的。如果没有VPS搭建govanityurls服务，那我们是否还有其他方法来自定义Go包导入路径呢？答案当然是有。 根据Go官方关于go get命令的文档，当go get从非知名托管站点(github, bitbucket等之外的站点)获取go包时，会尝试在返回的http/https应答head标签中查找是否有如下形式meta标签： &#60;meta name="go-import" content="import-prefix vcs repo-root"&#62; meta标签中的name值是固定的”go-import”，import-prefix即包vanity import path，比如：go.tonybai.com/gocmpp；vcs是采用的版本控制工具，git、svn或hg等；repo-root是包代码的实际存储服务器url。 下面是一个实际例子： &#60;meta name="go-import" content="go.tonybai.com/gocmpp git https://github.com/bigwhite/gocmpp"&#62; 对于这样的标签，go get会做进一步匹配(可参见GOROOT/src/cmd/go/internal/get/vcs.go中的matchGoImport函数实现)，看content值中的import-prefix是否是go get所需要的包的导入路径。如果是，则会向真正存储包代码的服务器再次发起代码获取请求(比如：git clone等）。 你可能会说：我用一个静态站点服务也能返回这样的应答。没错！但搭建静态站点一般还是需要VPS，这里我们介绍一种无须VPS的方法：利用github pages。 下面是利用github pages实现自定义Go包导入路径的原理图： 图：利用github pages实现自定义Go包导入路径 下面我们就以go.tonybai.com/gocmpp这个包导入路径的定制步骤来说明一下上述原理。 首先，我们要给tonybai.com这个域名添加一个子域名：go.tonybai.com作为我个人生产的所有Go包的导入路径前缀。我在DNS设置中为go.tonybai.com指定一个CNAME值：go.tonybai.com.github.io。这样当访问go.tonybai.com时，实际上是向go.tonybai.com.github.io发起请求。当然此刻如果你向go.tonybai.com发起请求时，你必然会得到404错误，因为github尚未建立起go.tonybai.com.github.io这个站点。 接下来，我们就来建立go.tonybai.com.github.io这个基于github pages的静态站点。我创建一个新的代码仓库：github.com/bigwhite/go.tonybai.com.github.io，在该仓库的”Settings”标签中，我们启用github pages，并将该仓库的master分支作为站点的根路径。在同一页面的Custom domain下，我们填入go.tonybai.com，点击save保存。github会在该仓库中创建一个名为CNAME的文件，其内容如下： $cat CNAME go.tonybai.com 表示该站点绑定了自定义域名：go.tonybai.com。 正常情况下，你还可以在Settings标签下启用该静态站点的HTTPS服务，github会自动向Let&#8217;s Encrypt发起证书申请。 注：由于我的域名之前已经在Let&#8217;s Encrypt申请过相关证书，这里始终失败。这样导致后续我们只能使用go [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/another-approach-to-customize-package-import-path-1.png" alt="img{512x368}" /></p>
<p>我们见到的<a href="https://tonybai.com/2015/03/09/understanding-import-packages/">Go包的导入路径</a>常常以github.com、bitbucket.org等代码托管站点的域名为前缀，这样的包导入路径有一个问题，那就是<strong>当Go包的托管站点发生变更时(比如从github.om迁移到bitbucket.org或gitlab.com)，该包的使用者需要更新包的导入路径</strong>。当然，在<a href="https://tonybai.com/2018/11/26/hello-go-module-proxy/">支持go module+GOPROXY</a>的情况下，如果使用者不再升级包版本，他/她完全可以继续使用原包导入路径，但这仅是特例。</p>
<p>还有一些包的导入路径并非以知名代码托管站点域名作为前缀，比如：Go官方扩展包text，它的包导入路径是<strong>golang.org/x/text</strong>，这种包导入路径被称为<strong>vanity import path</strong>，字面义是<strong>虚荣心导入路径</strong>，即以个人或组织官方域名作为前缀的包导入路径。采用vanity import path的包<strong>避免了包迁移对包使用者的影响</strong>。包使用者完全无需关心包的实际存储位置是在github上还是在bitbucket上或是私有服务器上。同时将vanity import path作为包的<a href="http://tonybai.com/2014/11/04/some-changes-in-go-1-4/">权威路径(canonical import path)</a>，也方便go get等对包权威路径的检查，避免包路径变更的前后不一致。</p>
<p>之前笔者曾经写过<a href="https://tonybai.com/2017/06/30/go-get-go-packages-in-private-code-repo-by-govanityurls/">两篇文章</a>介绍了利用<a href="https://github.com/bigwhite/govanityurls">govanityurls</a>这个工具实现自定义包导入路径的方法。不过这种方法有一个约束条件，那就是你需要有一台VPS主机来部署运行govanityurls。虽然现在的云主机很便宜，但是购买和自建毕竟还是要付出一定成本的。如果没有VPS搭建govanityurls服务，那我们是否还有其他方法来自定义Go包导入路径呢？答案当然是<strong>有</strong>。</p>
<p>根据Go官方<a href="https://tip.golang.org/cmd/go/#hdr-Remote_import_paths">关于go get命令的文档</a>，当go get从非知名托管站点(github, bitbucket等之外的站点)获取go包时，会尝试在返回的http/https应答head标签中查找是否有如下形式meta标签：</p>
<pre><code>&lt;meta name="go-import" content="import-prefix vcs repo-root"&gt;

</code></pre>
<p>meta标签中的name值是固定的”go-import”，import-prefix即包vanity import path，比如：<strong>go.tonybai.com/gocmpp</strong>；vcs是采用的版本控制工具，git、svn或hg等；repo-root是包代码的实际存储服务器url。</p>
<p>下面是一个实际例子：</p>
<pre><code>&lt;meta name="go-import" content="go.tonybai.com/gocmpp git https://github.com/bigwhite/gocmpp"&gt;

</code></pre>
<p>对于这样的标签，go get会做进一步匹配(可参见GOROOT/src/cmd/go/internal/get/vcs.go中的matchGoImport函数实现)，看content值中的import-prefix是否是go get所需要的包的导入路径。如果是，则会向真正存储包代码的服务器再次发起代码获取请求(比如：git clone等）。</p>
<p>你可能会说：我用一个静态站点服务也能返回这样的应答。没错！但搭建静态站点一般还是需要VPS，这里我们介绍一种无须VPS的方法：<strong>利用<a href="https://pages.github.com/">github pages</a></strong>。</p>
<p>下面是利用github pages实现自定义Go包导入路径的原理图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/another-approach-to-customize-package-import-path.png" alt="img{512x368}" /></p>
<p><center>图：利用github pages实现自定义Go包导入路径</center></p>
<p>下面我们就以<strong>go.tonybai.com/gocmpp</strong>这个包导入路径的定制步骤来说明一下上述原理。</p>
<p>首先，我们要给<strong>tonybai.com</strong>这个域名添加一个子域名：<strong>go.tonybai.com</strong>作为我个人生产的所有Go包的导入路径前缀。我在DNS设置中为<strong>go.tonybai.com</strong>指定一个CNAME值：<strong>go.tonybai.com.github.io</strong>。这样当访问<strong>go.tonybai.com</strong>时，实际上是向<strong>go.tonybai.com.github.io</strong>发起请求。当然此刻如果你向<strong>go.tonybai.com</strong>发起请求时，你必然会得到404错误，因为github尚未建立起<strong>go.tonybai.com.github.io</strong>这个站点。</p>
<p>接下来，我们就来建立<strong>go.tonybai.com.github.io</strong>这个基于github pages的静态站点。我创建一个新的代码仓库：<strong>github.com/bigwhite/go.tonybai.com.github.io</strong>，在该仓库的”Settings”标签中，我们启用github pages，并将该仓库的master分支作为站点的根路径。在同一页面的<strong>Custom domain</strong>下，我们填入<strong>go.tonybai.com</strong>，点击save保存。github会在该仓库中创建一个名为CNAME的文件，其内容如下：</p>
<pre><code>$cat CNAME

go.tonybai.com

</code></pre>
<p>表示该站点绑定了自定义域名：<strong>go.tonybai.com</strong>。</p>
<p>正常情况下，你还可以在<strong>Settings</strong>标签下启用该静态站点的HTTPS服务，github会自动向Let&#8217;s Encrypt发起证书申请。</p>
<blockquote>
<p>注：由于我的域名之前已经在Let&#8217;s Encrypt申请过相关证书，这里始终失败。这样导致后续我们只能使用go get -insecure去获取Go包代码。</p>
</blockquote>
<p>在该仓库中，我们创建一个名为gocmpp的文件：</p>
<pre><code>&lt;html&gt;
    &lt;head&gt;
        &lt;meta name="go-import" content="go.tonybai.com/gocmpp git https://github.com/bigwhite/gocmpp"&gt;
        &lt;meta http-equiv="refresh" content="0;URL='https://github.com/bigwhite/gocmpp'"&gt;
    &lt;/head&gt;
    &lt;body&gt;
        Redirecting you to the &lt;a href="https://github.com/bigwhite/gocmpp"&gt;project page&lt;/a&gt;...
    &lt;/body&gt;
&lt;/html&gt;

</code></pre>
<p>该文件内容作为访问<strong>go.tonybai.com/gocmpp</strong>的请求的应答。</p>
<p>大约20分钟后，github pages内容生效。我们就可以使用下面命令去获取本存储在github.com/bigwhite/gocmpp下面的包了：</p>
<pre><code>$go get go.tonybai.com/gocmpp

</code></pre>
<p>由于证书问题，这里我们只能用go get -insecure，即让go get使用http协议发起请求。</p>
<p>在gopath mode下，我们的执行结果如下：</p>
<pre><code>

$GO111MODULE=off go get -x -v -insecure go.tonybai.com/gocmpp
# get https://go.tonybai.com/gocmpp?go-get=1
# get https://go.tonybai.com/gocmpp?go-get=1: 200 OK (1.012s)
get "go.tonybai.com/gocmpp": found meta tag get.metaImport{Prefix:"go.tonybai.com/gocmpp", VCS:"git", RepoRoot:"https://github.com/bigwhite/gocmpp"} at //go.tonybai.com/gocmpp?go-get=1
go.tonybai.com/gocmpp (download)
cd .
git clone -- https://github.com/bigwhite/gocmpp /Users/tonybai/Go/src/go.tonybai.com/gocmpp
cd /Users/tonybai/Go/src/go.tonybai.com/gocmpp
git submodule update --init --recursive
cd /Users/tonybai/Go/src/go.tonybai.com/gocmpp
git show-ref
cd /Users/tonybai/Go/src/go.tonybai.com/gocmpp
git submodule update --init --recursive

.... ....

cd /Users/tonybai/Go/src/go.tonybai.com/gocmpp
/Users/tonybai/.bin/go1.14/pkg/tool/darwin_amd64/compile -o $WORK/b001/_pkg_.a -trimpath "$WORK/b001=&gt;" -p go.tonybai.com/gocmpp -complete -buildid O9VmohLTciBDjallbacN/O9VmohLTciBDjallbacN -goversion go1.14 -D "" -importcfg $WORK/b001/importcfg -pack -c=4 ./activetest.go ./client.go ./conn.go ./connect.go ./deliver.go ./fwd.go ./packet.go ./receipt.go ./server.go ./submit.go ./terminate.go
/Users/tonybai/.bin/go1.14/pkg/tool/darwin_amd64/buildid -w $WORK/b001/_pkg_.a # internal
cp $WORK/b001/_pkg_.a /Users/tonybai/Library/Caches/go-build/ec/ec99b1c49c84d1e2edf88bee646f17198acc38c2c8f5a3d859540a394d6c5d0c-d # internal
mkdir -p /Users/tonybai/Go/pkg/darwin_amd64/go.tonybai.com/
mv $WORK/b001/_pkg_.a /Users/tonybai/Go/pkg/darwin_amd64/go.tonybai.com/gocmpp.a
rm -r $WORK/b001/
/Users/tonybai/go/src git:(master)  $tree -L 1 go.tonybai.com

go.tonybai.com
└── gocmpp

1 directory, 0 files
</code></pre>
<p>我们看到go get成功通过go.tonybai.com/gocmpp获取到gocmpp包，并编译安装成功(安装到GOPATH/pkg/下面)。</p>
<p>下面是module-aware模式下的go get获取结果：</p>
<pre><code>$GOPROXY='direct' go get -insecure -x -v go.tonybai.com/gocmpp

# get https://go.tonybai.com/?go-get=1
# get https://go.tonybai.com/gocmpp?go-get=1
# get https://go.tonybai.com/?go-get=1: 200 OK (1.032s)
# get https://go.tonybai.com/gocmpp?go-get=1: 200 OK (1.056s)
get "go.tonybai.com/gocmpp": found meta tag get.metaImport{Prefix:"go.tonybai.com/gocmpp", VCS:"git", RepoRoot:"https://github.com/bigwhite/gocmpp"} at //go.tonybai.com/gocmpp?go-get=1
mkdir -p /Users/tonybai/Go/pkg/mod/cache/vcs # git3 https://github.com/bigwhite/gocmpp
... ...
0.017s # cd /Users/tonybai/Go/pkg/mod/cache/vcs/63c8ecfc5ed2c830894c13fd15ab1494ce9897aefba1d11c78740b046033e9ae; git cat-file blob 0f5a658fda5e029943f9b256fefe4fa4550e7906:go.mod
go get: go.tonybai.com/gocmpp@v0.0.0-20200715060927-0f5a658fda5e: parsing go.mod:
    module declares its path as: github.com/bigwhite/gocmpp
            but was required as: go.tonybai.com/gocmpp

</code></pre>
<p>我们看到go get同样获取到了gocmpp module，但是由于module-aware模式下，go get会对module根路径进行检查，因此go get发现了go.mod中的module根路径：github.com/bigwhite/gocmpp与要获取的module路径(go.tonybai.com/gocmpp)不符并报错。我们更新一下gocmpp项目中的go.mod内容后，这个问题将不复存在。</p>
<p>这样，我们在没有VPS的前提下也实现了自定义包导入路径。后续每当我创建一个新module或新包，我只需向该仓库(go.tonybai.com.github.io)提交一个以module或package名字命名的文件即可，就像上的gocmpp文件那样。</p>
<h2>* 参考资料：https://gianarb.it/blog/go-mod-vanity-url</h2>
<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>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>微信公众号：iamtonybai</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily(Go每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</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/11/15/another-approach-to-customize-package-import-path/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>图解git原理的几个关键概念</title>
		<link>https://tonybai.com/2020/04/07/illustrated-tale-of-git-internal-key-concepts/</link>
		<comments>https://tonybai.com/2020/04/07/illustrated-tale-of-git-internal-key-concepts/#comments</comments>
		<pubDate>Tue, 07 Apr 2020 15:10:37 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[blob]]></category>
		<category><![CDATA[branch]]></category>
		<category><![CDATA[clone]]></category>
		<category><![CDATA[Commit]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[hash]]></category>
		<category><![CDATA[hub]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[MerkleTree]]></category>
		<category><![CDATA[progit]]></category>
		<category><![CDATA[pull]]></category>
		<category><![CDATA[push]]></category>
		<category><![CDATA[SHA-1]]></category>
		<category><![CDATA[Subversion]]></category>
		<category><![CDATA[svn]]></category>
		<category><![CDATA[tag]]></category>
		<category><![CDATA[Tree]]></category>
		<category><![CDATA[trunk]]></category>
		<category><![CDATA[upstream]]></category>
		<category><![CDATA[vcs]]></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=2884</guid>
		<description><![CDATA[git是那个“爱骂人”的Linux之父Linus Torvalds继Linux内核后奉献给全世界程序员的第二个礼物（不能确定已经逐渐老去的Torvalds能否迸发第三春，第三次给我们一个超大惊喜^_^）。这里再强调一下，git读作/git/，而不是/dʒit/。 在诞生十余载后(2005年发布第一版)，git毫无争议地成为了程序员版本管理工具的首选，它改变了全世界程序员的代码版本管理和生产协作的模式，极大促进了开源软件运动的发展。进化到今天的git已经成为了一个比较复杂的工具，多数程序员都将目光聚焦在如何记住这些命令并用好这些命令，对这些复杂命令行背后的原理却知之不多，虽然大多数程序员的确不太需要深刻了解git背后的原理^_^。 关于git原理的文章在互联网上也呈现出“汗牛充栋”之势，有些文章“蜻蜓点水”，有些文章“事无巨细”，看后似乎都无法让我满意。结合自己对git原理的学习，我觉得多数人把握住git运作机制的几个关键概念即可，于是就有了这篇文章，我努力尝试给大家讲清楚。 一. 我就是仓库，我拥有全部 我们首先要明确一个git与先前的版本管理工具（主要是subversion）的不同。下面是使用subversion版本管理工具时，程序员进行代码生产以及程序员间围绕代码仓库进行协作的模式： 图：subversion代码生产和协作模式 众所周知，subversion是基于中心版本仓库进行版本管理协作的版本管理工具。就像上图中那样，所有开发人员开始生产代码的前提是必须先从中心仓库checkout一份代码拷贝到自己本地的工作目录；而进行版本管理操作或者与他人进行协作的前提也是：中心版本仓库必须始终可用。这有点像以太网的“半双工的集线器(hub)模式”：svn中心仓库就像集线器本身，每个程序员节点就像连接到集线器上的主机；当一个程序员提交(commit)代码到中心仓库时，其他程序员不能提交，否则会出现冲突；如果中心仓库挂掉了，那么整个版本管理过程也将停止，程序员节点间无法进行协作，这就像集线器(hub)挂掉后，所有连接到hub上的主机节点间的网络也就断开无法相互通信一样。 如果我们使用git，我们是不需要“集线器”的： 图：git代码生产和协作模式 如上图所示，git号称分布式版本管理系统，本质上是没有像subversion中那个所谓的“中心仓库”的。每个程序员都拥有一个本地git仓库，而不仅仅是一份代码拷贝，这个仓库就是一个独立的版本管理节点，它拥有程序员进行代码生产、版本管理、与其他程序员协作的全部信息。即便在一台没有网络连接的机器上，程序员也能利用该仓库完成代码生产和版本管理工作。在网络ready的情况下，任意两个git仓库之间可以进行点对点的协作，这种协作无需中间协调者(中心仓库)参与。 二. github实现了基于git网络协作的控制平面 git实现了分布式版本管理系统，每个git仓库节点都是自治的。诸多git仓库节点一起形成了一个分布式git版本管理网络。这样的一个分布式网络存在着与普通分布式系统的类似的问题：如何发现对端节点的git仓库、如何管理和控制仓库间的访问权限等。如果说linus的git本身是这个分布式网络的数据平面工具(实现client/server间的双向数据通信)，那么这个分布式网络还缺少一个“控制平面”。 而github恰恰给出了一份git分布式网络控制平面的实现：托管、发现、控制&#8230;。其名称中含有的“hub”字样让我们想起了上面的“hub模式”： 图：github：git分布式网络控制平面的实现 我们看到在github的git协作模式实践中，引入了“中心仓库”的概念，各个程序员的节点git仓库源于(clone于)中心仓库。但是它和subversion的“中心仓库”有着本质的不同，这个仓库只是一个“upstream”库、是一个权威库。它并不是“集线器”，也没有按照“集线器”的那种工作模式进行协作。所有程序员节点的代码生产和版本管理操作完全可以脱离该所谓“中心库”而独立实施。 三. objects是个筐，什么都往里面装 上面都是从“宏观”谈git的一些与众不同的理念，而git原理，其实是从这一节才真正开始的^_^。 我们知道：每个git仓库的所有数据都存储在仓库顶层路径下的.git目录下： $tree -L 1 -F . ├── COMMIT_EDITMSG ├── HEAD ├── config ├── description ├── hooks/ ├── index ├── info/ ├── logs/ ├── objects/ └── refs/ 5 directories, 5 files 而在这些目录和文件中，又以objects路径下的数据内容最多，也最为重要。在git的设计中，objects目录就是一个“筐”，git的核心对象(object)都往里面“装”。 图：git核心数据对象类型与objects目录 从上图中，我们看到objects中存储的最主要的有三类对象：blob、commit和tree。这时你可能还不知道它们究竟是啥。不过没关系，我们通过一个例子来做一下“对号入座”。 我们在一个目录下建立git-internal-repo-demo目录，进入该目录，执行下面命令创建一个git仓库： [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/illustrated-tale-of-git-internal-key-concepts/illustrated-tale-of-git-internal-key-concepts-0.png" alt="img{512x368}" /></p>
<p><a href="https://git-scm.com/">git</a>是那个“爱骂人”的<a href="https://www.kernel.org/">Linux</a>之父<a href="https://github.com/torvalds">Linus Torvalds</a>继Linux内核后奉献给全世界程序员的第二个礼物（不能确定已经逐渐老去的Torvalds能否迸发第三春，第三次给我们一个超大惊喜^_^）。这里再强调一下，git读作<strong>/git/</strong>，而不是<strong>/dʒit/</strong>。</p>
<p>在诞生十余载后(2005年发布第一版)，<strong>git</strong>毫无争议地成为了程序员版本管理工具的首选，它改变了全世界程序员的<strong>代码版本管理和生产协作的模式</strong>，极大促进了开源软件运动的发展。进化到今天的<strong>git</strong>已经成为了一个比较复杂的工具，多数程序员都将目光聚焦在如何记住这些命令并用好这些命令，对这些复杂命令行背后的原理却知之不多，虽然大多数程序员的确不太需要深刻了解git背后的原理^_^。</p>
<p>关于git原理的文章在互联网上也呈现出“汗牛充栋”之势，有些文章“蜻蜓点水”，有些文章“事无巨细”，看后似乎都无法让我满意。结合自己对git原理的学习，我觉得多数人把握住<strong>git运作机制</strong>的几个关键概念即可，于是就有了这篇文章，我努力尝试给大家讲清楚。</p>
<h2>一. 我就是仓库，<strong>我拥有全部</strong></h2>
<p>我们首先要明确一个git与先前的版本管理工具（主要是<a href="https://subversion.apache.org/">subversion</a>）的不同。下面是使用<a href="https://tonybai.com/tag/svn">subversion</a>版本管理工具时，程序员进行代码生产以及程序员间围绕代码仓库进行协作的模式：</p>
<p><img src="https://tonybai.com/wp-content/uploads/illustrated-tale-of-git-internal-key-concepts/illustrated-tale-of-git-internal-key-concepts-1.png" alt="img{512x368}" /></p>
<p><center>图：subversion代码生产和协作模式</center></p>
<p>众所周知，subversion是<strong>基于中心版本仓库进行版本管理协作的版本管理工具</strong>。就像上图中那样，所有开发人员开始生产代码的前提是<strong>必须先从中心仓库checkout一份代码拷贝到自己本地的工作目录</strong>；而进行版本管理操作或者与他人进行协作的前提也是：<strong>中心版本仓库必须始终可用</strong>。这有点像以太网的“半双工的集线器(hub)模式”：svn中心仓库就像集线器本身，每个程序员节点就像连接到集线器上的主机；当一个程序员提交(commit)代码到中心仓库时，其他程序员不能提交，否则会出现冲突；如果中心仓库挂掉了，那么整个版本管理过程也将停止，程序员节点间无法进行协作，这就像集线器(hub)挂掉后，所有连接到hub上的主机节点间的网络也就断开无法相互通信一样。</p>
<p>如果我们使用git，我们是<strong>不需要“集线器”</strong>的：</p>
<p><img src="https://tonybai.com/wp-content/uploads/illustrated-tale-of-git-internal-key-concepts/illustrated-tale-of-git-internal-key-concepts-2.png" alt="img{512x368}" /></p>
<p><center>图：git代码生产和协作模式</center></p>
<p>如上图所示，git号称分布式版本管理系统，本质上是没有像subversion中那个所谓的“中心仓库”的。<strong>每个程序员都拥有一个本地git仓库</strong>，而不仅仅是一份代码拷贝，这个仓库就是一个<strong>独立的版本管理节点</strong>，它拥有程序员进行代码生产、版本管理、与其他程序员协作的<strong>全部信息</strong>。即便在一台没有网络连接的机器上，程序员也能利用该仓库完成代码生产和版本管理工作。在网络ready的情况下，任意两个git仓库之间可以进行点对点的协作，这种协作无需中间协调者(中心仓库)参与。</p>
<h2>二. github实现了基于git网络协作的<strong>控制平面</strong></h2>
<p>git实现了分布式版本管理系统，每个git仓库节点都是自治的。诸多git仓库节点一起形成了一个<strong>分布式git版本管理网络</strong>。这样的一个分布式网络存在着与普通分布式系统的类似的问题：如何发现对端节点的git仓库、如何管理和控制仓库间的访问权限等。如果说linus的git本身是这个分布式网络的数据平面工具(实现client/server间的双向数据通信)，那么这个分布式网络还缺少一个<strong>“控制平面”</strong>。</p>
<p>而<a href="https://github.com">github</a>恰恰给出了一份git分布式网络控制平面的实现：托管、发现、控制&#8230;。其名称中含有的“hub”字样让我们想起了上面的“hub模式”：</p>
<p><img src="https://tonybai.com/wp-content/uploads/illustrated-tale-of-git-internal-key-concepts/illustrated-tale-of-git-internal-key-concepts-3.png" alt="img{512x368}" /></p>
<p><center>图：github：git分布式网络控制平面的实现</center></p>
<p>我们看到在github的git协作模式实践中，引入了“中心仓库”的概念，各个程序员的节点git仓库源于(clone于)中心仓库。但是它和subversion的“中心仓库”有着本质的不同，这个仓库只是一个“upstream”库、是一个权威库。它并不是“集线器”，也没有按照“集线器”的那种工作模式进行协作。所有程序员节点的代码生产和版本管理操作完全可以脱离该所谓“中心库”而独立实施。</p>
<h2>三. <strong>objects</strong>是个筐，什么都往里面装</h2>
<p>上面都是从“宏观”谈git的一些与众不同的理念，而git原理，其实是从这一节才真正开始的^_^。</p>
<p>我们知道：每个git仓库的所有数据都存储在仓库顶层路径下的<strong>.git</strong>目录下：</p>
<pre><code>$tree -L 1 -F
.
├── COMMIT_EDITMSG
├── HEAD
├── config
├── description
├── hooks/
├── index
├── info/
├── logs/
├── objects/
└── refs/

5 directories, 5 files

</code></pre>
<p>而在这些目录和文件中，又以<strong>objects</strong>路径下的数据内容最多，也最为重要。在git的设计中，<strong>objects目录就是一个“筐”，git的核心对象(object)都往里面“装”</strong>。<br />
<img src="https://tonybai.com/wp-content/uploads/illustrated-tale-of-git-internal-key-concepts/illustrated-tale-of-git-internal-key-concepts-4.png" alt="img{512x368}" /></p>
<p><center>图：git核心数据对象类型与objects目录</center></p>
<p>从上图中，我们看到objects中存储的最主要的有三类对象：blob、commit和tree。这时你可能还不知道它们究竟是啥。不过没关系，我们通过一个例子来做一下“对号入座”。</p>
<p>我们在一个目录下建立git-internal-repo-demo目录，进入该目录，执行下面命令创建一个git仓库：</p>
<pre><code>➜  /Users/tonybai/test/git/git-internal-repo-demo git:(master) ✗ $git init .
Initialized empty Git repository in /Users/tonybai/Test/git/git-internal-repo-demo/.git/

</code></pre>
<p>这是一个处于初始状态的git仓库，我们看看存储git仓库数据的<strong>.git</strong>目录下的结构：</p>
<pre><code>➜  /Users/tonybai/test/git/git-internal-repo-demo git:(master) $tree .git
.git
├── HEAD
├── config
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   └── update.sample
├── info
│   └── exclude
├── objects
│   ├── info
│   └── pack
└── refs
    ├── heads
    └── tags

8 directories, 15 files

</code></pre>
<p>这个时候，<strong>objects这个筐还是空的</strong>！我们这就为仓库添点内容：</p>
<pre><code>$mkdir -p cmd/demo

在cmd/demo目录下添加main.go文件，内容如下:

// cmd/demo/main.go
package main

import "fmt"

func main() {
    fmt.Println("hello, git")
}

</code></pre>
<p>接下来我们使用git add将cmd/demo目录加入到stage区：</p>
<pre><code>$git add .

$git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached &lt;file&gt;..." to unstage)

    new file:   cmd/demo/main.go

</code></pre>
<p>这时我们来看一下objects这个筐是否有变化：</p>
<pre><code>├── objects
│   ├── 3e
│   │   └── 759ef88951df9b9b07077a7ec01f96b8e659b3
│   ├── info
│   └── pack

</code></pre>
<p>我们有一个object已经被装入到“筐”中了。我们看到objects目录下是一些以哈希值命名的文件和目录，其中目录由两个字符组成，是每个object hash值的前两个字符。hash值后续的字符串用于命名对应的object文件。在这里我们的object的hash值(实质是sha-1算法)为3e759ef88951df9b9b07077a7ec01f96b8e659b3，于是这个对象就被放入名为3e的目录下，对应的object文件为759ef88951df9b9b07077a7ec01f96b8e659b3。</p>
<p>我们使用git提供的<strong>低级命令</strong>查看一下这个object究竟是什么，其中git cat-file -t查看object的类型，git cat-file -p查看object的内容：</p>
<pre><code>$git cat-file -t 3e759ef889
blob

$git cat-file -p 3e759ef889
package main

import "fmt"

func main() {
    fmt.Println("hello, git")
}
</code></pre>
<p>我们看到objects这个筐中多了一个blob类型的对象，对象内容就是前面main.go文件中内容。</p>
<p>接下来，我们提交一下这次变更：</p>
<pre><code>$git commit -m"first commit" .
[master (root-commit) 3062e0e] first commit
 1 file changed, 7 insertions(+)
 create mode 100644 cmd/demo/main.go

</code></pre>
<p>再来看看<strong>.git/objects</strong>中的变化：</p>
<pre><code>├── objects
│   ├── 1f
│   │   └── 51fe448aacc69c0f799def9506e61ed3eb60fa
│   ├── 30
│   │   └── 62e0ebad9415b704e96e5cee1542187b7ed571
│   ├── 3d
│   │   └── 2045367ea40c098ec5c7688119d72d97fb09a5
│   ├── 3e
│   │   └── 759ef88951df9b9b07077a7ec01f96b8e659b3
│   ├── 40
│   │   └── 6d08e1159e03ae82bcdbe1ad9f076a04a41e2b
│   ├── info
│   └── pack

</code></pre>
<p>我们看到筐里被一下子新塞入4个object。我们分别看看新增的4个object类型和内容都是什么：</p>
<pre><code>$git cat-file -t 1f51fe448a
tree
$git cat-file -p 1f51fe448a
100644 blob 3e759ef88951df9b9b07077a7ec01f96b8e659b3    main.go

$git cat-file -t 3062e0ebad
commit
$git cat-file -p 3062e0ebad
tree 406d08e1159e03ae82bcdbe1ad9f076a04a41e2b
author Tony Bai &lt;bigwhite.cn@aliyun.com&gt; 1586243612 +0800
committer Tony Bai &lt;bigwhite.cn@aliyun.com&gt; 1586243612 +0800

first commit

$git cat-file -t 3d2045367e
tree
$git cat-file -p 3d2045367e
040000 tree 1f51fe448aacc69c0f799def9506e61ed3eb60fa    demo

$git cat-file -t 406d08e115
tree
$git cat-file -p 406d08e115
040000 tree 3d2045367ea40c098ec5c7688119d72d97fb09a5    cmd

</code></pre>
<p>这里我们看到了另外两种类型的object被加入“筐”中：commit和tree类型。objects这个筐里目前有了5个object，我们不考虑git是以何种格式存储这些object的，我们想知道的是这几个object的关系是什么样的。请看下一小节^_^。</p>
<h2>四. 每个<strong>commit</strong>都是一个git仓库的快照</h2>
<p>要理清objects“筐”中各object间的关系，就必须要把握住一个关键概念：“每个<strong>commit</strong>都是git仓库的一个快照” &#8211; 以一个commit为入口，我们能将当时objects下面的所有object联系在一起。因此，上面5个object中的那个commit对象就是我们分析各object关系的入口。我们根据上述5个object的内容将这5个object的关系组织为下面这幅示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/illustrated-tale-of-git-internal-key-concepts/illustrated-tale-of-git-internal-key-concepts-5.png" alt="img{512x368}" /></p>
<p><center>图：commit、tree、blob对象之间的关系</center></p>
<p>通过上图我们看到：</p>
<ul>
<li>
<p>commit是对象关系图的入口；</p>
</li>
<li>
<p>tree对象用于描述目录结构，每个目录节点都会用一个tree对象表示。目录间、目录文件间的层次关系会在tree对象的内容中体现；</p>
</li>
<li>
<p>每个commit都会有一个root tree对象；</p>
</li>
<li>
<p>blob对象为tree的叶子节点，它的内容即为文件的内容。</p>
</li>
</ul>
<p>上面仅是一次commit后的关系图，为了更清晰的看到多个commit对象之间关系，我们再来对git repo进行一次变更提交:</p>
<pre><code>我们创建pkg/foo目录：

$mkdir -p pkg/foo

然后创建文件pkg/foo/foo.go，其内容如下：

// pkg/foo/foo.go
package foo

import "fmt"

func Foo() {
    fmt.Println("this is foo package")
}

</code></pre>
<p>提交这次变更：</p>
<pre><code>$git add pkg
$git commit -m"add package foo" .
[master 6f7f08b] add package foo
 1 file changed, 7 insertions(+)
 create mode 100644 pkg/foo/foo.go
</code></pre>
<p>下面是提交变更后的“筐”内的对象：</p>
<pre><code>$tree objects
objects
├── 1f
│   └── 51fe448aacc69c0f799def9506e61ed3eb60fa
├── 29
│   └── 3ae375dcef1952c88f35dd4d2a1d4576dea8ba
├── 30
│   └── 62e0ebad9415b704e96e5cee1542187b7ed571
├── 3d
│   └── 2045367ea40c098ec5c7688119d72d97fb09a5
├── 3e
│   └── 759ef88951df9b9b07077a7ec01f96b8e659b3
├── 40
│   └── 6d08e1159e03ae82bcdbe1ad9f076a04a41e2b
├── 65
│   └── 5dd3aae645813dc53834ebfa8d19608c4b3905
├── 6e
│   └── e873d9c7ca19c7fe609c9e1a963df8d000282b
├── 6f
│   └── 7f08b14168beb114c3cc099b8dc1c09ccd4739
├── cc
│   └── 9903a33cb99ae02a9cb648bcf4a71815be3474
├── info
└── pack

12 directories, 10 files

</code></pre>
<p>object已经多到不便逐一分析了。但我们把握住一点：<strong>commit是分析关系的入口</strong>。我们通过commit的输出或commit log(git log)可知，新增的commit对象的hash值为6f7f08b141。我们还是以它为入口分析新增object的关系以及它们与之前已存在的object的关系：</p>
<p><img src="https://tonybai.com/wp-content/uploads/illustrated-tale-of-git-internal-key-concepts/illustrated-tale-of-git-internal-key-concepts-6.png" alt="img{512x368}" /></p>
<p><center>图：commit、tree、blob对象之间的关系1</center></p>
<p>从上图我们看到：</p>
<ul>
<li>
<p>git新创建tree对象对应我们新建的pkg目录以及其子目录；</p>
</li>
<li>
<p>cmd目录下的子目录和文件内容并未改变，因此这次commit所对应的root tree对象(293ae375dc)直接使用了已存在的cmd目录对应的对象(3d2045367e);</p>
</li>
<li>
<p>新commit对象会将第一个commit对象作为parent，这样多个commit对象之间构成一个单向链表。</p>
</li>
</ul>
<p>上面的两个提交都是新增内容，我们再来提交一个commit，这次我们对已有文件内容做变更：</p>
<pre><code>将cmd/demo/main.go文件内容变更为如下内容：

// cmd/demo/main.go
package main

import (
    "fmt"

    "github.com/bigwhite/foo"
)

func main() {
    fmt.Println("hello, git")
    foo.Foo()
}

提交变更：

$git commit -m"call foo.Foo in main" .
[master 2f14635] call foo.Foo in main
 1 file changed, 6 insertions(+), 1 deletion(-)

</code></pre>
<p>和上面的分析方法一样，我们通过最新commit对应的hash值2f146359b4对新对象和现存对象的关系进行分析：</p>
<p><img src="https://tonybai.com/wp-content/uploads/illustrated-tale-of-git-internal-key-concepts/illustrated-tale-of-git-internal-key-concepts-7.png" alt="img{512x368}" /></p>
<p><center>图：commit、tree、blob对象之间的关系2</center></p>
<p>如上图，第三次变更提交后，我们看到：</p>
<ul>
<li>
<p>由于main.go文件变更，git重建了main.go blob对象、demo、cmd tree对象</p>
</li>
<li>
<p>由于pkg目录、其子目录布局、子目录下文件内容没有改变，于是新commit对象对应的root tree对象直接“复用”了上一次commit的pkg tree对象。</p>
</li>
<li>
<p>新commit对象加入commit对象单向链表，并将上一次的commit对象作为parent。</p>
</li>
</ul>
<p>我们看到沿着最新的commit对象(2f146359b4)，我们能获取当前仓库的最新结构布局以及各个blob对象的最新内容，即最新的一个快照！</p>
<h2>五. object是不可变的，默克尔树(Merkle Tree)判断变化</h2>
<p>从上面的三次变更，我们看到无论哪种对象object，<strong>一旦放入到objects这个“筐”就是不可变的(immutable)</strong>。即便是第三次commit对main.go进行了修改，git也只是根据main.go的最新内容<strong>创建一个新的blob对象</strong>，而不是修改或替换掉第一版main.go对应的blob对象。</p>
<p>对应目录的tree object亦是如此。如果某目录下的二级目录发生变化或目录下的文件内容发生改变，git会新生成一个对应该目录的tree对象，而不是去修改原先已存在的tree对象。</p>
<p>实际上，git tree对象的组织本身就是一棵<a href="http://en.wikipedia.org/wiki/Merkle_tree">默克尔树(Merkle Tree)</a>。</p>
<p>默克尔树是一类基于哈希值的二叉树或多叉树，其叶子节点上的值通常为数据块的哈希值，而非叶子节点上的值，是将该节点的所有孩子节点的组合结果的哈希值。默克尔树的特点是，底层数据的任何变动，都会传递到其父亲节点，一直到树根。</p>
<p><img src="https://tonybai.com/wp-content/uploads/illustrated-tale-of-git-internal-key-concepts/illustrated-tale-of-git-internal-key-concepts-merkle-tree.png" alt="img{512x368}" /></p>
<p><center>图：默克尔树(图片来自网络)</center></p>
<p>以上图为例：我们自下向上看，D0、D1、D2和D3是叶子节点包含的数据。N0、N1、N2和N3是叶子节点，它们是将数据（也就是D0、D1、D2和D3）进行hash运算后得到的hash值；继续往上看，N4和N5是中间节点，N4是N0和N1经过hash运算得到的哈希值，N5是N2和N3经过hash运算得到的哈希值。（注意，hash值计算方法：把相邻的两个叶子结点合并成一个字符串，然后运算这个字符串的哈希）。最后，Root节点是N4和N5经过hash运算后得到的哈希值，这就是这颗默克尔树的根哈希。当N0包含的数据发生变化时，根据默克尔树的节点hash值形成机制，我们可以快速判断出：<strong>N0、N4和root节点会发生变化</strong>。</p>
<p>对应git来说，叶子节点对应的就是每个文件的hash值，tree对象对应的是中间节点。因此，通过默克尔树(Merkle Tree)的特性，我们可以快速判断哪些对象对应的目录或文件发生了变化，应该重新创建对应的object。我们还以上面的第三次commit为例：</p>
<p><img src="https://tonybai.com/wp-content/uploads/illustrated-tale-of-git-internal-key-concepts/illustrated-tale-of-git-internal-key-concepts-8.png" alt="img{512x368}" /></p>
<p><center>图：通过默克尔树(Merkle Tree)的特性判断哪些对象发生变化需要重新创建</center><br />
如上图所示，第三次commit是因为cmd/demo/main.go内容发生了变化，根据merkle tree特性，我们可以快速判断红色的object会随之发生变化。于是git会自底向上逐一创建这些新对象：main.go文件对应的blob对象以及demo、cmd以及根节点对应的tree对象。</p>
<h2>六. branch和tag之所以轻量，因为它们都是“指针”</h2>
<p>使用subversion时，创建branch或打tag使用的是svn copy命令。svn copy执行的就是真实的文件拷贝，相当于将trunk下的目录和文件copy一份放到branch或tag下面，建立一个trunk的副本，这样的操作绝对是“超重量级”的。如果svn仓库中的文件数量庞大且size很大，那么svn copy执行起来不仅速度慢，而且还会在svn server上占用较大的磁盘存储空间，因此使用svn时，打tag和创建branch是要“谨慎”的。</p>
<p>而git的branch和tag则极为轻量，我们来给上面例子中的仓库创建一个dev分支：</p>
<pre><code>$git branch dev

</code></pre>
<p>我们看看.git下有啥变化：</p>
<pre><code>.

└── refs
    ├── heads
    │   ├── dev
    │   └── master
    └── tags

</code></pre>
<p>我们看到.git/refs/heads下面多出了一个dev文件，我们查看一下该文件的内容：</p>
<pre><code>$cat refs/heads/dev
2f146359b475909f2fdcdef046af3431c8077282

$git log --oneline

2f14635 (HEAD -&gt; master, dev) call foo.Foo in main
6f7f08b add package foo
3062e0e first commit

</code></pre>
<p>对比发现，dev文件中的内容恰是最新的commit对象：2f146359b475909f2fdcdef046af3431c8077282。</p>
<p>我们再来给repo打一个tag：</p>
<pre><code>$git tag v0.0.1

</code></pre>
<p>同样，我们来查看一下.git目录下的变化：</p>
<pre><code>└── refs
    ├── heads
    │   ├── dev
    │   └── master
    └── tags
        └── v0.0.1

</code></pre>
<p>我们看到在refs/tags下面增加一个名为v0.0.1的文件，查看其内容：</p>
<pre><code>$cat refs/tags/v0.0.1
2f146359b475909f2fdcdef046af3431c8077282

</code></pre>
<p>和dev分支文件一样，它的内容也是最新的commit对象：2f146359b475909f2fdcdef046af3431c8077282。</p>
<p>可见，使用git创建分支或tag仅仅是创建了一个指向某个commit对象的<strong>“指针”</strong>，这与subversion的副本操作相比，简直不能再轻量了。</p>
<p>前面说过，一个commit对象都是一个git仓库的快照，切换到(git checkout xxx)某个branch或tag，就是将本地工作拷贝切换到commit对象所代表的仓库快照的状态。当然也会将commit对象组成的单向链表的head指向该commit对象，这个head即.git/HEAD文件的内容。</p>
<h2>七. 小结</h2>
<p>到这里，git原理的几个关键概念就交代完了，再回顾一下：</p>
<ul>
<li>
<p>和subversion这样的集中式版本管理工具最大的不同就是每个程序员节点都是git仓库，拥有全部开发、协作所需的全部信息，完全可以脱离“中心节点”；</p>
</li>
<li>
<p>如果说git聚焦于数据平面的功能，那么github则是一个基于git网络协作的<strong>控制平面</strong>的实现；</p>
</li>
<li>
<p><strong>objects</strong>是个筐，什么都往里面装。git仓库的核心数据都存在.git/objects下面，主要类型包括：blob、tree和commit；</p>
</li>
<li>
<p>每个<strong>commit</strong>都是一个git仓库的快照，记住commit对象是分析对象关系的入口;</p>
</li>
<li>
<p>git是基于数据内容的hash值做等值判定的，object是不可变的，默克尔树(Merkle Tree)用来快速判断变化。</p>
</li>
<li>
<p>branch和tag因为是“指针”，因此创建、销毁和切换都非常轻量。</p>
</li>
</ul>
<h2>八. 参考资料</h2>
<ul>
<li>
<p><a href="https://git-scm.com/book/en/v2">Pro Git v2</a> &#8211; https://git-scm.com/book/en/v2</p>
</li>
<li>
<p><a href="https://www.cnblogs.com/kisun168/p/11408346.html">git介绍</a> &#8211; https://www.cnblogs.com/kisun168/p/11408346.html</p>
</li>
<li>
<p><a href="https://zhuanlan.zhihu.com/p/53750883">git内部原理</a> &#8211; https://zhuanlan.zhihu.com/p/53750883</p>
</li>
<li>
<p><a href="https://www.jianshu.com/p/72f9f8c9c47e">git仓库内部结构</a> &#8211; https://www.jianshu.com/p/72f9f8c9c47e</p>
</li>
</ul>
<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; 2020, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2020/04/07/illustrated-tale-of-git-internal-key-concepts/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>使用git操作svn仓库</title>
		<link>https://tonybai.com/2019/06/25/using-git-with-svn-repo/</link>
		<comments>https://tonybai.com/2019/06/25/using-git-with-svn-repo/#comments</comments>
		<pubDate>Tue, 25 Jun 2019 15:46:36 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[branch]]></category>
		<category><![CDATA[dcommit]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[Git-svn]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[rebase]]></category>
		<category><![CDATA[repository]]></category>
		<category><![CDATA[Subversion]]></category>
		<category><![CDATA[svn]]></category>
		<category><![CDATA[svnbucket]]></category>
		<category><![CDATA[tag]]></category>
		<category><![CDATA[分支]]></category>
		<category><![CDATA[标签]]></category>
		<category><![CDATA[版本控制]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=2723</guid>
		<description><![CDATA[如今，虽然Git已经大行其道，但是仍有很多IT公司和组织依旧在使用集中式的版本控制系统subversion，尤其是一些传统软件公司，他们倾向于集中式的联网开发。如果你是一个Git fans，并且你要是遇到代码仓库依旧是使用subversion进行版本控制的情况，你又该如何施展呢？ 其实git很早就支持与subversion repo的互操作了，2011年我就曾写过一篇《小试git-svn》的博文，也正是那一年，我第一次使用git操作subversion仓库。 《小试git-svn》一文仅是通过文字性描述简要说明了git操作svn repo的基本命令和功能，并未结合实际例子，也缺少直观的图片展示，并且未涉及branch和tag的操作。这里打算再写一篇关于使用git操作svn仓库的文章，相比于前者，我期望该文能更为系统并结合demo图文并茂的把使用git操作svn仓库这个事情讲的更形象和透彻一些。 一. 使用git操作svn repo的基本工作流 使用git操作svn repo的多数场景是已经存在一个svn repo，git fans想用git命令与之交互。下图展示了使用git操作这样的svn repo的基本工作流： 下面我们就用一个demo来详细地说明一下这个基本工作流。 1. 建立一个svn demo库 自己搭建一个svn server还是比较费力的，我们选择一个在线的svn代码托管SaaS服务：svnbucket.com。我们在svnbucket.com上注册账号并创建一个svn repo：test-git-svn，该repo采用标准的项目布局(trunk/branches/tags)： 接下来我们就开始git操作svn repo的过程！ 2. 通过git首次获取svn仓库 git是分布式版本管理工具，无论是git repo还是svn repo，如果要用git操作，那么首先需要获取到repo的所有数据。git提供了svn子命令来操作远程的svn repo库，我们看一下首次获取svn repo信息的过程： $git svn clone svn://svnbucket.com/bigwhite/test-git-svn/ Initialized empty Git repository in /Users/tony/Test/git-svn-test/test-git-svn/.git/ W: +empty_dir: branches W: +empty_dir: tags W: +empty_dir: trunk r1 = 8cfdc2f6059ff06f53c83d64518dcba146722c04 (refs/remotes/git-svn) Checked out [...]]]></description>
			<content:encoded><![CDATA[<p>如今，虽然<a href="https://git-scm.com/">Git</a>已经大行其道，但是仍有很多IT公司和组织依旧在使用集中式的版本控制系统<a href="https://subversion.apache.org/">subversion</a>，尤其是一些传统软件公司，他们倾向于集中式的联网开发。如果你是一个<a href="https://tonybai.com/tag/git">Git</a> fans，并且你要是遇到代码仓库依旧是使用<a href="https://tonybai.com/tag/svn">subversion</a>进行版本控制的情况，你又该如何施展呢？</p>
<p>其实git很早就<a href="https://git-scm.com/docs/git-svn">支持与subversion repo的互操作</a>了，2011年我就曾写过一篇<a href="https://tonybai.com/2011/01/20/try-git-svn/">《小试git-svn》</a>的博文，也正是那一年，我第一次使用git操作subversion仓库。</p>
<p><a href="https://tonybai.com/2011/01/20/try-git-svn/">《小试git-svn》</a>一文仅是通过文字性描述简要说明了git操作svn repo的基本命令和功能，并未结合实际例子，也缺少直观的图片展示，并且未涉及branch和tag的操作。这里打算再写一篇关于使用git操作svn仓库的文章，相比于前者，我期望该文能更为系统并结合demo图文并茂的把使用git操作svn仓库这个事情讲的更形象和透彻一些。</p>
<h2>一. 使用git操作svn repo的基本工作流</h2>
<p>使用git操作svn repo的多数场景是已经存在一个svn repo，git fans想用git命令与之交互。下图展示了使用git操作这样的svn repo的基本工作流：</p>
<p><img src="https://tonybai.com/wp-content/uploads/git-svn-basic-workflow.png" alt="img{512x368}" /></p>
<p>下面我们就用一个demo来详细地说明一下这个基本工作流。</p>
<h3>1. 建立一个svn demo库</h3>
<p>自己搭建一个svn server还是比较费力的，我们选择一个在线的svn代码托管SaaS服务：svnbucket.com。我们在svnbucket.com上注册账号并创建一个svn repo：<strong>test-git-svn</strong>，该repo采用标准的项目布局(trunk/branches/tags)：</p>
<p><img src="https://tonybai.com/wp-content/uploads/test-git-svn-init-layout.png" alt="img{512x368}" /></p>
<p>接下来我们就开始git操作svn repo的过程！</p>
<h3>2. 通过git首次获取svn仓库</h3>
<p>git是分布式版本管理工具，无论是git repo还是svn repo，如果要用git操作，那么首先需要获取到repo的所有数据。git提供了svn子命令来操作远程的svn repo库，我们看一下首次获取svn repo信息的过程：</p>
<pre><code>$git svn clone svn://svnbucket.com/bigwhite/test-git-svn/
Initialized empty Git repository in /Users/tony/Test/git-svn-test/test-git-svn/.git/
W: +empty_dir: branches
W: +empty_dir: tags
W: +empty_dir: trunk
r1 = 8cfdc2f6059ff06f53c83d64518dcba146722c04 (refs/remotes/git-svn)
Checked out HEAD:
  svn://svnbucket.com/bigwhite/test-git-svn r1
creating empty directory: branches
creating empty directory: tags
creating empty directory: trunk

$tree ./test-git-svn
./test-git-svn
├── branches
├── tags
└── trunk

3 directories, 0 files
$cd test-git-svn
$git branch -a
* master
  remotes/git-svn

</code></pre>
<p>可以看到：我们通过git svn clone(注意：不是git clone)将远程server上的svn repo下载到了本地，后续我们就可以在本地host上快乐地使用git管理本地的代码了。</p>
<h3>3. 从svn repo中同步最新的代码变更</h3>
<p>接下来，远程的svn仓库经常会发生了变更，某开发人员向svn仓库提交了一些initial code，比如在trunk下建立git-svn-demo目录，并创建go.mod和main.go：</p>
<pre><code>//在svn repo中的trunk/git-svn-demo目录下：

$cat main.go
package main

import "fmt"

func main() {
    fmt.Println("git-svn-demo initial version")
}

$cat go.mod
module github.com/bigwhite/git-svn-demo

</code></pre>
<p>如果我们本地使用svn工具，我们只需在联网的情况下通过svn update命令即可将远程svn repo的最新改动同步到本地working copy中。但在git下，我们不能像git repo同步那样使用git pull来同步，而是需要使用git svn rebase来获取svn repo中的最新更新，并rebase我们的工作目录(working copy)：</p>
<pre><code> $git svn rebase
    A    trunk/git-svn-demo/go.mod
    A    trunk/git-svn-demo/main.go
r2 = f826b74bfff2799deaafbca81354c38e0862509c (refs/remotes/git-svn)
First, rewinding head to replay your work on top of it...
Fast-forwarded master to refs/remotes/git-svn.

$tree .
.
├── branches
├── tags
└── trunk
    └── git-svn-demo
        ├── go.mod
        └── main.go

4 directories, 2 files

</code></pre>
<p>git svn rebase子命令会根据svn上的revision创建对应的commit，这一命令几乎等效于”svn update”，同样也可能会存在远程svn repo中的代码与git repo冲突的可能性，解决冲突的方法在<a href="https://tonybai.com/2011/01/20/try-git-svn/">《小试git-svn》</a>中已经做了描述，这里就不赘述了。</p>
<h3>4. 将代码更新推送到远程svn repo</h3>
<p>在这种模式下，本地开发已经完全变成了基于git的开发模式，开发者可以自由地发挥git的各种优势了，再也不用担心本地代码没有版本控制而出现各种“误删除”、“意外覆盖”的情况了。开发测试并提交(只需普通git commit)到local git repo后，最终还是要将这些commit推送到远程的svn repo中。这里我们不能用push，而要用git svn dcommit：</p>
<pre><code>// 本地git repo中更新后的main.go

$cat main.go
package main

import "fmt"

func main() {
    fmt.Println("git-svn-demo: git-svn dcommit v0")
}

</code></pre>
<p>先提交到git本地的仓库:</p>
<pre><code>$git commit -m"[git svn]: first commit" .
[master be36a7f] [git svn]: first commit
 1 file changed, 1 insertion(+), 1 deletion(-)

</code></pre>
<p>然后再“推送”到远程的svn 仓库：</p>
<pre><code>$git svn dcommit
Committing to svn://svnbucket.com/bigwhite/test-git-svn ...
    M    trunk/git-svn-demo/main.go
Committed r3
    M    trunk/git-svn-demo/main.go
r3 = e35efbe999cd035b2d5d67886c9a786ef86c681e (refs/remotes/git-svn)
No changes between be36a7f1164b73a994f28ee3b0e0bb711b5ba2ff and refs/remotes/git-svn
Resetting to the latest refs/remotes/git-svn

</code></pre>
<p>dcommit会将git repo当前branch与远程svn repo中的差异的git commit都提交到svn repo，并为每个git commit生成一个对应的svn revision。这和”git push”很类似。</p>
<p>我们再来本地做两次git commit：</p>
<pre><code>$git commit -m"[git svn]: commit #2" .

$git commit -m"[git svn]: commit #3" .

</code></pre>
<p>dcommit到svn repo：</p>
<pre><code>$git svn dcommit
Committing to svn://svnbucket.com/bigwhite/test-git-svn ...
    M    trunk/git-svn-demo/main.go
Committed r4
    M    trunk/git-svn-demo/main.go
r4 = c997db60e3d82c97ce8da23b308d611005740844 (refs/remotes/git-svn)
    M    trunk/git-svn-demo/main.go
Committed r5
    M    trunk/git-svn-demo/main.go
r5 = 3b6215a3e5ae0659743e1e8063f842448c19147c (refs/remotes/git-svn)
No changes between ee0df22b9f41882518a7c7b975c38924a9422395 and refs/remotes/git-svn
Resetting to the latest refs/remotes/git-svn

</code></pre>
<p>我们看到git svn为每个commit生成一个对应的svn revision(svn版本号），这里是r4、r5。</p>
<h2>二. 利用git branch的优势</h2>
<p>和svn建立branch的“重量级”操作（文件copy）相比，git的branch创建和切换可谓“超轻量级”。因此在日常使用git中，多数开发者都会充分发挥git branch的优势，通过在不同branch上的操作、分支的merge等来减少对master的并发修改带来冲突的影响。</p>
<p>我们经常使用feature branch或bugfix branch。以feature branch为例，在feature branch上一般会有多个commit。但在merge到master分支时，我们可以选择多种merge策略，或是fast forward，或是多个commit自动合并为一个commit，又或git merge支持&#8211;squash策略（即只merge代码到本地Working copy，不commit到git repo，后续可作为一个commit手工提交到git repo）。</p>
<p>我个人在用git操作svn repo库时，在git本地开发中，更倾向于使用git merge &#8211;squash的方法，因为在feature branch上，我更喜欢频繁的小变更的提交，导致commit很多。如果这些commit都dcommit到svn库，可能让svn commit history项目过多，有些commit甚至没有比较完善的意义。</p>
<p>我们在上面的demo上演示一下这个过程。</p>
<p>在本地建立新分支：feature-branch-1：</p>
<pre><code>$git checkout -b feature-branch-1
Switched to a new branch 'feature-branch-1'

</code></pre>
<p>在feature-branch-1做两次修改并commit：</p>
<pre><code>$git commit -m"add foo" .
[feature-branch-1 d12ca00] add foo
 1 file changed, 4 insertions(+)

$git commit -m"add bar" .
[feature-branch-1 160e5ed] add bar
 1 file changed, 4 insertions(+)

</code></pre>
<p>回到master分支，merge feature分支的修改，并合并为本地的一次commit：</p>
<pre><code> $git checkout master
Switched to branch 'master'

$git merge feature-branch-1 --squash
Updating 3b6215a..160e5ed
Fast-forward
Squash commit -- not updating HEAD
 trunk/git-svn-demo/main.go | 8 ++++++++
 1 file changed, 8 insertions(+)

$git commit -m"[git svn]: add foo and bar function" .
[master fe8f153] add foo and bar function
 1 file changed, 8 insertions(+)

</code></pre>
<p>接下来，将这次合并的commit同步到svn repo上：</p>
<pre><code>$git svn dcommit
Committing to svn://svnbucket.com/bigwhite/test-git-svn ...
    M    trunk/git-svn-demo/main.go
Committed r6
    M    trunk/git-svn-demo/main.go
r6 = 37bbfbdb99cb7331057a05b72dc55b3faf55b645 (refs/remotes/git-svn)
No changes between fe8f153cac62e027ca068fdd55c2bdaa8751aaf8 and refs/remotes/git-svn
Resetting to the latest refs/remotes/git-svn

</code></pre>
<h2>三. 通过git为svn库建立branch和打tag</h2>
<p>通过git为svn repo建立branch和tag这类操作其实并没有体现出git的优势，因此日常开发人员一般会用svn命令直接操作svn repo，而不是用git svn子命令。但这里我们仍然要介绍一下通过git为svn repo建立branch和tag的方法。</p>
<p>我们先来看看创建branch：</p>
<pre><code>$git svn branch feature-branch-1-from-git
Multiple branch paths defined for Subversion repository.
You must specify where you want to create the branch with the --destination argument.
</code></pre>
<p>我们看到git svn branch命令出错：让我们指定&#8211;destination参数，那我们就再来一遍：</p>
<pre><code> $git svn  branch feature-branch-1-from-git --destination=branches
Unknown branch destination branches

</code></pre>
<p>依旧报错！似乎git不认识“branches”这个存放branch的目录！要想解决这个问题，我们需要对.git/config中的配置做些变更，添加最后两行：</p>
<pre><code>$cat .git/config
[core]
        repositoryformatversion = 0
        filemode = true
        bare = false
        logallrefupdates = true
        ignorecase = true
        precomposeunicode = true
[svn-remote "svn"]
        url = svn://svnbucket.com/bigwhite/test-git-svn
        fetch = :refs/remotes/git-svn
        branches = branches/*:refs/remotes/*
        tags = tags/*:refs/remotes/*

</code></pre>
<p>原先的.git/config中并没有设置branhes和tags的入口。我们再来试一下建立branch：</p>
<pre><code>git svn --username=bigwhite  branch feature-branch-1-from-git
Copying svn://svnbucket.com/bigwhite/test-git-svn at r8 to svn://svnbucket.com/bigwhite/test-git-svn/branches/feature-branch-1-from-git...
Authorization failed: Unable to connect to a repository at URL 'svn://svnbucket.com/bigwhite/test-git-svn': Can't get password at /usr/local/Cellar/git/2.12.2/libexec/git-core/git-svn line 1200.

</code></pre>
<p>仍然报错！不过这个错误应该是<a href="https://github.com/git/git/commit/e0688e9b28f2c5ff711460ee8b62077be5df2360">git（我使用的是2.12.2版本）的一个bug</a>，我们用try-run方式运行的结果却是一切ok的：</p>
<pre><code>$git svn --username=bigwhite -n branch feature-branch-1-from-git
Copying svn://svnbucket.com/bigwhite/test-git-svn at r8 to svn://svnbucket.com/bigwhite/test-git-svn/branches/feature-branch-1-from-git...

</code></pre>
<p>打tag的方式与建立 branch的方式类似：</p>
<pre><code> $git svn tag v1.0.0 -n  -m "[git svn]: tag v1.0.0" --destination=tags
Copying svn://svnbucket.com/bigwhite/test-git-svn at r5 to svn://svnbucket.com/bigwhite/test-git-svn/tags/v1.0.0...

</code></pre>
<h2>四. 小结</h2>
<p>git svn子命令是git fans操作svn repo的利器。由于git svn clone svn_repo后的repo就是一个标准的本地git repo，因此我们还可以为该git repo建立remote upstream repo，这样就可以在local git repo、remote git repo以及remote svn repo三者之间进行代码变更的同步了，当然这种场景操作还是蛮复杂的，也相对少见。</p>
<p>个人建议，无论个人还是组织，即便使用svn中心repo，在本地也尽量用git来进行源码版本管理，并通过git svn与中心svn repo互操作。</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/25/using-git-with-svn-repo/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>也谈Commit log</title>
		<link>https://tonybai.com/2013/05/09/also-talk-about-commit-log/</link>
		<comments>https://tonybai.com/2013/05/09/also-talk-about-commit-log/#comments</comments>
		<pubDate>Thu, 09 May 2013 09:18:55 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Apache]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[Bug]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Chinglish]]></category>
		<category><![CDATA[Commit]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Mercurial]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[pre-commit-hook]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Solaris]]></category>
		<category><![CDATA[Subversion]]></category>
		<category><![CDATA[svn]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[Unix]]></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=1264</guid>
		<description><![CDATA[在版本控制工具大行其道的今天，作为程序员，势必要每天与各种版本控制系统（比如Subversion、Git、Mercurial等）打交道， 每天不commit几次代码都不好意思说自己是专业程序员^_^。不过commit代码可不止敲入commit命令这么简单，对于一个专业程序员 来说，我们还要关注每次commit所携带的背景信息，这里暂且称之为&#8220;commit context&#8221;。在每次commit时，这些上下文信息只能通过commit log来体现。 一、Commit Context 今日的软件复杂度日益增加，软件开发模式也早已从单打独斗的英雄模式变成了团队协作模式了，而在团队模式下，版本控制系统发挥着至关重要的作用， 它让开发过程变得有序，将冲突解决的成本尽可能地降低到最低。但版本控制系统毕竟不是智能的，它只是机械地记录着每次提交前后的内容的raw差 异，至于这个差异究竟代表了什么，版本管理系统是不得而知的，这就需要我们开发者们来提供，这就算是产生commit context的动机吧。即便是一个人开发维护的项目，个人的记忆也是有时效性的，时间久了，以前的代码变更context势必也就淡忘了，良好且规范的 commit context有助于更好的维护项目，追踪历史思路和行为，甚至在查找bug时也是能帮得上大忙的，比如确认bug引入的时段边界、代码范围等。 前面说了，commit context最终是以commit log形式提供的，这才是我在这篇文章中真正要说的内容^_^。评价一个项目的好坏，无论是商业项目，还是开源项目，代码本身质量是一个重要的方面，代码 维护的规范性则是另外不可忽略的一个重要因素，而在代码维护规范性方面，commit log的规范是一项重要内容。做了这么多年Coding工作，到目前为止部门内部还没有哪一个项目在commit log规范方面是让我满意和欣赏的。另外本人在亲为commit log方面也是不能让自己满意的，这也是促使我思考commit log这块内容的一个初衷。 commit log承载着每次commit动作的context。一般来说context中至少要有一项内容，那就是此次代码变更的summary，这是最基本的要 求。如果你的commit log还是空着的，那你真该反思反思了，那是对自己和他人的不负责任。但无论是商业公司内部开发还是开源项目，commit context涉及到的因素往往不止一个，很多情况下commit context还与项目过程、质量保证流程以及项目使用的一些工具系统有 关联。我们来看两个知名开源项目的commit log样例吧。 [example1 - Linux Kernel] audit: catch possible NULL audit buffers It&#39;s possible for audit_log_start() to return NULL.&#160; Handle it in the various callers. Signed-off-by: Kees Cook [...]]]></description>
			<content:encoded><![CDATA[<p>在<a href="http://tonybai.com/2011/02/18/put-everything-under-version-control/">版本控制工具</a>大行其道的今天，作为程序员，势必要每天与各种版本控制系统（比如<a href="http://subversion.apache.org">Subversion</a>、<a href="http://git-scm.com">Git</a>、<a href="http://mercurial.selenic.com/‎">Mercurial</a>等）打交道， 每天不commit几次代码都不好意思说自己是专业程序员^_^。不过commit代码可不止敲入commit命令这么简单，对于一个专业程序员 来说，我们还要关注每次commit所携带的背景信息，这里暂且称之为&ldquo;commit context&rdquo;。在每次commit时，这些上下文信息只能通过commit log来体现。</p>
<p><b>一、Commit Context</b></p>
<p>今日的软件复杂度日益增加，软件开发模式也早已从单打独斗的英雄模式变成了团队协作模式了，而在团队模式下，版本控制系统发挥着至关重要的作用， 它让开发过程变得有序，将冲突解决的成本尽可能地降低到最低。但版本控制系统毕竟不是智能的，它只是机械地记录着每次提交前后的内容的raw差 异，至于这个差异究竟代表了什么，版本管理系统是不得而知的，这就需要我们开发者们来提供，这就算是产生commit context的动机吧。即便是一个人开发维护的项目，个人的记忆也是有时效性的，时间久了，以前的代码变更context势必也就淡忘了，良好且规范的 commit context有助于更好的维护项目，追踪历史思路和行为，甚至在查找bug时也是能帮得上大忙的，比如确认bug引入的时段边界、代码范围等。</p>
<p>前面说了，commit context最终是以commit log形式提供的，这才是我在这篇文章中真正要说的内容^_^。评价一个项目的好坏，无论是商业项目，还是开源项目，代码本身质量是一个重要的方面，代码 维护的规范性则是另外不可忽略的一个重要因素，而在代码维护规范性方面，commit log的规范是一项重要内容。做了这么多年Coding工作，到目前为止部门内部还没有哪一个项目在commit log规范方面是让我满意和欣赏的。另外本人在亲为commit log方面也是不能让自己满意的，这也是促使我思考commit log这块内容的一个初衷。</p>
<p>commit log承载着每次commit动作的context。一般来说context中至少要有一项内容，那就是此次代码变更的summary，这是最基本的要 求。如果你的commit log还是空着的，那你真该反思反思了，那是对自己和他人的不负责任。但无论是商业公司内部开发还是开源项目，commit context涉及到的因素往往不止一个，很多情况下commit context还与<b>项目过程、质量保证流程以及项目使用的一些工具系统</b>有 关联。我们来看两个知名开源项目的commit log样例吧。</p>
<p><i>[example1 - Linux Kernel]</i></p>
<p><font face="Courier New">audit: catch possible NULL audit buffers<br />
	It&#39;s possible for audit_log_start() to return NULL.&nbsp; Handle it in the<br />
	various callers.</font></p>
<p><font face="Courier New">Signed-off-by: Kees Cook <a class="moz-txt-link-rfc2396E" href="mailto:keescook@chromium.org">&lt;keescook@chromium.org&gt;</a><br />
	Cc: Al Viro <a class="moz-txt-link-rfc2396E" href="mailto:viro@zeniv.linux.org.uk">&lt;viro@zeniv.linux.org.uk&gt;</a><br />
	Cc: Eric Paris <a class="moz-txt-link-rfc2396E" href="mailto:eparis@redhat.com">&lt;eparis@redhat.com&gt;</a><br />
	Cc: Jeff Layton <a class="moz-txt-link-rfc2396E" href="mailto:jlayton@redhat.com">&lt;jlayton@redhat.com&gt;</a><br />
	Cc: &quot;Eric W. Biederman&quot; <a class="moz-txt-link-rfc2396E" href="mailto:ebiederm@xmission.com">&lt;ebiederm@xmission.com&gt;</a><br />
	Cc: Julien Tinnes <a class="moz-txt-link-rfc2396E" href="mailto:jln@google.com">&lt;jln@google.com&gt;</a><br />
	Cc: Will Drewry <a class="moz-txt-link-rfc2396E" href="mailto:wad@google.com">&lt;wad@google.com&gt;</a><br />
	Cc: Steve Grubb <a class="moz-txt-link-rfc2396E" href="mailto:sgrubb@redhat.com">&lt;sgrubb@redhat.com&gt;</a><br />
	Cc: Andrea Arcangeli <a class="moz-txt-link-rfc2396E" href="mailto:aarcange@redhat.com">&lt;aarcange@redhat.com&gt;</a><br />
	Signed-off-by: Andrew Morton <a class="moz-txt-link-rfc2396E" href="mailto:akpm@linux-foundation.org">&lt;akpm@linux-foundation.org&gt;</a><br />
	Signed-off-by: Linus Torvalds <a class="moz-txt-link-rfc2396E" href="mailto:torvalds@linux-foundation.org">&lt;torvalds@linux-foundation.org&gt;</a></font></p>
<p>这是<a href="https://www.kernel.org">Linux Kernel</a>项目的一个commit log的内容。从这个log携带的context信息来看，我们能够清楚地了解如下一些内容：</p>
<p>- 修改的内核模块范围audit<br />
	- 修改的原因summary: to catch possible NULL audit buffers<br />
	- 这个patch从诞生到被merge到trunk过程中涉及到的相关的人员列表<br />
	- 这个patch由Who sign-off的。</p>
<p>将mail list放入到commit log中，这是Linux Kernel开发过程规范所要求的，同样也是质量保证的一个方法。在《<a href="http://tonybai.com/2012/03/27/how-to-participate-linux-community-section-1/">如何加入Linux内核开发社区</a>》系列文章中你可以了解到一些有关Linux Kernel开发过程的内容。从这个例子中我们主要可以看出commit context与Project过程、质量保证链条方面的相关性。</p>
<p><i>[example2 - Apache Subversion]</i></p>
<p><font face="Courier New">Fix issue #3498 &#8211; Subversion password stores freeze Eclipse</font></p>
<p><font face="Courier New">* subversion/libsvn_auth_gnome_keyring/gnome_keyring.c<br />
	&nbsp; (simple_gnome_keyring_first_creds, simple_gnome_keyring_save_creds,<br />
	&nbsp;&nbsp; ssl_client_cert_pw_gnome_keyring_first_creds,<br />
	&nbsp;&nbsp; ssl_client_cert_pw_gnome_keyring_save_creds): If the keyring is locked<br />
	&nbsp;&nbsp;&nbsp; and we are in interactive mode but have no unlock prompt function, don&#39;t<br />
	&nbsp;&nbsp;&nbsp; throw a &quot;GNOME Keyring is locked and we are non-interactive&quot; error;<br />
	&nbsp;&nbsp;&nbsp; instead, continue without unlocking it, so that the unlocking may be<br />
	&nbsp;&nbsp;&nbsp; handled by the default GNOME Keyring unlock dialog box.</font></p>
<p>这是Apache Subversion项目的一个commit log的内容。同样从这个log携带的context信息来看，我们能够清楚地了解如下一些内容：</p>
<p>- 修改的代码范围subversion/libsvn_auth_gnome_keyring/gnome_keyring.c，包括括号中的函数名列表， 这个显然更为细致。<br />
	- 修改的原因summary: <font face="Courier New">Fix issue #3498 &#8211; Subversion password stores freeze Eclipse</font><br />
	- 这个patch与问题跟踪系统的关联性 -<font face="Courier New">issue #3498</font>。</p>
<p>通过这个commit log，我们可以快速找到此patch对应的问题跟踪系统中的条目#3498，这样可以查看到一些更为细致的context信息。从这个例子我们主要能够 看出commit context与项目所使用的一些工具系统的关联。</p>
<p>综合以上可以看出良好的commit log是可以清楚全面反映commit context的。这里的&ldquo;全面&rdquo;是project-dependent的，是需要能够体现出涉及project的一切必要信息的：过程的、质量的、工具 的。</p>
<p><b>二、Commit log格式</b></p>
<p>Commit log没有放之四海而皆准的统一格式，而是project-dependent的。就我个人而言，我会在下面的几个问题上有纠结。</p>
<p><b><i>* 语言</i></b></p>
<p>不得不承认在创造编程语言方面，西方文化占了主导，语言中的关键字也多取自英语。虽然目前主流的语言以及新兴的语言都号称源码原生支持utf8或 unicode其他字符集格式，但却是很少见到在源文件中使用非英语命名变量或函数的，这也影响了我在commit log中对语言的选择 &#8211; 我基本上都是用英文编写commit log的。目前主流的版本控制工具都是支持unicode字符集的，你用中文提交也是没有任何问题的，尤其是在国内商业项目中，使用中文描述起来，理解上快且歧义少。我是不反对用中文写commit log的，但反感的是中英文混合写commit log（有些人用中文，有些人用英文）。每当批量看commit log时，中英文混在一起，一点美感都没有了。</p>
<p>commit log不是给最终用户看的，而是给开发维护人员看的。因此选择语言种类时要看这种语言是否能给开发维护人员的工作带来便利，精确全面地传达context。即便 应用是要发布给非洲人民，但若开发人员都是中国人，一样可以用中文编写commit log。</p>
<p><b><i>* 地道</i></b></p>
<p>说到&ldquo;地道&rdquo;，主要是针对你选择<b>外语</b>（大多数情况是英语）作为你commit log的承载语言时。就像生活在国外要用外国人熟悉的语言习惯与人交流似的，我们在用英语编写commit log时也要学会选用&ldquo;地道&rdquo;的词汇，远离<a href="http://en.wikipedia.org/wiki/Chinglish">Chinglish</a>。当然想立即做到&ldquo;地道&rdquo;也不是那么容易，毕竟我们一直以来就按照Chinglish的思维去学 习英语的，一个比较好的方式就是多看看知名开源项目（比如linux kernel）的commit log，看看人家是如何选择词汇和组织句子的。其实Commit log中用到的词汇和句型很少，看多了也就找猫画虎的学会了。</p>
<p><b><i>* 规范</i></b></p>
<p>&ldquo;没有规矩，不成方圆&rdquo;，无论是商业软件项目，还是大型开源项目，莫不如此。如果要想很好的传达commit context，一个设计规范，内容全面的commit log格式是必不可少的。我们无需从头做起，很多开源项目在这方面都已经有一些良好的实践，比如上面提到的linux kernel的commit log convention，再比如这里有Apache Subversion的<a href="http://subversion.apache.org/docs/community-guide/conventions.html#coding-style">Commit log要求</a>。TYPO3和FLOW3也有自己详细的<a href="http://wiki.typo3.org/CommitMessage_Format_(Git)">Commit log说明</a>。</p>
<p>制定规范时总体来说，注意以下几点：<br />
	&#8211; 格式简明扼要，只保留必要的项；<br />
	&#8211; 注意与项目过程、质量保证流程的结合，以及与第三方工具的关联（注意序号或ID的唯一性）；<br />
	&#8211; 对于规模较大的系统，可以考虑在log中体现影响的涉及的&ldquo;子模块&rdquo;或&ldquo;子目录&rdquo;名字或者逻辑功能的名字（比如前面linux kernel例子中的audit），这样便于快速定位本地commit的影响范畴。</p>
<p><b>三、Commit模板</b></p>
<p>如果像linux kernel或subversion那样涉及到过程、质量控制以及第三方工具的集成（比如问题跟踪系统、代码评审系统等）时，建议设置Commit log template(模板)以简化开发者commit log编写的工作。</p>
<p><b><i>* Subversion命令行客户端支持commit log模板</i></b></p>
<p>Subversion在命令行客户端侧暂无对模板的支持。不过可以通过一些trick模拟实现这个功能：</p>
<p>- 创建commit log模板log.tmpl，放在特定目录下，本例中放在用户的$HOME目录下<br />
	- 添加并导出环境变量SVN_EDITOR<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <font face="Courier New">export SVN_EDITOR=&quot;rm svn-commit.tmp &amp;&amp; cp ~/log.tmpl svn-commit.tmp &amp;&amp; vi &quot;</font></p>
<p>svn commit时，svn客户端会在当前路径下会执行类似$SVN_EDITOR svn-commit.tmp的命令，而svn-commit.tmp文件已经被替换为我们的模板文件，开发者只需按模板填写内容，并保存退出即可。如果 commit成功，svn客户端会删除当前目录下的svn-commit.tmp，否则svn-commit.tmp不会被删除，这将导致下次再提交 时，svn客户端检测到svn-commit.tmp的存在，从而新建立一个svn-commit.2.tmp的新文件，导致模板失效，这也是这个方法的 一个瑕疵。</p>
<p><b><i>* Git命令行支持commit log模板</i></b></p>
<p>Git是目前very hot的分布式版本管理工具，起步晚，但起点高，因此已经内置了对模板的支持，只需将模板文件配置一下即可。<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<font face="Courier New"> git config &#8211;global commit.template ~/log.tmpl</font></p>
<p><b>四、良好格式commit log</b><b>的实施</b></p>
<p>即便有了良好格式的commit log的模板定义，但就我经验而言，实施起来也还会遇到诸多问题。commit行为是客户端发起的，要让所有开发者都能很好的使用模板并主动按模板提交需 要一些流程以及工具支持。比如在server段部署<a href="http://tonybai.com/2010/08/07/use-svn-pre-commit-hook/">pre-commit hook</a>，对提交的log格式进行检查，不符合模板格式的予以拒绝等。</p>
<p>对于与问题跟踪系统有关联的log格式，还要注意保持问题跟踪系统id或序号的唯一性，这显然是管理和过程方面的工作。</p>
<p>对于开源项目，一般merge到trunk需要owner的检查，所以反倒实施起来容易了些，只要有一篇内容丰富的 developer/community guide或convention之类的文档即可，多数知名的opensource project(比如linux kernel、subversion、apache httpd server、python等)都是有这类文档的，为这些project提交patch前是要好好阅读这些文档的，不能坏了规矩^_^。&nbsp; &nbsp; &nbsp;<br />
	&nbsp;</p>
<p style='text-align:left'>&copy; 2013, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2013/05/09/also-talk-about-commit-log/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>SVN命令输出结果的语言选择</title>
		<link>https://tonybai.com/2013/03/15/choose-lang-for-svn-cmd-output/</link>
		<comments>https://tonybai.com/2013/03/15/choose-lang-for-svn-cmd-output/#comments</comments>
		<pubDate>Fri, 15 Mar 2013 09:08:19 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[Coding-Review]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[patch]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[ReviewBoard]]></category>
		<category><![CDATA[Subversion]]></category>
		<category><![CDATA[svn]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[Windows]]></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=1218</guid>
		<description><![CDATA[今天一位网上的朋友在使用reviewboard时遇到了问题，我们在评论中探讨了一下。他的问题目前已经定位，大致是这样的：他在Windows上用svn diff生成的patch文件在提交给reviewboard时出错，但在linux上生成的patch文件是没有问题的。后来他发现这两个patch文件内容稍有区别：Windows上的patch文件中的diff结果包含中文，比如&#8220;版本 10&#8221;；而在linux下生成的那份patch文件中，&#34;版本 10&#34;变成了&#34;revision 10&#34;。reviewboard拒绝了带中文的那份patch，估计是reviewboard的字符编码设置让其无法识别windows下的那个字符集。 多数情况下，我们根本无需关心svn命令输出中到底是英文还是中文。subversion对国际化支持到很好，它会根据自己所在环境下的区域和语言设置来选择到底输出哪种文字，对不同地区说不同语言的程序员来说，这绝对是一个好事。 但问题毕竟是出现了。我们该如何解决呢？我们该如何选择svn输出的语言呢？我不用Windows，所以这里我说说Linux下的设置方法，这也是今天在思考那位朋友的问题时才找到的方法。 方法的关键就在于前面说过的Subversion会自动检测你的区域和语言环境设置。以我的Ubuntu 12.04LTS为例，执行locale命令，可以看到以下输出： LANG=zh_CN.UTF-8 LANGUAGE=zh_CN:zh LC_CTYPE=&#34;zh_CN.UTF-8&#34; LC_NUMERIC=&#34;zh_CN.UTF-8&#34; LC_TIME=&#34;zh_CN.UTF-8&#34; LC_COLLATE=&#34;zh_CN.UTF-8&#34; LC_MONETARY=&#34;zh_CN.UTF-8&#34; LC_MESSAGES=&#34;zh_CN.UTF-8&#34; LC_PAPER=&#34;zh_CN.UTF-8&#34; LC_NAME=&#34;zh_CN.UTF-8&#34; LC_ADDRESS=&#34;zh_CN.UTF-8&#34; LC_TELEPHONE=&#34;zh_CN.UTF-8&#34; LC_MEASUREMENT=&#34;zh_CN.UTF-8&#34; LC_IDENTIFICATION=&#34;zh_CN.UTF-8&#34; LC_ALL= 也就是说默认情况下，我的区域是CN，语言是zh。在这种环境下svn命令的输出都是包含中文的，比如下面这段输出： 路径: . URL: https://lcut.googlecode.com/svn/trunk 版本库根: https://lcut.googlecode.com/svn 版本库 UUID: 22405a7c-d843-be82-cc3b-46f1d7cb9705 版本: 57 节点种类: 目录 调度: 正常 最后修改的作者: bigwhite.cn@gmail.com 最后修改的版本: 57 我尝试修改locale。先将LC_ALL修改为en_US.UTF-8（通过locale -a你可以查看系统支持的locale列表，从中能看到en_US.utf8）。修改后(export LC_ALL=en_US.utf8)，执行locale，发现除了LANGUAGE和LANG还是原值外，其余变量都已经改为en_US.utf8了。不过svn info的输出结果依旧包含中文。 看来LANGUAGE或LANG两个变量中的一个会影响到svn的输出结果。先修改LANG为en_US.utf8，执行svn info，发现结果依旧包含中文。再试试修改LANGUAGE，export LANGUAGE=en_US.en（注意不是en_US.utf8，LANGUAGE变量的值与其他的变量稍有不同）。再执行svn info，这回终于等到英文结果输出了： Path: . URL: [...]]]></description>
			<content:encoded><![CDATA[<p>今天一位网上的朋友在使用<a href="http://tonybai.com/2009/09/19/review-board-installation-and-configuration/">reviewboard</a>时遇到了问题，我们在评论中探讨了一下。他的问题目前已经定位，大致是这样的：他在Windows上用svn diff生成的patch文件在提交给<a href="http://www.reviewboard.org">reviewboard</a>时出错，但在linux上生成的patch文件是没有问题的。后来他发现这两个patch文件内容稍有区别：Windows上的patch文件中的diff结果包含中文，比如&ldquo;版本 10&rdquo;；而在linux下生成的那份patch文件中，&quot;版本 10&quot;变成了&quot;revision 10&quot;。reviewboard拒绝了带中文的那份patch，估计是reviewboard的字符编码设置让其无法识别windows下的那个字符集。</p>
<p>多数情况下，我们根本无需关心svn命令输出中到底是英文还是中文。<a href="http://tonybai.com/2011/03/23/also-talk-about-solving-the-svn-conflicts/">subversion</a>对国际化支持到很好，它会根据自己所在环境下的区域和语言设置来选择到底输出哪种文字，对不同地区说不同语言的程序员来说，这绝对是一个好事。</p>
<p>但问题毕竟是出现了。我们该如何解决呢？我们该如何选择svn输出的语言呢？我不用Windows，所以这里我说说Linux下的设置方法，这也是今天在思考那位朋友的问题时才找到的方法。</p>
<p>方法的关键就在于前面说过的Subversion会自动检测你的区域和语言环境设置。以我的<a href="http://tonybai.com/2012/12/04/upgrade-ubuntu-to-1204-lts/">Ubuntu 12.04LTS</a>为例，执行locale命令，可以看到以下输出：</p>
<p><span style="font-family:courier new,courier,monospace;">LANG=zh_CN.UTF-8<br />
	LANGUAGE=zh_CN:zh<br />
	LC_CTYPE=&quot;zh_CN.UTF-8&quot;<br />
	LC_NUMERIC=&quot;zh_CN.UTF-8&quot;<br />
	LC_TIME=&quot;zh_CN.UTF-8&quot;<br />
	LC_COLLATE=&quot;zh_CN.UTF-8&quot;<br />
	LC_MONETARY=&quot;zh_CN.UTF-8&quot;<br />
	LC_MESSAGES=&quot;zh_CN.UTF-8&quot;<br />
	LC_PAPER=&quot;zh_CN.UTF-8&quot;<br />
	LC_NAME=&quot;zh_CN.UTF-8&quot;<br />
	LC_ADDRESS=&quot;zh_CN.UTF-8&quot;<br />
	LC_TELEPHONE=&quot;zh_CN.UTF-8&quot;<br />
	LC_MEASUREMENT=&quot;zh_CN.UTF-8&quot;<br />
	LC_IDENTIFICATION=&quot;zh_CN.UTF-8&quot;<br />
	LC_ALL=</span></p>
<p>也就是说默认情况下，我的区域是CN，语言是zh。在这种环境下svn命令的输出都是包含中文的，比如下面这段输出：</p>
<p><span style="font-family:courier new,courier,monospace;">路径: .<br />
	URL: https://lcut.googlecode.com/svn/trunk<br />
	版本库根: https://lcut.googlecode.com/svn<br />
	版本库 UUID: 22405a7c-d843-be82-cc3b-46f1d7cb9705<br />
	版本: 57<br />
	节点种类: 目录<br />
	调度: 正常<br />
	最后修改的作者: bigwhite.cn@gmail.com<br />
	最后修改的版本: 57</span></p>
<p>我尝试修改locale。先将LC_ALL修改为en_US.UTF-8（通过locale -a你可以查看系统支持的locale列表，从中能看到en_US.utf8）。修改后(export LC_ALL=en_US.utf8)，执行locale，发现除了LANGUAGE和LANG还是原值外，其余变量都已经改为en_US.utf8了。不过svn info的输出结果依旧包含中文。</p>
<p>看来LANGUAGE或LANG两个变量中的一个会影响到svn的输出结果。先修改LANG为en_US.utf8，执行svn info，发现结果依旧包含中文。再试试修改LANGUAGE，export LANGUAGE=en_US.en（注意不是en_US.utf8，LANGUAGE变量的值与其他的变量稍有不同）。再执行svn info，这回终于等到英文结果输出了：</p>
<p><span style="font-family:courier new,courier,monospace;">Path: .<br />
	URL: https://lcut.googlecode.com/svn/trunk<br />
	Repository Root: https://lcut.googlecode.com/svn<br />
	Repository UUID: 22405a7c-d843-be82-cc3b-46f1d7cb9705<br />
	Revision: 57<br />
	Node Kind: directory<br />
	Schedule: normal<br />
	Last Changed Author: bigwhite.cn@gmail.com<br />
	Last Changed Rev: 57</span></p>
<p>目前还不清楚这招在Windows下是否也生效，记得Windows上也有设置环境变量的地方。</p>
<p style='text-align:left'>&copy; 2013, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2013/03/15/choose-lang-for-svn-cmd-output/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>也谈Go语言代码包分发</title>
		<link>https://tonybai.com/2012/10/25/go-package-distributing/</link>
		<comments>https://tonybai.com/2012/10/25/go-package-distributing/#comments</comments>
		<pubDate>Thu, 25 Oct 2012 14:32:55 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[bitbucket]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[launchpad]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[svn]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[版本控制]]></category>
		<category><![CDATA[程序员]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1068</guid>
		<description><![CDATA[Go语言目前(截至1.0.2版本)尚不支持直接链接.a文件(这里的.a文件指的不是传统静态共享库，而是对golang的非main包build后的产物)。这样一来Go的第三方库包或组织内部的公共代码库包只能以源码的形式分发了。 Go提供了get命令用于获取他人分发的代码包。我们通过get命令既可以获取一些知名代码托管站点上的代码，也可以获取组织内部版本控制服务器上的公共代码。 Go get支持的托管站点包括github、google code、BitBucket以及Launchpad，针对这类情况，我们可以得到&#8220;特殊&#8221;语法的照顾： go get github.com/bmizerany/assert go get bitbucket.org/bmizerany/assert go get code.google.com/p/assert go get launchpad.net/assert 由于Go已经&#8220;内置&#8221;了github、google code等的版本控制工具类型，因此我们无需再做任何额外指定，只需用代码的url（去掉http://）即可。 执行get后，代码会被下载到GOPATH环境变量配置中的第一个路径下的src目录下面。例如：我们的GOPATH=/home/tonybai /goworkspace1:/home/tonybai/goworkspace2，执行go get github.com/bmizerany/assert后，我们将在/home/tonybai/goworkspace1下看到github.com 目录，而assert包在本地的完整路径就是/home/tonybai/goworkspace1/github.com/bmizerany /assert。这样我们在代码中直接import &#34;github.com/bmizerany/assert&#34;即可使用assert这个第三方包了。 在组织内部我们也会有自己的私有公共代码库，一份代码库可能被多个项目所使用。在每个项目中都保存一份公共库代码显然是不利于后续版本升级维护的，这样就需要各个项目统一从同一个地方获取或更新公共库代码。这种情况我们同样可以用go get命令来做。 假设内部使用subversion作为版本控制工具，公共库架设在10.10.12.13/svn0/share/golib。这时我们不能简单地的通 过&#34;go get 10.10.12.13/svn0/share/golib&#34;来获取到代码，我们需要告诉get我们采用哪种版本控制工具，而这种信息的传递是通过在库名称后面加上后缀的方式进行的。比如： go get &#34;10.10.12.13/svn0/share/golib.svn&#34; 这样在/home/tonybai/goworkspace1下就会出现10.10.12.13/svn0/share/golib.svn目录结构。我 们在代码中可以直接import对应的包，比如import &#34;10.10.12.13/svn0/share/golib.svn/assert&#34;。 通过对get命令特性的了解，我们也可以确定分发的代码包到底应该如何组织。从上面的例子我们可以看出我们分发的代码包结构不需很复杂，直接在库的 repository下建立包目录即可，比如上面例子中库repository为golib，assert就是直接建立在下面的目录，同时也是包名。 go get可自动识别http_proxy环境变量，这样Go也可以通过代理获取外部代码包。 使用外部代码包的项目可以通过go get -u url来更新代码包版本为最新版本。 &#169; 2012, bigwhite. 版权所有.]]></description>
			<content:encoded><![CDATA[<p><a href="http://golang.org">Go语言</a>目前(截至1.0.2版本)尚不支持直接链接.a文件(这里的.a文件指的不是传统静态共享库，而是对golang的非main包build后的产物)。这样一来Go的第三方库包或组织内部的公共代码库包只能以源码的形式分发了。</p>
<p>	Go提供了get命令用于获取他人分发的代码包。我们通过get命令既可以获取一些知名代码托管站点上的代码，也可以获取组织内部版本控制服务器上的公共代码。</p>
<p>	Go get支持的托管站点包括github、google code、BitBucket以及Launchpad，针对这类情况，我们可以得到&ldquo;特殊&rdquo;语法的照顾：</p>
<p>	go get github.com/bmizerany/assert<br />
	go get bitbucket.org/bmizerany/assert<br />
	go get code.google.com/p/assert<br />
	go get launchpad.net/assert</p>
<p>	由于Go已经&ldquo;内置&rdquo;了github、google code等的版本控制工具类型，因此我们无需再做任何额外指定，只需用代码的url（去掉http://）即可。</p>
<p>	执行get后，代码会被下载到GOPATH环境变量配置中的第一个路径下的src目录下面。例如：我们的GOPATH=/home/tonybai /goworkspace1:/home/tonybai/goworkspace2，执行go get github.com/bmizerany/assert后，我们将在/home/tonybai/goworkspace1下看到github.com 目录，而assert包在本地的完整路径就是/home/tonybai/goworkspace1/github.com/bmizerany /assert。这样我们在代码中直接import &quot;github.com/bmizerany/assert&quot;即可使用assert这个第三方包了。</p>
<p>	在组织内部我们也会有自己的私有公共代码库，一份代码库可能被多个项目所使用。在每个项目中都保存一份公共库代码显然是不利于后续版本升级维护的，这样就需要各个项目统一从同一个地方获取或更新公共库代码。这种情况我们同样可以用go get命令来做。</p>
<p>	假设内部使用subversion作为版本控制工具，公共库架设在10.10.12.13/svn0/share/golib。这时我们不能简单地的通 过&quot;go get 10.10.12.13/svn0/share/golib&quot;来获取到代码，我们需要告诉get我们采用哪种版本控制工具，而这种信息的传递是通过在库名称后面加上后缀的方式进行的。比如：</p>
<p>	go get &quot;10.10.12.13/svn0/share/golib.svn&quot;</p>
<p>	这样在/home/tonybai/goworkspace1下就会出现10.10.12.13/svn0/share/golib.svn目录结构。我 们在代码中可以直接import对应的包，比如import &quot;10.10.12.13/svn0/share/golib.svn/assert&quot;。</p>
<p>	通过对get命令特性的了解，我们也可以确定分发的代码包到底应该如何组织。从上面的例子我们可以看出我们分发的代码包结构不需很复杂，直接在库的 repository下建立包目录即可，比如上面例子中库repository为golib，assert就是直接建立在下面的目录，同时也是包名。</p>
<p>	go get可自动识别http_proxy环境变量，这样Go也可以通过代理获取外部代码包。</p>
<p>	使用外部代码包的项目可以通过go get -u url来更新代码包版本为最新版本。</p>
<p style='text-align:left'>&copy; 2012, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2012/10/25/go-package-distributing/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
	</channel>
</rss>
