<?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; Subversion</title>
	<atom:link href="http://tonybai.com/tag/subversion/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Wed, 15 Apr 2026 23:35:12 +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 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>图解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>godep的一个“坑”</title>
		<link>https://tonybai.com/2014/10/30/a-hole-of-godep/</link>
		<comments>https://tonybai.com/2014/10/30/a-hole-of-godep/#comments</comments>
		<pubDate>Thu, 30 Oct 2014 14:38:09 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Buildc]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[godep]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[GOPATH]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Subversion]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[构建]]></category>
		<category><![CDATA[编译]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1595</guid>
		<description><![CDATA[很多人学习和使用Golang一段时间后，都会被golang的第三方包依赖版本搞得有些烦躁，golang设计者最初过于乐观的设计使得今天大 家不得不各自想办法解决这个问题。godep就是综合了多年第三方包依赖问题的解决方案后的一个趋向统一的方案，至少是在go get的设计没有进化前的一个比较不错的方案。 今天试用了一把godep，不过&#8220;体验&#8221;并不理想，这缘于我遇到了godep的一个&#8220;坑&#8221;，不过是那种你在正式项目中不一定遇到的&#8220;坑&#8221;，这里来说到说到。 按照godep官方使用说明的第一步，先下载godep： $ go get github.com/tools/godep $godep Godep is a tool for managing Go package dependencies. Usage: &#160;&#160;&#160; godep command [arguments] The commands are: &#160;&#160;&#160; save&#160;&#160;&#160;&#160; list and copy dependencies into Godeps &#160;&#160;&#160; go&#160;&#160;&#160;&#160;&#160;&#160; run the go tool in a sandbox &#160;&#160;&#160; get&#160;&#160;&#160;&#160;&#160; download and install packages with specified dependencies &#160;&#160;&#160; path&#160;&#160;&#160;&#160; [...]]]></description>
			<content:encoded><![CDATA[<p>很多人学习和使用<a href="http://tonybai.com/tag/golang">Golang</a>一段时间后，都会被<a href="http://golang.org">golang</a>的第三方包依赖版本搞得有些烦躁，golang设计者最初过于乐观的设计使得今天大 家不得不各自想办法解决这个问题。<a href="https://github.com/tools/godep">godep</a>就是综合了多年第三方包依赖问题的解决方案后的一个趋向统一的方案，至少是在go get的设计没有进化前的一个比较不错的方案。</p>
<p>今天试用了一把godep，不过&ldquo;体验&rdquo;并不理想，这缘于我遇到了godep的一个&ldquo;坑&rdquo;，不过是那种你在正式项目中不一定遇到的&ldquo;坑&rdquo;，这里来说到说到。</p>
<p>按照godep官方使用说明的第一步，先下载godep：</p>
<p><font face="Courier New">$ go get github.com/tools/godep</font><br />
	<font face="Courier New">$godep<br />
	Godep is a tool for managing Go package dependencies.</font></p>
<p><font face="Courier New">Usage:</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; godep command [arguments]</font></p>
<p><font face="Courier New">The commands are:</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; save&nbsp;&nbsp;&nbsp;&nbsp; list and copy dependencies into Godeps<br />
	&nbsp;&nbsp;&nbsp; go&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; run the go tool in a sandbox<br />
	&nbsp;&nbsp;&nbsp; get&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; download and install packages with specified dependencies<br />
	&nbsp;&nbsp;&nbsp; path&nbsp;&nbsp;&nbsp;&nbsp; print sandbox path for use in a GOPATH<br />
	&nbsp;&nbsp;&nbsp; restore&nbsp; check out listed dependency versions in GOPATH<br />
	&nbsp;&nbsp;&nbsp; update&nbsp;&nbsp; use different revision of selected packages</font></p>
<p><font face="Courier New">Use &quot;godep help [command]&quot; for more information about a command.</font></p>
<p>确认正确下载后，我们来准备一个测试例子，目录如下：</p>
<p><font face="Courier New">$GOPATH/<br />
	&nbsp;&nbsp;&nbsp; src/<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; tonybai.com/<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; foolib/<br />
	&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; foo.go<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fooapp/<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp; &nbsp; &nbsp;&nbsp; main.go</font>&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;<br />
	&nbsp;&nbsp;&nbsp;<br />
	<font face="Courier New">//foo.go<br />
	package foo</font></p>
<p><font face="Courier New">func Add(a, b int) int {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return a + b<br />
	}</font></p>
<p><font face="Courier New">//main.go<br />
	package main</font></p>
<p><font face="Courier New">import (<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;fmt&quot;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; foo &quot;tonybai.com/foolib&quot;<br />
	)</font></p>
<p><font face="Courier New">func main() {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fmt.Println(foo.Add(1, 3))<br />
	}</font></p>
<p>在<font face="Courier New">fooapp</font>下，编译执行程序：</p>
<p><font face="Courier New">$go run main.go<br />
	4</font></p>
<p>接下来godep登场，根据godep文档中得步骤，接下来我们应该在一个构建依赖关系完整的项目中执行godep save以保存依赖关系以及依赖的当前版本第三方包：</p>
<p><font face="Courier New">$godep save<br />
	godep: directory &quot;/Users/tony/Test/GoToolsProjects/src&quot; is not using a known version control system<br />
	godep: error loading dependencies</font></p>
<p>出错了！godep提示$GOPATH/src目录没有使用任何版本控制系统(<font face="Courier New">not using a known version control system</font>)。 奇怪啊！这个错误什么意思呢？难道使用godep还需要将$GOPATH/src整体作为一个Project纳入git or subversion repository中？无奈之下，我只能先这么做，再作观察。我在$GOPATH下执行git init，建立一个local git repository，然后将src add到这个repository中。</p>
<p>回到fooapp下，再次执行godep save，居然依旧是同样地错误结果。于是到godep的issues中去查，看看是否有人和我遇到了同样地问题！godep的<a href="https://github.com/tools/godep/issues/116">#116 issue</a>中提到的问题恰恰和我的一致，不过这个issue一 直是open状态，也没有人comments。接着翻看一下godep的源码，godep依赖一些第三方包，save这个命令在分析版本控制工具库时也是 调用了多层外部包实现的，短时间内无法定位问题。</p>
<p>静想一下，godep是管理第三方包依赖关系的，而第三方包多是go get下载的，是不是foolib要放到repository中才行呢？于是尝试在foolib中建立git repository并做一次commit。第三次在fooapp下执行godep save，错误依旧！</p>
<p>难道fooapp也必须放在repository中？试试吧。在fooapp下init一个git repository，将fooapp下的main.go提交到repository中。再执行godep save：</p>
<p><font face="Courier New">$godep save<br />
	$ls -l<br />
	total 8<br />
	drwxr-xr-x&nbsp; 5 tony&nbsp; staff&nbsp; 170 10 30 22:01 Godeps/<br />
	-rw-r&#8211;r&#8211;&nbsp; 1 tony&nbsp; staff&nbsp; 103 10 30 21:44 main.go</font></p>
<p>这回成功了！godep save在fooapp下建立了Godeps目录，其结构如下：</p>
<p><font face="Courier New">$ls -R<br />
	Godeps.json&nbsp;&nbsp;&nbsp; Readme&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; _workspace/</font></p>
<p><font face="Courier New">./_workspace:<br />
	src/</font></p>
<p><font face="Courier New">./_workspace/src:<br />
	tonybai.com/</font></p>
<p><font face="Courier New">./_workspace/src/tonybai.com:<br />
	foolib/</font></p>
<p><font face="Courier New">./_workspace/src/tonybai.com/foolib:<br />
	foolib.go</font></p>
<p>godep将当前版本的foolib copy到Godeps/_workspace下了。</p>
<p>Godeps.json记录了fooapp对foolib的依赖关系：</p>
<p><font face="Courier New">{<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;ImportPath&quot;: &quot;fooapp&quot;,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;GoVersion&quot;: &quot;go1.3&quot;,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;Deps&quot;: [<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;ImportPath&quot;: &quot;tonybai.com/foolib&quot;,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;Rev&quot;: &quot;<b>20a9c2a682537813d37847f2f270bf929672cc84</b>&quot;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ]<br />
	}</font></p>
<p>godep记录了foolib的当前revision number，这个number恰是我最新一次commit的hash code：</p>
<p><font face="Courier New">~/Test/GoToolsProjects/src/tonybai.com/foolib]$git log<br />
	commit <b>20a9c2a682537813d37847f2f270bf929672cc84</b><br />
	Author: Tony Bai &lt;bigwhite.cn@gmail.com&gt;<br />
	Date:&nbsp;&nbsp; Thu Oct 30 22:00:25 2014 +0800</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; init</font></p>
<p>到这里让我觉得godep的设计思路有些与我的<a href="http://github.com/bigwhite/buildc">buildc</a>（<a href="http://tonybai.com/2011/12/08/buildc-a-building-assistant-tool-for-c-app/">C程序辅助构建工具</a>）的思路有些类似，只是godep做得更彻底：</p>
<p>&nbsp;&nbsp;&nbsp; 1、godep将项目依赖统统放到项目的私有_workspace下，而buildc是共享的，通过project下的版本号配置区分依赖<br />
	&nbsp;&nbsp;&nbsp; 2、godep将依赖管理到revision(修订号)级别，buildc只是根据version来区分依赖。</p>
<p>godep的辅助构建原理（<font face="Courier New">godep go build main.go</font>）通过一条命令即可看出来：</p>
<p><font face="Courier New">$godep go env<br />
	GOARCH=&quot;amd64&quot;<br />
	GOBIN=&quot;/usr/local/go/bin&quot;<br />
	GOCHAR=&quot;6&quot;<br />
	GOEXE=&quot;&quot;<br />
	GOHOSTARCH=&quot;amd64&quot;<br />
	GOHOSTOS=&quot;darwin&quot;<br />
	GOOS=&quot;darwin&quot;<br />
	<strong>GOPATH=&quot;/Users/tony/Test/GoToolsProjects/src/fooapp/Godeps/_workspace:/Users/tony/Test/GoToolsProjects&quot;</strong></font></p>
<p>godep临时将_workspace放在GOPATH列表的前面，这样gc在编译时就会按顺序先在_workspace下面找依赖包，这样fooapp的私有依赖就会理所当然的被gc用到，即便在其他GOPATH路径下有同名包（可能是不同版本的）。</p>
<p>显然这也算是godep的一个小bug吧（或者是godep依赖的包的bug，目前不确认），毕竟提示的路径是不正确的，不应该提示<font face="Courier New">&quot;/Users/tony/Test/GoToolsProjects/src&quot; is not using a known version control system，而应该是</font><font face="Courier New">&quot;/Users/tony/Test/GoToolsProjects/src/tonybai.com/foolib或</font><font face="Courier New">&quot;/Users/tony/Test/GoToolsProjects/src/fooapp没有版本控制系统的repository留存。</font></p>
<p><font face="Courier New">另外觉得</font>godep的author应该把这个&ldquo;坑&rdquo;作为一个使用godep的前提进行说明，并在github主页给出明确展示，即便这个&ldquo;坑&rdquo;多数人可能不会遇到。</p>
<p style='text-align:left'>&copy; 2014, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2014/10/30/a-hole-of-godep/feed/</wfw:commentRss>
		<slash:comments>7</slash:comments>
		</item>
		<item>
		<title>只为那一抹释然</title>
		<link>https://tonybai.com/2013/12/26/just-for-being-relieved/</link>
		<comments>https://tonybai.com/2013/12/26/just-for-being-relieved/#comments</comments>
		<pubDate>Wed, 25 Dec 2013 23:18:30 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Coding-Review]]></category>
		<category><![CDATA[Commit-Log]]></category>
		<category><![CDATA[Jenkins]]></category>
		<category><![CDATA[KM]]></category>
		<category><![CDATA[LCUT]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Refactoring]]></category>
		<category><![CDATA[ReviewBoard]]></category>
		<category><![CDATA[Subversion]]></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">http://tonybai.com/?p=1461</guid>
		<description><![CDATA[一切没有目标的努力，都是瞎忙活儿。 &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; &#160;&#160;&#160; - Tony Bai 刚实施回来，就又投入到新工作中，到今天才有那么一点点时间写写这件事儿。 * 缘起 我们的遗留系统性能一直不高，导致这一局面的因素有很多，比如最初设计和实现的&#8220;考虑不足&#8221;、后续维护人员的&#8220;随波逐流&#8221;甚至缺少勇气对影响性能的关 键代码进行重构等等。技术债务就这样一直积累着。直到两年前，我们终见其导致的巨大的影响了。 由于客户方成本压缩，单节点性能低意味着需要更多的硬件投入，并连带着报价升高，导致我们的产品市场竞争力下降。而竞争对手产品的性能是我们的 3-5倍，这终于引起了领导的重视，并下达了开发高性能版本的任务命令。 * 抉择 遗留系统的问题有很多，性能差仅仅是表象之一。可维护性差更让人印象深刻。遗留系统就像一件打满补丁的旧衣裳，虽然依旧能穿着遮体御寒，但却让我 们时刻战战兢兢，生怕一个动作会导致它解体，变得支离破碎。 对于我们这样一个mission-critical的系统来说，开发周期显然是不会短的。在性能达标的同时，更为重要的是保证产品的质量，确保上 线后运行稳定。因此摆在我们面前有两条路： &#160;&#160;&#160; 1、在遗留系统上做&#8220;大修&#8221; &#8211; 大规模重构； &#160;&#160;&#160; 2、重写，把构成系统的骨架重新设计和实现，使它能够足够坚固，满足在&#8220;高速公路&#8221;上驰骋的要求。 我们最终选择了重写，也就是风险较大的那条路。在我们的理解中，重写软件就好比汽车升级平台，就像大众将传统的PQ25、PQ35等统统升级为 MQB平台那样。平台的升级，不光影响技术，还会影响方方面面，比如团队的能力、思维方式、合作模式以及团队过程改善等等。做 得好的话，会使整个团队迈上一个新台阶，这是原地修补所不能够带来的。 对于我个人来说，这也是我期望中的实验田，我将把之前研究的诸多实践落地，帮助团队提升能力。 自私地说，重写系统也是我的一个小理想，能遇到这样一个从无到有构建一个系统的机会是不多的，因此很是希望能看到一个系统一点一点的在自己的呵护 下&#8220;成长&#8221;起来。虽然我也清楚完成这样一个系统需要很长时间，而这期间我可能需要时刻紧绷着神经，直到系统正式上线后，才能感受到那一抹释然。 * 建立&#8220;骨架(skeleton)&#8221; 我们将项目分成两个阶段：建立系统&#8220;骨架&#8221;和为系统&#8220;添肉&#8221;，即添加业务逻辑。 系统的性能目标是原遗留系统的10倍，这样我们建立的骨架的性能至少要高于原遗留系统的10倍。在&#8220;添肉&#8221;之前我们要充分证明骨架的设计是合理、 有效、稳定和高性能的。 遗留系统性能低，并非因为当初的设计者能力有什么问题，更多是局限于当初的设计目标。系统初期业务量不大，接入的外部网元不多，因此系统大量使用 了链表这种简单但低效的数据结构；为了easy coding，当初的设计者选择了全局大锁；在客户端-服务器处理模型上，选择了一个连接一个进程的&#8220;高耗能&#8221;模式。最初这样的设计应对当年的业务量也是 绰绰有余的，但应付今天的业务规模就显得颇为捉襟见肘了，以至于我们不得不通过罗列机器来满足业务增长的状况。服务器增多，却导致了我们维护 和监控难度的增加。 为了应付现有业务量规模以及未来若干年的业务量增长，我们的新系统的骨架在设计时显然要扬长避短： &#160;&#160;&#160; &#8211; [...]]]></description>
			<content:encoded><![CDATA[<p><i>一切没有目标的努力，都是瞎忙活儿。</i><br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; <i>- Tony Bai</i></p>
<p>刚实施回来，就又投入到新工作中，到今天才有那么一点点时间写写这件事儿。</p>
<p><b>* 缘起</b></p>
<p>我们的遗留系统性能一直不高，导致这一局面的因素有很多，比如最初设计和实现的&ldquo;考虑不足&rdquo;、后续维护人员的&ldquo;随波逐流&rdquo;甚至缺少勇气对影响性能的关 键代码进行<a href="http://en.wikipedia.org/wiki/Code_refactoring">重构</a>等等。技术债务就这样一直积累着。直到两年前，我们终见其导致的巨大的影响了。</p>
<p>由于客户方成本压缩，单节点性能低意味着需要更多的硬件投入，并连带着报价升高，导致我们的产品市场竞争力下降。而竞争对手产品的性能是我们的 3-5倍，这终于引起了领导的重视，并下达了开发高性能版本的任务命令。</p>
<p><b>* 抉择</b></p>
<p>遗留系统的问题有很多，性能差仅仅是表象之一。可维护性差更让人印象深刻。遗留系统就像一件打满补丁的旧衣裳，虽然依旧能穿着遮体御寒，但却让我 们时刻战战兢兢，生怕一个动作会导致它解体，变得支离破碎。</p>
<p>对于我们这样一个<a href="http://en.wikipedia.org/wiki/Mission_critical‎">mission-critical</a>的系统来说，开发周期显然是不会短的。在性能达标的同时，更为重要的是保证产品的质量，确保上 线后运行稳定。因此摆在我们面前有两条路：<br />
	&nbsp;&nbsp;&nbsp; 1、在遗留系统上做&ldquo;大修&rdquo; &#8211; 大规模<a href="http://tonybai.com/2006/03/28/c-refactoring/">重构</a>；<br />
	&nbsp;&nbsp;&nbsp; 2、重写，把构成系统的骨架重新设计和实现，使它能够足够坚固，满足在&ldquo;高速公路&rdquo;上驰骋的要求。</p>
<p>我们最终选择了重写，也就是风险较大的那条路。在我们的理解中，重写软件就好比汽车升级平台，就像大众将传统的PQ25、PQ35等统统升级为 MQB平台那样。<b>平台的升级，不光影响技术，还会影响方方面面</b>，比如团队的能力、思维方式、合作模式以及团队过程改善等等。做 得好的话，会使整个团队迈上一个新台阶，这是原地修补所不能够带来的。</p>
<p>对于我个人来说，这也是我期望中的实验田，我将把之前研究的诸多实践落地，帮助团队提升能力。</p>
<p>自私地说，重写系统也是我的一个小理想，能遇到这样一个从无到有构建一个系统的机会是不多的，因此很是希望能看到一个系统一点一点的在自己的呵护 下&ldquo;成长&rdquo;起来。虽然我也清楚完成这样一个系统需要很长时间，而这期间我可能需要时刻紧绷着神经，直到系统正式上线后，才能感受到那一抹释然。</p>
<p><b>* 建立&ldquo;骨架(skeleton)&rdquo;</b></p>
<p>我们将项目分成两个阶段：建立系统&ldquo;骨架&rdquo;和为系统&ldquo;添肉&rdquo;，即添加业务逻辑。</p>
<p>系统的性能目标是原遗留系统的10倍，这样我们建立的骨架的性能至少要高于原遗留系统的10倍。在&ldquo;添肉&rdquo;之前我们要充分证明骨架的设计是合理、 有效、稳定和高性能的。</p>
<p>遗留系统性能低，并非因为当初的设计者能力有什么问题，更多是局限于当初的设计目标。系统初期业务量不大，接入的外部网元不多，因此系统大量使用 了链表这种简单但低效的数据结构；为了easy coding，当初的设计者选择了全局大锁；在客户端-服务器处理模型上，选择了一个连接一个进程的&ldquo;高耗能&rdquo;模式。最初这样的设计应对当年的业务量也是 绰绰有余的，但应付今天的业务规模就显得颇为捉襟见肘了，以至于我们不得不通过罗列机器来满足业务增长的状况。服务器增多，却导致了我们维护 和监控难度的增加。</p>
<p>为了应付现有业务量规模以及未来若干年的业务量增长，我们的新系统的骨架在设计时显然要扬长避短：<br />
	&nbsp;&nbsp;&nbsp; &#8211; 我们重新设计了通用的服务端框架和客户端框架，使得系统各个业务模块采用相同的通信处理机制；<br />
	&nbsp;&nbsp;&nbsp; &#8211; 我们没有选择线程，而是依旧采用成熟的进程（资源隔离式） + IO多路复用（linux下epoll机制）的服务器-客户端模型，与以往不同的是，我们在每个进程中处理多个链接，设定进程数量在合理水平，避免大量上下文切换带来的性能损耗；<br />
	&nbsp;&nbsp;&nbsp; &#8211; 将传统的全局big lock更换成了细粒度锁；<br />
	&nbsp;&nbsp;&nbsp; &#8211; 采用高效的数据结构和算法，比如用hash和array替代掉list等；<br />
	&nbsp;&nbsp;&nbsp; &#8211; 用简单队列替换掉原先复杂的队列调度结构，降低代码理解难度和后续维护门槛。<br />
	&nbsp;&nbsp;&nbsp; &#8211; &#8230; &#8230;</p>
<p>我们要求对骨架代码进行严格的<a href="http://tonybai.com/2005/11/08/the-design-and-implementation-of-c-unittest-framework/">单元测试</a>，通过<a href="http://tonybai.com/2010/09/30/opensource-a-lightweight-c-unit-test-framework/">lcut</a>为骨架代码建立起单元测试集，并结合持续集成对骨架代码进行持续的单元测试验证。</p>
<p>骨架完成后，我们对其进行了全面的压力测试，确保其性能水平达到我们设计要求，这是我们进入下一阶段的前提条件。</p>
<p><b>* 添肉(business logic)</b></p>
<p>有了稳定、可靠、高效的骨架，我们在&rdquo;添肉&ldquo;阶段就更加有信心了。用C写纯业务逻辑是苦逼了一些，但还好我们没有全部将以前遗留代码扔掉，我们为了保证功 能Feature不丢失，我们会尽量复用之前的业务逻辑，当然是&ldquo;规范地&rdquo;搬到新系统中的，尽可能地去除原有代码中的<a href="http://tonybai.com/2010/12/06/do-not-provide-soil-for-bad-smell-code/">Bad smell</a>。</p>
<p>与骨架相比，业务逻辑相对复杂，且耦合较多，因此对这些业务逻辑做单元测试真是一件让人头疼的事情。不过这也和我们最初的估计相符，最初制定的策略就是对骨架代码做高覆盖，对业务代码则宽松些，尽量覆盖即可。</p>
<p><b>* 附加实践</b></p>
<p>就像前面所说的那样，围绕着这次重写系统，我策划了很多实践有了落脚之地，包括：<br />
	&nbsp;&nbsp;&nbsp; &#8211; 试点<a href="http://tonybai.com/2011/11/23/those-things-about-knowledge-management/">知识管理</a> ：通过这次重写，建立起关于该系统的知识库；<br />
	&nbsp;&nbsp;&nbsp; &#8211; 增加基于<a href="http://tonybai.com/2011/03/04/some-experience-on-using-review-board/">ReviewBoard</a>的<a href="http://tonybai.com/2010/12/18/thoughts-on-online-coding-review/">在线代码评审</a>环节；<br />
	&nbsp;&nbsp;&nbsp; &#8211; 引入基于<a href="http://tonybai.com/2012/02/14/install-and-configure-jenkins/">Jenkins</a>的<a href="http://tonybai.com/2012/02/15/intergating-on-multiple-platforms-simultaneously-using-jenkins/">持续集成</a>；<br />
	&nbsp;&nbsp;&nbsp; &#8211; 重新思考和设计<a href="http://tonybai.com/2012/01/17/also-talk-about-building-c-app/">构建</a>环节，通过<a href="http://tonybai.com/2011/12/08/buildc-a-building-assistant-tool-for-c-app/">buildc</a>提高构建效率；<br />
	&nbsp;&nbsp;&nbsp; &#8211; 重新设计通用<a href="http://tonybai.com/2012/02/01/also-talk-about-c-app-install-package-making-and-deploying/">安装包</a>；<br />
	&nbsp;&nbsp;&nbsp; &#8211; 使用<a href="http://tonybai.com/2010/09/30/opensource-a-lightweight-c-unit-test-framework/">LCUT</a>对骨架进行单元测试覆盖；<br />
	&nbsp;&nbsp;&nbsp; &#8211; 规范<a href="http://tonybai.com/2013/05/09/also-talk-about-commit-log/">commit log</a>以及<a href="http://tonybai.com/2013/07/08/code-review-from-rule-of-man-to-rule-of-law/">代码提交流程</a>；<br />
	&nbsp;&nbsp;&nbsp; &#8211; 应用<a href="http://tonybai.com/2011/04/21/apply-style-check-to-c-code/">代码风格检查</a>工具，使得所有代码风格一致。</p>
<p>事实证明上述实践在这次系统重写的过程中产生了很好的效果，尤其在代码质量保证方面，系统上线后的结果也恰恰印证了这一点。</p>
<p><b>* 上线</b></p>
<p>&ldquo;丑媳妇总要见公婆&rdquo;。我们的新系统也到了该上线服务的时候了。为了这次上线，我们做了较为充分的实施准备，无论是人员还是时间，都有倾向性的向这个系统 投入。我们也提前做好了应对各种突发问题的预案。可实际情况出乎预料，与遗留系统的版本升级相比，这次全新系统上线显得十分顺利，系统的核心相当稳定，出 现的一些问题也都比较边缘，对这次成功上线已经不构成什么影响了。</p>
<p><b>* 那一抹释然</b></p>
<p>在实施人员庆贺上线成功时，在领导口头表扬时，我的内心却显得十分平静。对于新系统来说，这是一个好的开始。对我个人来说，我感受到了那一抹期望已久的释 然。在这个领域里这个方向上已经摸爬滾打了多年，虽然还有好多地方需要改进，好多实践需要完善，但我的内心告诉我：&ldquo;够了&rdquo;、&ldquo;已经没什么牵挂了&rdquo;、&ldquo;是 时候换换方向、换换领域了&rdquo;、&ldquo;让其他人去做吧&rdquo;。我已经在产品和团队中融入了我的思想，我相信他们都能很好的演化和发展。而我则为接受新思想、新领域做 好了准备。</p>
<p>的确也到了为自己设立新目标的时候了！</p>
<p style='text-align:left'>&copy; 2013, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2013/12/26/just-for-being-relieved/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>代码评审，由人治过渡到“法治”</title>
		<link>https://tonybai.com/2013/07/08/code-review-from-rule-of-man-to-rule-of-law/</link>
		<comments>https://tonybai.com/2013/07/08/code-review-from-rule-of-man-to-rule-of-law/#comments</comments>
		<pubDate>Mon, 08 Jul 2013 08:23:27 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[APR]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[Code-Review]]></category>
		<category><![CDATA[pre-commit-hook]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[ReviewBoard]]></category>
		<category><![CDATA[Subversion]]></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=1294</guid>
		<description><![CDATA[事实证明：有效的代码评审(Code Review，也有叫代码审查的），对保证代码质量具有十分重要的作用。因此这两年来我一直尝试着在这块不断改进和完善，以期望能形成一套合理、规范、有 效且高效的代码评审流程，这包括引入在线代码评审系统、走查和在线评审结合、规范评审Request的规模与有效性、设立评审专员等，用心不可谓不良苦 ^_^。大家也的确形成了及时提交Code Review Request或组织进行代码走查的良好习惯。不过我还是发现了一些问题。 * 有些组（我对其影响力不足的^_^）依旧没有严格执行代码评审环节，代码屡屡出现低级错误； * 走查形式的会议评审缺乏全面性，效果好坏与参与者的&#8220;状态&#8221;直接相关； * 在线评审环节缺乏&#8220;责任制&#8221;，常出现的一种情况是：请求大家评审，结果可能却是大家都没有评审。出现&#34;Request Review Miss&#34;的现象。 这让我陷入思考：长期以来我们在代码评审这块过于依赖人的自觉性，理想地认为每个人都能认识到代码评审的重要性，并认真地执行代码评审的流程或充满激情地 参与到其他人发起的代码评审过程中去，但结果事与愿违。这就像党员如何保持纯洁性一样，如果仅仅依靠个人道德/职业水平约束，这事往往是不成的。事实证明 人治在中国社会是会造成各种社会问题的。我们的代码评审环节也是一样，我们不能再期望所有人都能和我站在一条认知和激情水平线上，于是我打算尝试向&#8220;法 治&#8221;过渡。 &#34;法&#34;，规则制度也，是团队一致认同的可以提升产品质量的规则制度。以此为前提，我要做的就是设立&#8220;检查和预防&#8221;机构，即以很低的Cost，检查大家是否按&#8220;法&#8221;完成了代码评审环节，提醒大家要按&#8220;法&#8221;进行。我采取了几个措施： 【规范Commit Log 】 这是一个前提工作。实现规范的Commit Log便于后续的检查和监督，同时细化规范的Commit Log信息对代码维护是大有裨益的。在Commit Log中还增加了一些关联信息，方便维护者了解该Commit的背景。初期的模板是这么来确定的： 模板结构： TITLE BODY RELATIONSHIPS 展开后如下： [Category] Title content Body content [BUGID] QC#733 &#124; JIRA#766 [REVIEWID] RB#767 [REVIEWED BY] xx, yy, zz [SIGNOFF BY] xx TITLE Category： &#160;&#160; &#8211; BUGFIX [...]]]></description>
			<content:encoded><![CDATA[<p>事实证明：有效的<a href="http://tonybai.com/2011/02/22/code-reviews/">代码评审</a>(Code Review，也有叫代码审查的），对保证代码质量具有十分重要的作用。因此这两年来我一直尝试着在这块不断改进和完善，以期望能形成一套合理、规范、有 效且高效的代码评审流程，这包括引入<a href="http://tonybai.com/2010/12/18/thoughts-on-online-coding-review/">在线代码评审系统</a>、走查和在线评审结合、规范评审Request的规模与有效性、设立评审专员等，用心不可谓不良苦 ^_^。大家也的确形成了及时提交Code Review Request或组织进行代码走查的良好习惯。不过我还是发现了一些问题。</p>
<p>* 有些组（我对其影响力不足的^_^）依旧没有严格执行代码评审环节，代码屡屡出现低级错误；<br />
	* 走查形式的会议评审缺乏全面性，效果好坏与参与者的&ldquo;状态&rdquo;直接相关；<br />
	* 在线评审环节缺乏&ldquo;责任制&rdquo;，常出现的一种情况是：请求大家评审，结果可能却是大家都没有评审。出现&quot;Request Review Miss&quot;的现象。</p>
<p>这让我陷入思考：长期以来我们在代码评审这块过于依赖人的自觉性，理想地认为每个人都能认识到代码评审的重要性，并认真地执行代码评审的流程或充满激情地 参与到其他人发起的代码评审过程中去，但结果事与愿违。这就像党员如何保持纯洁性一样，如果仅仅依靠个人道德/职业水平约束，这事往往是不成的。事实证明 人治在中国社会是会造成各种社会问题的。我们的代码评审环节也是一样，我们不能再期望所有人都能和我站在一条认知和激情水平线上，于是我打算尝试向&ldquo;法 治&rdquo;过渡。</p>
<p>&quot;法&quot;，规则制度也，是团队一致认同的可以提升产品质量的规则制度。以此为前提，我要做的就是设立&ldquo;检查和预防&rdquo;机构，即以很低的Cost，检查大家是否按&ldquo;法&rdquo;完成了代码评审环节，提醒大家要按&ldquo;法&rdquo;进行。我采取了几个措施：</p>
<p><b>【规范Commit Log </b><b>】</b></p>
<p>这是一个前提工作。实现规范的<a href="http://tonybai.com/2013/05/09/also-talk-about-commit-log/">Commit Log</a>便于后续的检查和监督，同时细化规范的Commit Log信息对代码维护是大有裨益的。在Commit Log中还增加了一些关联信息，方便维护者了解该Commit的背景。初期的模板是这么来确定的：</p>
<p>模板结构：</p>
<p><font face="Courier New"><i>TITLE</i><br />
	<i>BODY</i><br />
	<i>RELATIONSHIPS</i></font></p>
<p>展开后如下：</p>
<p><font face="Courier New"><i>[Category] Title content</i></font></p>
<p><font face="Courier New"><i>Body content</i></font></p>
<p><font face="Courier New"><i>[BUGID] QC#733 | JIRA#766</i><br />
	<i>[REVIEWID] RB#767</i><br />
	<i>[REVIEWED BY] xx, yy, zz</i><br />
	<i>[SIGNOFF BY] xx</i></font></p>
<p>TITLE Category：<br />
	&nbsp;&nbsp; &#8211; BUGFIX 代码修复<br />
	&nbsp;&nbsp; &#8211; FEATURE 新功能特性添加<br />
	&nbsp;&nbsp; &#8211; TASK 诸如代码美化、调整版本号等<br />
	&nbsp;&nbsp; &#8211; URGENT 紧急提交，对此类commit，可不做review和拦截</p>
<p>BODY Content：<br />
	&nbsp;&nbsp; 有关此次修改的详细信息说明</p>
<p>RELATIONSHIPS：<br />
	&nbsp;&nbsp; &#8211; [BUGID] 一般用Bug跟踪系统的ID号<br />
	&nbsp;&nbsp; &#8211; [REVIEWID] reviewboard上的ID号<br />
	&nbsp;&nbsp; &#8211; [REVIEWED BY] xx, yy, zz<br />
	&nbsp;&nbsp; &#8211; [SIGNOFF BY] xx</p>
<p><b>【&quot;全覆盖&quot;原则</b>】</p>
<p>所有变更代码都要发起在线&ldquo;Code Review Request&rdquo;，即便是会议走查的代码，会后也要补提&ldquo;Review Request&rdquo;。</p>
<p><b>【&ldquo;低保&rdquo;原则</b>】</p>
<p>每个Review Request至少选择两名评审负责人，填到&quot;Request&quot;中，这两个人必须对此Request给出评审意见，这是一个评审的<b>最低保障</b>了，这总比没有人评审要好。当然了其他人也都可以参与评审。只有这两名评审负责人明确提交&quot;ship it&quot; Comment后，该代码才算是通过评审。</p>
<p><b>【关键路径拦截】</b></p>
<p>&quot;对不起，若不符合规定，你的工作将无法进行下去&quot;。有了统一的Commit Log模板，我们就可以对大家的代码Commit环节做检查和<a href="http://tonybai.com/2010/08/07/use-svn-pre-commit-hook/">拦截</a>了。如果代码没有进行评审，无法填写模板中的字段内容，那代码将无法提交到代码库中。如 果虚构Commit log内容，这将是极大的错误，在抽查中一旦发现，后果将是很严重的^_^。</p>
<p>当然这一过程中还有很多细节需要考虑，比如Reviewer的选择不能集中在一个人身上，否则会造成热点；再比如紧急提交代码应该如何处理等等。&ldquo;法治&rdquo; 是与一定的&ldquo;国情&rdquo;相匹配的，并不是所有的组织都需要进行这么严格且略有死板&ldquo;法治&rdquo;手段，依团队内组员的专业能力和认知水平而定。</p>
<p>有些公司开发了自己的统一开发平台，将一系列流程都在一套系统中规范了起来，这当然是更好的&ldquo;法治&rdquo;了。但在没有这样的平台的前提下，初步使用上述的几个手段，还是会收获一些改进的。</p>
<p style='text-align:left'>&copy; 2013, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2013/07/08/code-review-from-rule-of-man-to-rule-of-law/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>buildc 0.3.0版本发布</title>
		<link>https://tonybai.com/2013/05/11/buildc-0-3-0-release/</link>
		<comments>https://tonybai.com/2013/05/11/buildc-0-3-0-release/#comments</comments>
		<pubDate>Sat, 11 May 2013 00:46:42 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[Buildc]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[CBehave]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[LCUT]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[Subversion]]></category>
		<category><![CDATA[学习]]></category>
		<category><![CDATA[工作]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[程序员]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1268</guid>
		<description><![CDATA[自buildc正式在项目中应用以来，我们收到了许多同事针对buildc演进的意见和建议。其中确实有些易用性的问题是在最初设计时未考虑周全的，尤其是.buildc.rc中的配置，同事们对该文件的配置已经&#8220;怨声载道&#8221;了。 .buildc.rc是用来配置某开发者在开发过程中使用的第三方库所在subversion repository信息的，例如： a_repository = (&#39;SVN库地址&#39;, &#39;本地缓存路径&#39;, &#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; [ &#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; # 格式：[(&#8220;第三方库名称&#8221;, &#8220;库版本&#8221;, &#8220;特征库文件&#8221;), &#8230;] &#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; (&#39;libevent&#39;, &#39;2.0.10&#39;, &#39;lib/libevent.a&#39;), &#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; (&#39;instantclient&#39;, &#39;10.2.0.5.0&#39;, &#39;lib/libnnz10.so&#39;), &#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; &#8230; &#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ] &#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ) b_repository = (&#39;SVN库地址&#39;, &#39;本地缓存路径&#39;, []) c_repository = (&#39;SVN库地址&#39;, &#39;本地缓存路径&#39;, []) &#8230; external_repositories = [ &#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; a_repository, &#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; b_repository, &#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; c_repository, &#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; &#8230; &#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ] 这里面需要维护最多、最频繁的就是各个repository中具备的第三方库名、版本号。开发者所开发的项目所依赖的第三方库信息发生变化，不仅仅需要修 改project下的buildc.cfg文件，还可能要修改.buildc.rc，大家维护起来确实体验不好，会多耗费一些工作量。 [...]]]></description>
			<content:encoded><![CDATA[<p>自<a href="http://tonybai.com/2011/12/08/buildc-a-building-assistant-tool-for-c-app/">buildc</a>正式在项目中应用以来，我们收到了许多同事针对<a href="http://code.google.com/p/buildc">buildc</a>演进的意见和建议。其中确实有些易用性的问题是在最初设计时未考虑周全的，尤其是.buildc.rc中的配置，同事们对该文件的配置已经&ldquo;怨声载道&rdquo;了。</p>
<p>.buildc.rc是用来配置某开发者在开发过程中使用的第三方库所在subversion repository信息的，例如：</p>
<p><font face="Courier New">a_repository = (&#39;SVN库地址&#39;, &#39;本地缓存路径&#39;,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; [<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; # 格式：[(&ldquo;第三方库名称&rdquo;, &ldquo;库版本&rdquo;, &ldquo;特征库文件&rdquo;), &hellip;]<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; (&#39;libevent&#39;, &#39;2.0.10&#39;, &#39;lib/libevent.a&#39;),<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; (&#39;instantclient&#39;, &#39;10.2.0.5.0&#39;, &#39;lib/libnnz10.so&#39;),<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ]<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; )<br />
	b_repository = (&#39;SVN库地址&#39;, &#39;本地缓存路径&#39;, [])<br />
	c_repository = (&#39;SVN库地址&#39;, &#39;本地缓存路径&#39;, [])<br />
	&hellip;<br />
	external_repositories = [<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; a_repository,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; b_repository,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c_repository,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &hellip;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ]</font></p>
<p>这里面需要维护最多、最频繁的就是各个repository中具备的第三方库名、版本号。开发者所开发的项目所依赖的第三方库信息发生变化，不仅仅需要修 改project下的buildc.cfg文件，还可能要修改.buildc.rc，大家维护起来确实体验不好，会多耗费一些工作量。</p>
<p>针对这个主要问题，我们决定对buildc进行一次较大范围的重构，重构后的版本定为<a href="https://buildc.googlecode.com/files/buildc-0.3.0.tar.gz">buildc 0.3.0版本</a>。以下是buildc 0.3.0版本的主要改动点：</p>
<p><b>一、简化.buildc.rc的配置，重新定义cache相关命令的语义</b></p>
<p>0.3.0及以后版本的.buildc.rc只需配置repository的地址信息以及cache缓存的本地路径信息，无需再提供repository 里面具体的第三方库以及版本号信息了，这样一来，大多数情况下，project依赖的第三方库发生变更，都无需修改.buildc.rc了。</p>
<p><font face="Courier New">a_repository = (&#39;SVN库地址&#39;, &#39;本地缓存路径&#39;)<br />
	b_repository = (&#39;SVN库地址&#39;, &#39;本地缓存路径&#39;)<br />
	c_repository = (&#39;SVN库地址&#39;, &#39;本地缓存路径&#39;)<br />
	&hellip;<br />
	external_repositories = [<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; a_repository,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; b_repository,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c_repository,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &hellip;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ]</font></p>
<p>随之而变的是buildc cache相关命令的语义，0.3.0中cache相关命令的语义如下：</p>
<p><i>* buildc cache init </i>- 生成.buildc.repository，该文件是svn库的目录结构文件，相当于一份svn repository内部的地图，repository中存放的各种第三方库以及版本均在该文件中索引；如果该文件已经存在，命令执行的结果为：提示已存在。</p>
<p><i>* buildc cache upgrade</i> &#8211; 根据.buildc.rc的最新更新，重新生成.buildc.repository文件，并将该文件中所有lib本地的 Revision号置为none。该文件并不会执行本地cache的library的真实更新操作。</p>
<p><i>* buildc cache update</i>&nbsp; -&nbsp;<br />
	&nbsp;&nbsp;&nbsp; 1. 如果.buildc.rc已经修改，但没有执行buildc cache upgrade，update会对比本地缓存库信息与对应的.repository文件中的同名lib信息，如果不一致，则提醒执行upgrade。<br />
	&nbsp;&nbsp;&nbsp; 2. 如果.buildc.repository是新生成的，所有lib本地的Revision号均是none，则提示没有要更新的本地缓存库；<br />
	&nbsp;&nbsp;&nbsp; 3. 如果某个项目已经download了自己依赖的库，那update将比对svn库中和本地库的revision差异，并下载最新库版本。并修改.buildc.repository中对应库的本地revision number。</p>
<p>* <i>buildc cache remove</i> &#8211; 将.buildc.repository中对应库的本地revision number都置为none，并删除本地缓存的库文件。</p>
<p><b>二、重新定义config make的语义</b></p>
<p>前面提到了，在执行buildc cache init时，buildc只是负责生成.repository文件，而并不真实执行库文件的下载和缓存。那何时真正下载呢？答案是在执行buildc config make时。这里颇有些&ldquo;lazy evaluate&rdquo;的味道，需要时再&ldquo;download and cache it&quot;。</p>
<p>* <em>buildc config make</em></p>
<p>1. 如果.buildc.rc已经修改，但没有执行buildc cache upgrade，config make会对比本地缓存库信息与对应的.repository文件中的同名lib信息，如果不一致，则提醒执行upgrade。<br />
	2. 如果.buildc.rc是新生成的，或执行cache upgrade后的，config make会根据project对应的buildc.cfg中配置的第三方库，在.buildc.repository中查找是否存在（包括对应的版本 号），如果存在，则从subversion server端自动下载；否则提示出错。<br />
	3. 如果本地缓存中某个库文件不存在，buildc config make会检测到，并自动下载该库，并cache起来。<br />
	4. 如果subversion端某个库的svn revision号发生的更新，buildc config make会检测到，并下载最新的版本。</p>
<p>总之一切都是在buildc config make时来完成的，按需下载或更新，这样你甚至无需进行手工的library Cache维护。</p>
<p><b>三、转向OO</b><b>范型</b></p>
<p>实现buildc 0.3.0的小同事(wtz1989227@gmail.com)对OO情有独钟，因此在这个版本中，他将以前的结构化代码做了大幅度调整，并用OO的方 式进行了重构。按照wtz的思路，这次改造比较初级，OOD做得还不够充分，以后慢慢调整。实际代码中反映出来的情况也的确是这样。</p>
<p><b>四、</b><b>buildc 0.2.3发布</b></p>
<p>在将buildc 0.3.0代码merge到trunk之前，我创建了buildc-0.2的maintain branch，虽然理论上buildc 0.3.0在功能和配置方面与buildc 0.2.x版本是兼容的，但毕竟代码调整幅度较大。另外建议大家都转移到0.3.0这个最新版本上来，buildc-0.2分支顶多做一些bugfix， 不会再有新feature添加进去了。</p>
<p>昨天在发布buildc 0.3.0的同时，还发布了buildc-0.2的一个Bugfix版 &#8211; <a href="https://buildc.googlecode.com/files/buildc-0.2.3.tar.gz">buildc 0.2.3</a>，该版本主要做了如下一些fix：</p>
<p>&nbsp; * 执行cache upgrade时增加对.buildc.rc中repository特征文件存在性的检查；<br />
	&nbsp; * 执行config make时增加对Make.rules文件是否为空的判断；<br />
	&nbsp; * 执行pack source时，添加VERSION文件，记录打包的上下文信息。</p>
<p><b>五、其它</b></p>
<p>考虑到<a href="http://github.com">github</a>的活跃度远远高于<a href="http://code.google.com">google code</a>，加上google code最近访问十分不稳定，因此之前就将<a href="https://github.com/bigwhite/buildc">buildc</a>（还有<a href="https://github.com/bigwhite/cbehave">cbehave</a>、<a href="https://github.com/bigwhite/lcut">lcut</a>以及我的<a href="https://github.com/bigwhite/experiments">实验代码库</a>）fork了一份到github上了，也攒攒 github上人气，因此这次buildc 0.2.3和buildc 0.3.0的代码还要发布到github上一次。git工具平时用的少，尤其是提交代码到github，这次算入门了。</p>
<p>* 代码远程提交<br />
	用git remote add一个github的remote repository后，就可以使用git push origin master将本地的commit推送到github上了。</p>
<p>* 打tag，并推送tag</p>
<p>&nbsp;&nbsp; &#8212; 查看Tag的git命令是<font face="Courier New">git tag</font>；<br />
	&nbsp;&nbsp; &#8212; 本地打tag，用这个命令： <font face="Courier New">git tag -a v0.2.3 -m&quot;0.2.3 released&quot;</font>；<br />
	&nbsp;&nbsp; &#8212; 推送Tag到remote repository：<font face="Courier New">git push &#8211;tags origin master</font>，不加&#8211;tags是无法推送tag的。</p>
<p>* branch操作<br />
	&nbsp; &#8212; 查看branch：<font face="Courier New">git branch</font><br />
	&nbsp; &#8212; 创建branch：<font face="Courier New">git branch buildc-0.2</font><br />
	&nbsp; &#8212; 推送branch：<font face="Courier New">git push origin buildc-0.2</font><br />
	&nbsp; &#8212; 本地切换branch：<font face="Courier New">git checkout buildc-0.2</font></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/11/buildc-0-3-0-release/feed/</wfw:commentRss>
		<slash:comments>0</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>升级到Ubuntu 12.04LTS</title>
		<link>https://tonybai.com/2012/12/04/upgrade-ubuntu-to-1204-lts/</link>
		<comments>https://tonybai.com/2012/12/04/upgrade-ubuntu-to-1204-lts/#comments</comments>
		<pubDate>Tue, 04 Dec 2012 07:21: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[Chrome]]></category>
		<category><![CDATA[filezilla]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[grub2]]></category>
		<category><![CDATA[iBus]]></category>
		<category><![CDATA[iptux]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[MBR]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Subversion]]></category>
		<category><![CDATA[ThinkPad]]></category>
		<category><![CDATA[Thunderbird]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[Unity]]></category>
		<category><![CDATA[Vim]]></category>
		<category><![CDATA[Windows]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[工作]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[程序员]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1127</guid>
		<description><![CDATA[Ubuntu 10.04 LTS已经伴随我两年了，经过我这么长时间的折腾，Ubuntu早已不堪重负^_^。在未升级前，Ubuntu 10.04已经表现出诸多问题： - 在家中连接无线路由器时间漫长，且经常掉线； - 在公司用有线网络经常掉线； - 由于反复安装软件，系统中残留较多垃圾数据； - Ubuntu 10.04官方源中的软件版本都有些低，很多软件手工安装高版本比较费力； 另外原先与Ubuntu 10.04共存的Windows 7系统已经早在大半年前就罢工了，无法引导进入，原因不明，我也懒得去fix，平时根本也用不到Windows系统。因此这次升级系统还有另外一个目的， 那就是将Windows 7的残余数据彻底清除出我的本本。 虽然Ubuntu最新版本是刚刚发布不久的12.10，但本着只用LTS版的原则，这次打算升级12.04 LTS，目前的最新版本是12.04.1。 原以为我的老旧的ThinkPad X60可以安装64位的12.04，但在安装时引导程序提示X60的CPU不是X86-64类型的，而是一颗双核的i686 CPU。恼火啊！下载和刻录一个iso容易吗，尤其在公司这个代理网络里！无奈只能重新折腾，重新下载和刻录32位的Ubuntu 12.04.1。 安装方法这里不赘述了。这次在安装时我使用了安装界面上可选的自定义安装分区的方法将12.04安装到了原Windows 7的分区中了，但安装结束重启后，Grub2的引导初始页面居然依旧显示以前的系统菜单，并且菜单中并没有我新装的12.04菜单项。重新安装，这次格掉 了原Ubuntu 10.04的安装分区。经过漫长等待后重启机器，映入眼帘的是&#34;grub rescue&#62;&#34;，引导再次失败，显而易见，Grub2依旧没有找到正确的引导分区。 Google了一把，原来是我对Grub2的引导原理理解还不够，Grub2是两阶段引导。直接格式化原有分区并安装新系统并未重新刷新 MBR(主引导记录)中的第二阶段引导分区的id，因此机器启动后，MBR依旧按原有的配置去寻找那个分区ID，但装有Ubuntu的分区ID已 经发生了变化，原引导分区被重新格式化并且无系统，因此Grub2无法找到分区，无法开启第二阶段引导。 无奈只能使用livecd，进入terminal，执行如下命令（ubuntu 12.04安装在sda1）： &#62; sudo mount /dev/sda1 /mnt &#62; sudo grub-install &#8211;boot-directory=/mnt/boot&#160; /dev/sda 再次重启后，系统引导正常，终于可以进入12.04了。网上说利用grub rescue命令也可以刷新MBR记录，不过我没能试验成功。 不同Ubuntu的配置过程大同小异，我早已轻车熟路了： - 添两个源：搜狐和网易的ubuntu 12.04的源，然后更新软件包列表； - 打开更新管理器，设置首选软件源； - 打开&#8220;语言支持&#8221;，下载和更新语言包； - [...]]]></description>
			<content:encoded><![CDATA[<p><a href="http://tonybai.com/2010/08/25/move-to-ubuntu-thoroughly/">Ubuntu 10.04 </a>LTS已经伴随我两年了，经过我这么长时间的折腾，Ubuntu早已不堪重负^_^。在未升级前，Ubuntu 10.04已经表现出诸多问题：</p>
<p>- 在家中连接无线路由器时间漫长，且经常掉线；<br />
	- 在公司用有线网络经常掉线；<br />
	- 由于反复安装软件，系统中残留较多垃圾数据；<br />
	- Ubuntu 10.04官方源中的软件版本都有些低，很多软件手工安装高版本比较费力；</p>
<p>另外原先与Ubuntu 10.04共存的Windows 7系统已经早在大半年前就罢工了，无法引导进入，原因不明，我也懒得去fix，平时根本也用不到Windows系统。因此这次升级系统还有另外一个目的， 那就是将Windows 7的残余数据彻底清除出我的本本。</p>
<p>虽然Ubuntu最新版本是刚刚发布不久的12.10，但本着只用LTS版的原则，这次打算升级12.04 LTS，目前的最新版本是12.04.1。</p>
<p>原以为我的老旧的ThinkPad X60可以安装64位的12.04，但在安装时引导程序提示X60的CPU不是X86-64类型的，而是一颗双核的i686 CPU。恼火啊！下载和刻录一个iso容易吗，尤其在公司这个代理网络里！无奈只能重新折腾，重新下载和刻录32位的Ubuntu 12.04.1。</p>
<p>安装方法这里不赘述了。这次在安装时我使用了安装界面上可选的自定义安装分区的方法将12.04安装到了原Windows 7的分区中了，但安装结束重启后，Grub2的引导初始页面居然依旧显示以前的系统菜单，并且菜单中并没有我新装的12.04菜单项。重新安装，这次格掉 了原Ubuntu 10.04的安装分区。经过漫长等待后重启机器，映入眼帘的是&quot;grub rescue&gt;&quot;，引导再次失败，显而易见，Grub2依旧没有找到正确的引导分区。</p>
<p>Google了一把，原来是我对Grub2的引导原理理解还不够，Grub2是两阶段引导。直接格式化原有分区并安装新系统并未重新刷新 MBR(主引导记录)中的第二阶段引导分区的id，因此机器启动后，MBR依旧按原有的配置去寻找那个分区ID，但装有Ubuntu的分区ID已 经发生了变化，原引导分区被重新格式化并且无系统，因此Grub2无法找到分区，无法开启第二阶段引导。</p>
<p>无奈只能使用livecd，进入terminal，执行如下命令（ubuntu 12.04安装在sda1）：<br />
	<span style="font-family:courier new,courier,monospace;">&gt; sudo mount /dev/sda1 /mnt<br />
	&gt; sudo grub-install &#8211;boot-directory=/mnt/boot&nbsp; /dev/sda</span></p>
<p>再次重启后，系统引导正常，终于可以进入12.04了。网上说利用grub rescue命令也可以刷新MBR记录，不过我没能试验成功。</p>
<p>不同Ubuntu的配置过程大同小异，我早已轻车熟路了：</p>
<p>- 添两个源：搜狐和网易的ubuntu 12.04的源，然后更新软件包列表；<br />
	- 打开更新管理器，设置首选软件源；<br />
	- 打开&ldquo;语言支持&rdquo;，下载和更新语言包；<br />
	- 安装Google Chrome、Vim、iptux、rdesktop、Filezilla、subversion、htop、git、golang、apache2、 parcellite等工具；<br />
	- Thunderbird配置恢复(Ubuntu 12.04已经将<a href="http://tonybai.com/2011/03/21/upgrade-thunderbird/">thunderbird</a>作为默认mail客户端)；<br />
	- 恢复用户配置，包括.bashrc、<a href="http://tonybai.com/2010/09/10/use-the-document-template-of-ubuntu/">模板</a>、vim配置和插件等；<br />
	- 恢复hosts、apache2等配置；</p>
<p>Ubuntu演进到今天，对中文的支持已经很好了。默认情况下的iBus拼音已经很好用了。更新完语言包后，输入法变成SunPinyin，用起 来的确比小企鹅输入法智能多了。</p>
<p>Ubuntu默认的桌面环境是自行开发的Unity，至少目前感觉还行，其Dash程序启动器比较好用，基本可以替代原先在Gnome下用的 launchy。不过对于我用的X60 12寸普通屏幕(非宽屏)来讲，左边的Dock启动栏显然占据了应用本已不大的界面空间。</p>
<p>Ubuntu 12.04配置与应用安装时遇到了两个问题，这里做个分享和备忘：</p>
<p>1、ext3分区自动挂载以及权限问题</p>
<p>这次安装时，原安装ubuntu 10.04的分区被重新格式化了，但并未挂载目录。系统启动后，该分区未被自动挂载，只能手动挂载。于是尝试通过修改/etc/fstab自动挂载该ext3分区。</p>
<p>root下建立/home1目录，在/etc/fstab中添加一行，将该分区自动挂载到/home1：</p>
<p><span style="font-family:courier new,courier,monospace;"># / was on /dev/sda3 during installation<br />
	UUID=1ed84fc1-5ba2-4e82-94f5-c3e4f5654036 /home1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ext3&nbsp;&nbsp;&nbsp; defaults,errors=remount-ro 0&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0</span></p>
<p>重启后，该分区如预期一样被自动挂载。但有出现了新问题，该分区下无法用普通用户权限创建文件，也就是没有写权限。反复改了几次fstab中的挂载参数， 都无法解决。后想到既然分区已经挂载到了/home1目录，那修改/home1目录的权限是否可以解决这个问题呢？于是sudo chmod 777 /home1。命令执行完后重启。新分区自动挂载，并可写了。</p>
<p>2、恢复iptux默认配置</p>
<p>部门都用飞秋作为内部IM工具。Linux下的feiq协议兼容工具是iptux。Ubuntu 12.04下用apt-get就可以正确安装iptux，运行也一切OK。但我在配置iptux时，无意中选择了&ldquo;启动后主面板自动隐藏&rdquo;，导致始终无法 看到iptux主界面，也就无法发送消息。于是开始尝试恢复iptux的默认配置。</p>
<p>直接上方法：<br />
	- 后台杀掉iptux；<br />
	- cd ~/.gconf/apps/iptux<br />
	- 删除iptux配置文件<br />
	- 执行gconftool-2 &#8211;recursive-unset /apps/iptux</p>
<p>注意如果不用上面方法，即便是卸载再重装iptux也是无济于事的。</p>
<p style='text-align:left'>&copy; 2012 &#8211; 2013, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2012/12/04/upgrade-ubuntu-to-1204-lts/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
	</channel>
</rss>
