<?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; GOROOT</title>
	<atom:link href="http://tonybai.com/tag/goroot/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语言演进的双保险：GOEXPERIMENT与GODEBUG</title>
		<link>https://tonybai.com/2024/10/11/go-evolution-dual-insurance-goexperiment-godebug/</link>
		<comments>https://tonybai.com/2024/10/11/go-evolution-dual-insurance-goexperiment-godebug/#comments</comments>
		<pubDate>Fri, 11 Oct 2024 15:08:35 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[aliastypeparams]]></category>
		<category><![CDATA[generics]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[Go1]]></category>
		<category><![CDATA[go1.21]]></category>
		<category><![CDATA[go1.22]]></category>
		<category><![CDATA[go1.23]]></category>
		<category><![CDATA[gobuild]]></category>
		<category><![CDATA[GODEBUG]]></category>
		<category><![CDATA[GOEXPERIMENT]]></category>
		<category><![CDATA[goinstall]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gopls]]></category>
		<category><![CDATA[GOROOT]]></category>
		<category><![CDATA[gorun]]></category>
		<category><![CDATA[gotest]]></category>
		<category><![CDATA[goversion]]></category>
		<category><![CDATA[HTTP2]]></category>
		<category><![CDATA[internal]]></category>
		<category><![CDATA[loopvar]]></category>
		<category><![CDATA[main]]></category>
		<category><![CDATA[Module]]></category>
		<category><![CDATA[panic]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[ticker]]></category>
		<category><![CDATA[time]]></category>
		<category><![CDATA[Timer]]></category>
		<category><![CDATA[typealias]]></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=4340</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/10/11/go-evolution-dual-insurance-goexperiment-godebug Go语言自诞生以来就以其简洁、高效和强大的并发支持而闻名，Go团队承诺保持Go1向后兼容性，以确保用户的代码在未来的版本中继续正常运行。然而，保持语言的稳定性与不断创新(增加新特性)之间的平衡一直是Go团队面临的挑战。为了应对这一挑战，Go语言引入了两个关键机制：GOEXPERIMENT和GODEBUG来平衡新功能的试验、稳定发布和向后兼容。这两个机制共同构成了Go语言特性发布的“双保险”，确保语言能够稳步前进的同时，不会因为激进的改变而影响现有代码的稳定性。本文就来简单探讨一下这两个机制是如何保障Go语言新特性稳定发布的。 1. GOEXPERIMENT：新特性的摇篮 GOEXPERIMENT是一个Go语言的环境变量，是用于控制实验性特性的机制。它允许开发者在编译时（使用go build、go install、go run或go test）启用一些尚未正式发布的语言特性或优化。通过GOEXPERIMENT，Go团队能够在正式发布之前广泛测试新功能，收集反馈并进行必要的调整。 比如，在今年8月发布的Go 1.23版本发布了一个实验特性：带有类型参数的type alias，就像下面代码一样，我们可以在编译时开启该实验特性： // github.com/bigwhite/experiments/blob/master/go1.23-examples/lang/generic_type_alias.go $GOEXPERIMENT=aliastypeparams go build generic_type_alias.go $./generic_type_alias Int Slice: [1 2 3 4 5] String Slice: [hello world] Person Slice: [{Alice 30} {Bob 25}] 如果不开启实验特性，上述的代码就会编译失败： // github.com/bigwhite/experiments/blob/master/go1.23-examples/lang/generic_type_alias.go $go build generic_type_alias.go # command-line-arguments ./generic_type_alias.go:5:6: generic type alias requires GOEXPERIMENT=aliastypeparams 我们看到：通过设置GOEXPERIMENT=featureflag可以开启对应的实验特性，如果要同时开启多个实验特性，可以用逗号分隔的实验特性列表，就像下面这样： $GOEXPERIMENT=featureflag1,featureflag2,...,featureflagN go [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/go-evolution-dual-insurance-goexperiment-godebug-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/10/11/go-evolution-dual-insurance-goexperiment-godebug">本文永久链接</a> &#8211; https://tonybai.com/2024/10/11/go-evolution-dual-insurance-goexperiment-godebug</p>
<p>Go语言自诞生以来就以其简洁、高效和强大的并发支持而闻名，Go团队承诺保持<a href="https://go.dev/doc/go1compat">Go1向后兼容性</a>，以确保用户的代码在未来的版本中继续正常运行。然而，保持语言的稳定性与不断创新(增加新特性)之间的平衡一直是Go团队面临的挑战。为了应对这一挑战，Go语言引入了两个关键机制：GOEXPERIMENT和GODEBUG来平衡新功能的试验、稳定发布和向后兼容。这两个机制共同构成了Go语言特性发布的“双保险”，确保语言能够稳步前进的同时，不会因为激进的改变而影响现有代码的稳定性。本文就来简单探讨一下这两个机制是如何保障Go语言新特性稳定发布的。</p>
<h2>1. GOEXPERIMENT：新特性的摇篮</h2>
<p>GOEXPERIMENT是一个Go语言的环境变量，是用于控制实验性特性的机制。它允许开发者在编译时（使用go build、go install、go run或go test）启用一些尚未正式发布的语言特性或优化。通过GOEXPERIMENT，Go团队能够在正式发布之前广泛测试新功能，收集反馈并进行必要的调整。</p>
<p>比如，在今年8月发布的<a href="https://tonybai.com/2024/08/19/some-changes-in-go-1-23">Go 1.23版本</a>发布了一个实验特性：<a href="https://go.dev/blog/alias-names">带有类型参数的type alias</a>，就像下面代码一样，我们可以在编译时开启该实验特性：</p>
<pre><code>// github.com/bigwhite/experiments/blob/master/go1.23-examples/lang/generic_type_alias.go

$GOEXPERIMENT=aliastypeparams go build generic_type_alias.go
$./generic_type_alias
Int Slice: [1 2 3 4 5]
String Slice: [hello world]
Person Slice: [{Alice 30} {Bob 25}]
</code></pre>
<p>如果不开启实验特性，上述的代码就会编译失败：</p>
<pre><code>// github.com/bigwhite/experiments/blob/master/go1.23-examples/lang/generic_type_alias.go

$go build generic_type_alias.go
# command-line-arguments
./generic_type_alias.go:5:6: generic type alias requires GOEXPERIMENT=aliastypeparams
</code></pre>
<p>我们看到：<strong>通过设置GOEXPERIMENT=featureflag可以开启对应的实验特性</strong>，如果要同时开启多个实验特性，可以用逗号分隔的实验特性列表，就像下面这样：</p>
<pre><code>$GOEXPERIMENT=featureflag1,featureflag2,...,featureflagN go build
</code></pre>
<p>那么如何查看当前Go版本有哪些实验验特性可用呢？我们可以借助<a href="https://tonybai.com/2024/09/06/go-doc-add-http-support/">go doc</a>工具，以go 1.23.0为例：</p>
<pre><code>$go doc goexperiment.Flags
package goexperiment // import "internal/goexperiment"

type Flags struct {
    FieldTrack        bool
    PreemptibleLoops  bool
    StaticLockRanking bool
    BoringCrypto      bool

    // RegabiWrappers enables ABI wrappers for calling between
    // ABI0 and ABIInternal functions. Without this, the ABIs are
    // assumed to be identical so cross-ABI calls are direct.
    RegabiWrappers bool
    // RegabiArgs enables register arguments/results in all
    // compiled Go functions.
    //
    // Requires wrappers (to do ABI translation), and reflect (so
    // reflection calls use registers).
    RegabiArgs bool

    // HeapMinimum512KiB reduces the minimum heap size to 512 KiB.
    //
    // This was originally reduced as part of PacerRedesign, but
    // has been broken out to its own experiment that is disabled
    // by default.
    HeapMinimum512KiB bool

    // CoverageRedesign enables the new compiler-based code coverage
    // tooling.
    CoverageRedesign bool

    // Arenas causes the "arena" standard library package to be visible
    // to the outside world.
    Arenas bool

    // CgoCheck2 enables an expensive cgo rule checker.
    // When this experiment is enabled, cgo rule checks occur regardless
    // of the GODEBUG=cgocheck setting provided at runtime.
    CgoCheck2 bool

    // LoopVar changes loop semantics so that each iteration gets its own
    // copy of the iteration variable.
    LoopVar bool

    // CacheProg adds support to cmd/go to use a child process to implement
    // the build cache; see https://github.com/golang/go/issues/59719.
    CacheProg bool

    // NewInliner enables a new+improved version of the function
    // inlining phase within the Go compiler.
    NewInliner bool

    // RangeFunc enables range over func.
    RangeFunc bool

    // AliasTypeParams enables type parameters for alias types.
    // Requires that gotypesalias=1 is set with GODEBUG.
    // This flag will be removed with Go 1.24.
    AliasTypeParams bool
}
    Flags is the set of experiments that can be enabled or disabled in the
    current toolchain.

    When specified in the GOEXPERIMENT environment variable or as build tags,
    experiments use the strings.ToLower of their field name.

    For the baseline experimental configuration, see objabi.experimentBaseline.

    If you change this struct definition, run "go generate".
</code></pre>
<p>go doc输出结果中的Flags结构体其实是$GOROOT/internal/goexperiment包中的一个类型，这个类型每一个字段对应一个实验特性，字段名的小写即可作为GOEXPERIMENT的值，比如AliasTypeParams的小写形式aliastypeparams正是我们在前面示例中使用的实验特性。</p>
<p>在Flags结构体中，我们看到了几个十分熟悉的字段，比如LoopVar、RangeFunc、Arenas等，这些实验特性有些已经正式落地，比如：<a href="https://tonybai.com/2023/08/20/some-changes-in-go-1-21/">Go 1.21</a>引入的<a href="https://go.dev/wiki/LoopvarExperiment">实验特性Loopvar</a>在<a href="https://tonybai.com/2024/02/18/some-changes-in-go-1-22">Go 1.22版本</a>中成为正式语法特性。而<a href="https://github.com/golang/go/issues/51317">Arenas</a>这个在<a href="https://tonybai.com/2023/02/08/some-changes-in-go-1-20">Go 1.20版本</a>引入的实验特性则因为实现上缺陷而迟迟不能转正，目前<a href="https://tonybai.com/2024/09/30/how-to-keep-up-with-go-evolution">处于proposal hold状态</a>。</p>
<p>Go对实验特性的引入分为两种情况：</p>
<ul>
<li>默认开启实验特性，无需在编译时通过GOEXPERIMENT=featureflag显式开启</li>
</ul>
<p>在Go 1.22中的exectracer2就是这样一个实验特性，它控制着是否使用新的<a href="https://tonybai.com/2021/06/28/understand-go-execution-tracer-by-example">execution trace</a>的实现。</p>
<p>对于这样的实验特性，我们可以通过GOEXPERIMENT=nofeatureflag对其进行显式关闭，以Go 1.22引入的实验特性ExecTracer2为例，可以使用下面命令关闭该实验特性：</p>
<pre><code>$GOEXPERIMENT=noexectracer2 go build
</code></pre>
<blockquote>
<p>注：之后使用go version your-go-app，可以看到“your-go-app: go1.22.0 X:noexectracer2”的输出。</p>
</blockquote>
<ul>
<li>默认不开启实验特性，需在编译时通过GOEXPERIMENT=featureflag显式开启</li>
</ul>
<p>这就是我们最熟悉的实验特性引入方式，Go 1.23的AliasTypeParams实验特性就是默认不开启的，前面的例子已经给出了开发方法，这里就不赘述了。</p>
<p>实验特性通常经过1到2个版本的实验便会落地，成为正式特性。已经落地的实验特性通常会从Flags结构体中移除，比如Go 1.22的goexperiment.Flags结构体中的ExecTracer2，在Go 1.23中就看不到了。但总有一些已经落地的实验特性对应的flag字段依然还留存在Flags结构体里，比如：LoopVar，<strong>这个原因还不得而知</strong>！并且这样的已经成为正式特性的Flag，我们也无法再通过GOEXPERIMENT=nofeatureflag对其进行显式关闭了，因为它已经不再是实验特性了！</p>
<p>不过有些实验特性即便转正落地了，也会考虑到新特性对legacy code行为的影响而去读取go.mod中的go version再决定是否应用新特性，比如LoopVar。LoopVar转正后，该特性也仅在编译的包来自于包含声明Go 1.22或更高版本的模块时适用，比如：Go 1.22或Go 1.23。这可以确保没有程序会因为简单地采用新的Go版本而改变行为，我们来看一个例子：</p>
<pre><code>// go.mod

module demo

go 1.20

// main.go
package main

import (
    "fmt"
    "time"
)

func main() {
    var m = [...]int{1, 2, 3, 4, 5}

    for i, v := range m {
        go func() {
            time.Sleep(time.Second * 3)
            fmt.Println(i, v)
        }()
    }

    time.Sleep(time.Second * 5)
}
</code></pre>
<p>我们使用go 1.23.0版本编译该包，并运行输出的程序：</p>
<pre><code>$go build
$./demo
4 5
4 5
4 5
4 5
4 5
</code></pre>
<p>可以看到，即便使用了Go 1.23版本，但因当前module的go version依然是go 1.20，Go编译器默认不会开启loopvar特性。</p>
<p>不过如果我们显式使用GOEXPERIMENT=loopvar，go编译器便不会考虑go.mod文件中的go version是什么版本，都会开启loopvar新特性：</p>
<pre><code>$GOEXPERIMENT=loopvar go build
$./demo
4 5
1 2
0 1
2 3
3 4
</code></pre>
<p>Go编译器会有一套Go试验特性的默认值，如果你通过GOEXPERIMENT显式开启了某些特性，导致该特性flag值与默认值不同，那么我们可以通过go version命令查看到这些不同之处。以上面GOEXPERIMENT=loopvar go build构建出的demo为例：</p>
<pre><code>$go version demo
demo: go1.23.0 X:loopvar
</code></pre>
<p>目前Go官方尚没有一个专门的页面用于汇总GOEXPERIMENT的各个flag的随Go版本release的历史，我们只能通过Flag字段在go issues查找其对应的issue来重温当时的情况。</p>
<p>到这里，我们可以看到GOEXPERIMENT引入的实验特性机制可以让Go团队相对稳健的向Go语言引入新特性（虽然不是所有新特性都需要走式样特性的流程，比如<a href="https://tonybai.com/2022/03/25/intro-generics">对泛型的支持</a>等），但是当新特性破坏了向后兼容，或者Go团队要对现有特性的错误语义(比如<a href="https://tonybai.com/2023/08/20/some-changes-in-go-1-21/">panicnil</a>)进行变更时，Go1这个严格的兼容性规则就很可能成为阻碍在大家面前的一道门槛！为了在保持兼容性和推动创新之间取得平衡，Go团队就需要一种新的机制，通过渐进式的方法来引入破坏性(break change)的变更，这就是GODEBUG控制机制，下面我们就来说说GODEBUG。</p>
<h2>2. GODEBUG：在运行时控制特性行为的开关</h2>
<p>GODEBUG也是一个Go环境变量，和GOEXPERIMENT用于构建时不同，GODEBUG用在运行时控制Go程序的某些行为。它允许开发者临时将某一特性恢复到旧的行为，即使在新版本中该特性的默认行为已经发生了改变。</p>
<p>GODEBUG的设置形式为逗号分隔的key=value对，例如：</p>
<pre><code>$GODEBUG=http2client=0,http2server=0 ./your-go-app
</code></pre>
<p>这个设置会禁用客户端和服务器端对HTTP/2的使用。</p>
<p>上面是使用GODEBUG禁用新特性的例子。对于存量特性语义或实现变更，比如Go 1.23版本对time.Timer和Ticker进行了重实现，新实现底层使用了无缓冲channel，但通过下面设置可以恢复原先实现中的带缓冲channel：</p>
<pre><code>$GODEBUG=asynctimerchan=1 ./your-go-app
</code></pre>
<p>考虑到兼容性而进行的GODEBUG设置将在至少两年（四个Go版本）内保持。但一些设置，例如http2client和http2server，将会更长时间地保持，甚至是无限期的。</p>
<p>除了GODEBUG环境变量之外，Go还提供了其他几种进行特性行为设置的方式，下面我们来看看。</p>
<h2>3. GODEBUG、go:debug和go.mod中godebug directive的关系</h2>
<h3>3.1. //go:debug指令</h3>
<p>从Go 1.21开始，可以在源代码中使用//go:debug指令来设置GODEBUG的值。这些指令必须放在文件的顶部，在package语句之前。例如：</p>
<pre><code>//go:debug panicnil=1
//go:debug asynctimerchan=0
package main
</code></pre>
<p>这些指令会在编译时被处理，并影响生成的二进制文件的行为。</p>
<h3>3.2 go.mod中的godebug指令</h3>
<p>从Go 1.23开始，可以在go.mod文件中使用godebug指令来设置GODEBUG的默认值，例如：</p>
<pre><code>// go.mod

godebug (
    default=go1.21
    panicnil=1
    asynctimerchan=0
)
</code></pre>
<p>这个配置会影响整个模块(module)的默认GODEBUG设置。</p>
<h3>3.3 优先级和应用范围</h3>
<p>那么GODEBUG、//go:debug以及go.mod中的godebug指令的优先级关系是怎样的呢？</p>
<p>显然，环境变量GODEBUG优先级最高，因为它可以在运行时覆盖其他设置，适用于临时调试或特定运行环境。</p>
<p>go:debug指令优先级次之，通常应用于特定的main包，适用于对特定程序进行精细控制。</p>
<p>而go.mod中的godebug指令优先级最低，为整个模块设置默认值，适用于项目级别的配置。</p>
<p>基于上述关系，我们来看看一个Go应用GODEBUG设置的默认值的确定过程。当没有显示设置GODEBUG环境变量时，各设置的默认值按以下顺序确定：</p>
<ul>
<li>首先查看用于构建程序的Go工具链(版本)的默认值。</li>
<li>然后根据go.mod或go.work中声明的Go版本(go version)进行调整。</li>
<li>之后应用go.mod中的godebug指令（如果有的话）。</li>
<li>最后是//go:debug，通常仅应用于main module。</li>
</ul>
<p>例如，如果一个项目的go.mod声明了go 1.20，那么即使使用Go 1.21工具链编译，也会默认使用panicnil=1（即允许panic(nil)）。</p>
<p>不过有特殊情况需要注意，比如对于声明早于Go 1.20版本的项目，GODEBUG默认值会被配置为匹配Go 1.20的行为，而不是更早的版本；又比如在测试环境中，&#42;_test.go文件中的//go:debug指令会被视为测试主包的指令等。</p>
<p>这么看规则还是蛮复杂的，那么编译后待执行的程序的默认GODEBUG的设置究竟是什么呢？我们可以通过go version -m来查看，以gopls v0.16.2为例：</p>
<pre><code>$go version -m /Users/tonybai/Go/bin/gopls
/Users/tonybai/Go/bin/gopls: go1.23.0
    path    golang.org/x/tools/gopls
    mod golang.org/x/tools/gopls    v0.16.2 h1:K1z03MlikHfaMTtG01cUeL5FAOTJnITuNe0TWOcg8tM=
    dep github.com/BurntSushi/toml  v1.2.1  h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
    dep github.com/google/go-cmp    v0.6.0  h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
    dep golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338  h1:2O2DON6y3XMJiQRAS1UWU+54aec2uopH3x7MAiqGW6Y=
    dep golang.org/x/mod    v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
    dep golang.org/x/sync   v0.8.0  h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
    dep golang.org/x/telemetry  v0.0.0-20240829154258-f29ab539cc98  h1:Wm3cG5X6sZ0RSVRc/H1/sciC4AT6HAKgLCSH2lbpR/c=
    dep golang.org/x/text   v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
    dep golang.org/x/tools  v0.22.1-0.20240829175637-39126e24d653   h1:6bJEg2w2kUHWlfdJaESYsmNfI1LKAZQi6zCa7LUn7eI=
    dep golang.org/x/vuln   v1.0.4  h1:SP0mPeg2PmGCu03V+61EcQiOjmpri2XijexKdzv8Z1I=
    dep honnef.co/go/tools  v0.4.7  h1:9MDAWxMoSnB6QoSqiVr7P5mtkT9pOc1kSxchzPCnqJs=
    dep mvdan.cc/gofumpt    v0.6.0  h1:G3QvahNDmpD+Aek/bNOLrFR2XC6ZAdo62dZu65gmwGo=
    dep mvdan.cc/xurls/v2   v2.5.0  h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
    build   -buildmode=exe
    build   -compiler=gc
    build   DefaultGODEBUG=asynctimerchan=1,gotypesalias=0,httplaxcontentlength=1,httpmuxgo121=1,httpservecontentkeepheaders=1,panicnil=1,tls10server=1,tls3des=1,tlskyber=0,tlsrsakex=1,tlsunsafeekm=1,winreadlinkvolume=0,winsymlink=0,x509keypairleaf=0,x509negativeserial=1
    build   CGO_ENABLED=1
    build   CGO_CFLAGS=
    build   CGO_CPPFLAGS=
    build   CGO_CXXFLAGS=
    build   CGO_LDFLAGS=
    build   GOARCH=amd64
    build   GOOS=darwin
    build   GOAMD64=v1

</code></pre>
<p>我们看到其DefaultGODEBUG如下：</p>
<pre><code>DefaultGODEBUG=asynctimerchan=1,gotypesalias=0,httplaxcontentlength=1,httpmuxgo121=1,httpservecontentkeepheaders=1,panicnil=1,tls10server=1,tls3des=1,tlskyber=0,tlsrsakex=1,tlsunsafeekm=1,winreadlinkvolume=0,winsymlink=0,x509keypairleaf=0,x509negativeserial=1
</code></pre>
<p>相对于GOEXPERIMENT的flags的数量，GODEBUG的设置项更多，下面我们根据go官方资料整理一个GODEBUG设置项列表供大家参考（信息截至2024.10.7）。</p>
<h2>4. GODEBUG设置的历史演进</h2>
<p>下表按照Go版本顺序列出了各个GODEBUG设置，包括它们被引入的版本、含义以及如何开启和关闭它们：</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-evolution-dual-insurance-goexperiment-godebug-2.png" alt="" /></p>
<p>不过请注意以下几点：</p>
<ul>
<li>默认值可能会随着Go版本的更新而改变。</li>
<li>某些设置可能在未来的Go版本中被移除。</li>
<li>部分设置（如tlsmaxrsasize）允许指定具体的数值，而不仅仅是0或1。</li>
<li>有些设置（如multipartmaxheaders和multipartmaxparts）在默认情况下是无限制的，需要明确设置一个数值来启用限制。</li>
</ul>
<h2>5. 小结</h2>
<p>在Go语言的演进过程中，GOEXPERIMENT和GODEBUG两个机制起到了至关重要的作用。GOEXPERIMENT为新特性的实验和测试提供了灵活的环境，使得开发者可以在正式发布之前尝试和反馈新功能，从而确保Go语言的创新不会影响到已有代码的稳定性。通过这种方式，Go团队能够逐步引入新特性，同时维持向后兼容性。</p>
<p>另一方面，GODEBUG则为开发者提供了在运行时控制特性行为的工具，使得新版本引入的破坏性更改能够被临时禁用。这种灵活性使得开发者有一个平滑过渡的机会，能够在更新的同时，保证应用的平稳运行，避免了因语言更新而导致的潜在问题，使Go能够在保持稳定性的同时不断创新。</p>
<p>总的来说，这两个机制共同构成了Go语言特性发布的“双保险”，确保了语言的持续发展与稳定性之间的平衡。这一策略不仅促进了Go语言的创新，也增强了开发者的信心，使其能够在不断变化的环境中有效地编写和维护代码。</p>
<h2>6. 参考资料</h2>
<ul>
<li><a href="https://go.dev/doc/godebug">Go, Backwards Compatibility, and GODEBUG</a> &#8211; https://go.dev/doc/godebug</li>
<li><a href="https://go.googlesource.com/proposal/+/master/design/56986-godebug.md">Proposal: Extended backwards compatibility for Go</a> &#8211; https://go.googlesource.com/proposal/+/master/design/56986-godebug.md</li>
</ul>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
<li>Gopher Daily Feed订阅 &#8211; https://gopherdaily.tonybai.com/feed</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2024, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2024/10/11/go-evolution-dual-insurance-goexperiment-godebug/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go未用代码消除与可执行文件瘦身</title>
		<link>https://tonybai.com/2024/05/05/dead-code-elimination-and-executable-file-slimming-in-go/</link>
		<comments>https://tonybai.com/2024/05/05/dead-code-elimination-and-executable-file-slimming-in-go/#comments</comments>
		<pubDate>Sat, 04 May 2024 22:31:11 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[dead-code-elimination]]></category>
		<category><![CDATA[dependency]]></category>
		<category><![CDATA[deps]]></category>
		<category><![CDATA[gcflags]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[go.sum]]></category>
		<category><![CDATA[gobuild]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GOROOT]]></category>
		<category><![CDATA[Inline]]></category>
		<category><![CDATA[link]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[Reflect]]></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=4151</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/05/05/dead-code-elimination-and-executable-file-slimming-in-go 在日常编写Go代码时，我们会编写很多包，也会在编写的包中引入了各种依赖包。在大型Go工程中，这些直接依赖和间接依赖的包数目可能会有几十个甚至上百个。依赖包有大有小，但通常我们不会使用到依赖包中的所有导出函数或类型方法。 这时Go初学者就会有一个疑问：这些直接依赖包和间接依赖包中的所有代码是否会进入到最终的可执行文件中呢？即便我们只是使用了某个依赖包中的一个导出函数。 这里先给出结论：不会！在这篇文章中，我们就来探索一下这个话题，了解一下其背后的支撑机制以及对Go可执行文件Size的影响。 1. 实验：哪些函数进入到最终的可执行文件中了？ 我们先来做个实验，验证一下究竟哪些函数进入到最终的可执行文件中了！我们建立demo1，其目录结构和部分代码如下： // dead-code-elimination/demo1 $tree -F . . ├── go.mod ├── main.go └── pkga/ └── pkga.go // main.go package main import ( "fmt" "demo/pkga" ) func main() { result := pkga.Foo() fmt.Println(result) } // pkga/pkga.go package pkga import ( "fmt" ) func Foo() string { return "Hello from [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/dead-code-elimination-and-executable-file-slimming-in-go-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/05/05/dead-code-elimination-and-executable-file-slimming-in-go">本文永久链接</a> &#8211; https://tonybai.com/2024/05/05/dead-code-elimination-and-executable-file-slimming-in-go</p>
<p>在日常编写Go代码时，我们会编写很多包，也会在编写的包中引入了各种依赖包。在大型Go工程中，这些直接依赖和间接依赖的包数目可能会有几十个甚至上百个。依赖包有大有小，但通常我们不会使用到依赖包中的所有导出函数或类型方法。</p>
<p>这时Go初学者就会有一个疑问：这些直接依赖包和间接依赖包中的所有代码是否会进入到最终的可执行文件中呢？即便我们只是使用了某个依赖包中的一个导出函数。</p>
<p>这里先给出结论：不会！在这篇文章中，我们就来探索一下这个话题，了解一下其背后的支撑机制以及对Go可执行文件Size的影响。</p>
<h2>1. 实验：哪些函数进入到最终的可执行文件中了？</h2>
<p>我们先来做个实验，验证一下究竟哪些函数进入到最终的可执行文件中了！我们建立demo1，其目录结构和部分代码如下：</p>
<pre><code>// dead-code-elimination/demo1
$tree -F .
.
├── go.mod
├── main.go
└── pkga/
    └── pkga.go

// main.go
package main

import (
    "fmt"

    "demo/pkga"
)

func main() {
    result := pkga.Foo()
    fmt.Println(result)
}

// pkga/pkga.go

package pkga

import (
    "fmt"
)

func Foo() string {
    return "Hello from Foo!"
}

func Bar() {
    fmt.Println("This is Bar.")
}
</code></pre>
<p>这个示例十分简单！main函数中调用了pkga包的导出函数Foo，而pkga包中除了Foo函数，还有Bar函数(但并没有被任何其他函数调用)。现在我们来编译一下这个module，然后查看一下编译出的可执行文件中都包含pkga包的哪些函数！(本文实验中使用的<a href="https://tonybai.com/2024/02/18/some-changes-in-go-1-22/">Go为1.22.0版本</a>)</p>
<pre><code>$go build
$go tool nm demo|grep demo
</code></pre>
<p>在输出的可执行文件中，居然没有查到关于pkga的任何符号信息，这可能是Go的优化在“作祟”。我们关闭掉Go编译器的优化后，再来试试：</p>
<pre><code>$go build -gcflags '-l -N'
$go tool nm demo|grep demo
 108ca80 T demo/pkga.Foo
</code></pre>
<p>关掉<a href="https://tonybai.com/2022/10/17/understand-go-inlining-optimisations-by-example">内联优化</a>后，我们看到pkga.Foo出现在最终的可执行文件demo中，但并未被调用的Bar函数并没有进入可执行文件demo中。</p>
<p>我们再来看一下有间接依赖的例子：</p>
<pre><code>// dead-code-elimination/demo2
$tree .
.
├── go.mod
├── main.go
├── pkga
│   └── pkga.go
└── pkgb
    └── pkgb.go

// pkga/pkga.go
package pkga

import (
    "demo/pkgb"
    "fmt"
)

func Foo() string {
    pkgb.Zoo()
    return "Hello from Foo!"
}

func Bar() {
    fmt.Println("This is Bar.")
}
</code></pre>
<p>在这个示例中，我们在pkga.Foo函数中又调用了一个新包pkgb的Zoo函数，我们来编译一下该新示例并查看一下哪些函数进入到最终的可执行文件中：</p>
<pre><code>$go build -gcflags='-l -N'
$go tool nm demo|grep demo
 1093b40 T demo/pkga.Foo
 1093aa0 T demo/pkgb.Zoo
</code></pre>
<p>我们看到：只有程序执行路径上能够触达（被调用）的函数才会进入到最终的可执行文件中！</p>
<p>在复杂的示例中，我们也可以通过带有-ldflags=&#8217;-dumpdep&#8217;的go build命令来查看这种调用依赖关系(这里以demo2为例)：</p>
<pre><code>$go build -ldflags='-dumpdep' -gcflags='-l -N' &gt; deps.txt 2&gt;&amp;1

$grep demo deps.txt
# demo
main.main -&gt; demo/pkga.Foo
demo/pkga.Foo -&gt; demo/pkgb.Zoo
demo/pkga.Foo -&gt; go:string."Hello from Foo!"
demo/pkgb.Zoo -&gt; math/rand.Int31n
demo/pkgb.Zoo -&gt; demo/pkgb..stmp_0
demo/pkgb..stmp_0 -&gt; go:string."Zoo in pkgb"
</code></pre>
<p>到这里，我们知道了Go通过某种机制保证了只有真正使用到的代码才会最终进入到可执行文件中，即便某些代码（比如pkga.Bar）和那些被真正使用的代码（比如pkga.Foo）在同一个包内。这同时保证了最终可执行文件大小在可控范围内。</p>
<p>接下来，我们就来看看Go的这种机制。</p>
<h2>2. 未用代码消除(dead code elimination)</h2>
<p>我们先来复习一下go build的构建过程，以下是 go build 命令的步骤概述：</p>
<ol>
<li>
<p>读取go.mod和go.sum：如果当前目录包含go.mod文件，go build会读取该文件以确定项目的依赖项。它还会根据go.sum文件中的校验和验证依赖项的完整性。</p>
</li>
<li>
<p>计算包依赖图：go build 分析正在构建的包及其依赖项中的导入语句，以构建依赖图。该图表示包之间的关系，使编译器能够确定包的构建顺序。</p>
</li>
<li>
<p>决定要构建的包：基于构建缓存和依赖图，go build 确定需要构建的包。它检查构建缓存，以查看已编译的包是否是最新的。如果自上次构建以来某个包或其依赖项发生了更改，go build将重新构建这些包。</p>
</li>
<li>
<p>调用编译器（go tool compile）：对于每个需要构建的包，go build调用Go编译器（go tool compile）。编译器将Go源代码转换为特定目标平台的机器码，并生成目标文件（.o 文件）。</p>
</li>
<li>
<p>调用链接器（go tool link）：在编译所有必要的包之后，go build 调用 Go 链接器（go tool link）。链接器将编译器生成的目标文件合并为可执行二进制文件或包归档文件。它解析包之间的符号和引用，执行必要的重定位，并生成最终的输出。</p>
</li>
</ol>
<p>上述的整个构建过程可以由下图表示：</p>
<p><img src="https://tonybai.com/wp-content/uploads/dead-code-elimination-and-executable-file-slimming-in-go-2.png" alt="" /></p>
<p>在构建过程中，go build 命令还执行各种优化，例如未用代码消除和内联，以提高生成二进制文件的性能和降低二进制文件的大小。其中的未用代码消除就是保证Go生成的二进制文件大小可控的重要机制。</p>
<p>未用检测算法的实现位于$GOROOT/src/cmd/link/internal/ld/deadcode.go文件中。该算法通过图遍历的方式进行，具体过程如下：</p>
<ol>
<li>从系统的入口点开始，标记所有可通过重定位到达的符号。重定位是两个符号之间的依赖关系。</li>
<li>通过遍历重定位关系，算法标记所有可以从入口点访问到的符号。例如，在主函数main.main中调用了pkga.Foo函数，那么main.main会有对这个函数的重定位信息。</li>
<li>标记完成后，算法会将所有未被标记的符号标记为不可达的未用。这些未被标记的符号表示不会被入口点或其他可达符号访问到的代码。</li>
</ol>
<p>不过，这里有一个特殊的语法元素要注意，那就是带有方法的类型。类型的方法是否进入到最终的可执行文件中，需要考虑不同情况。在deadcode.go，用于标记可达符号的函数实现将可达类型的方法的调用方式分为三种：</p>
<ol>
<li>直接调用</li>
<li>通过可到达的接口类型调用</li>
<li>通过反射调用：reflect.Value.Method（或 MethodByName）或 reflect.Type.Method（或 MethodByName）</li>
</ol>
<p>第一种情况，可以直接将调用的方法被标记为可到达。第二种情况通过将所有可到达的接口类型分解为方法签名来处理。遇到的每个方法都与接口方法签名进行比较，如果匹配，则将其标记为可到达。这种方法非常保守，但简单且正确。</p>
<p>第三种情况通过寻找编译器标记为REFLECTMETHOD的函数来处理。函数F上的REFLECTMETHOD意味着F使用反射进行方法查找，但编译器无法在静态分析阶段确定方法名。因此所有调用reflect.Value.Method 或reflect.Type.Method的函数都是REFLECTMETHOD。调用reflect.Value.MethodByName或reflect.Type.MethodByName且参数为非常量的函数也是REFLECTMETHOD。如果我们找到了REFLECTMETHOD，就会放弃静态分析，并将所有可到达类型的导出方法标记为可达。</p>
<p>下面是一个来自参考资料中的示例：</p>
<pre><code>// dead-code-elimination/demo3/main.go

type X struct{}
type Y struct{}

func (*X) One()   { fmt.Println("hello 1") }
func (*X) Two()   { fmt.Println("hello 2") }
func (*X) Three() { fmt.Println("hello 3") }
func (*Y) Four()  { fmt.Println("hello 4") }
func (*Y) Five()  { fmt.Println("hello 5") }

func main() {
    var name string
    fmt.Scanf("%s", &amp;name)
    reflect.ValueOf(&amp;X{}).MethodByName(name).Call(nil)
    var y Y
    y.Five()
}
</code></pre>
<p>在这个示例中，类型&#42;X有三个方法，类型&#42;Y有两个方法，在main函数中，我们通过反射调用X实例的方法，通过Y实例直接调用Y的方法，我们看看最终X和Y都有哪些方法进入到最后的可执行文件中了：</p>
<pre><code>$go build -gcflags='-l -N'

$go tool nm ./demo|grep main
 11d59c0 D go:main.inittasks
 10d4500 T main.(*X).One
 10d4640 T main.(*X).Three
 10d45a0 T main.(*X).Two
 10d46e0 T main.(*Y).Five
 10d4780 T main.main
... ...
</code></pre>
<p>我们看到通过直接调用的可达类型Y只有代码中直接调用的方法Five进入到最终可执行文件中，而通过反射调用的X的所有方法都可以在最终可执行文件找到！这与前面提到的第三种情况一致。</p>
<h2>3. 小结</h2>
<p>本文介绍了Go语言中的未用代码消除和可执行文件瘦身机制。通过实验验证，只有在程序执行路径上被调用的函数才会进入最终的可执行文件，未被调用的函数会被消除。</p>
<p>本文解释了Go编译过程，包括包依赖图计算、编译和链接等步骤，并指出未用代码消除是其中的重要优化策略。具体的未用代码消除算法是通过图遍历实现的，标记可达的符号并将未被标记的符号视为未用。文章还提到了对类型方法的处理方式。</p>
<p>通过这种未用代码消除机制，Go语言能够控制最终可执行文件的大小，实现可执行文件瘦身。</p>
<p>本文涉及的源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/dead-code-elimination">这里</a>下载。</p>
<h2>4. 参考资料</h2>
<ul>
<li><a href="https://golab.io/talks/getting-the-most-out-of-dead-code-elimination">Getting the most out of Dead Code elimination</a> &#8211; https://golab.io/talks/getting-the-most-out-of-dead-code-elimination</li>
<li><a href="https://github.com/golang/go/issues/6853">all: binaries too big and growing</a> &#8211; https://github.com/golang/go/issues/6853</li>
<li><a href="https://github.com/aarzilli/whydeadcode">aarzilli/whydeadcode</a> &#8211; https://github.com/aarzilli/whydeadcode</li>
</ul>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2024, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2024/05/05/dead-code-elimination-and-executable-file-slimming-in-go/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go 1.20新特性前瞻</title>
		<link>https://tonybai.com/2022/11/17/go-1-20-foresight/</link>
		<comments>https://tonybai.com/2022/11/17/go-1-20-foresight/#comments</comments>
		<pubDate>Thu, 17 Nov 2022 13:45:59 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[arena]]></category>
		<category><![CDATA[cache]]></category>
		<category><![CDATA[Cgo]]></category>
		<category><![CDATA[coverage]]></category>
		<category><![CDATA[crypto]]></category>
		<category><![CDATA[err]]></category>
		<category><![CDATA[error]]></category>
		<category><![CDATA[fmt]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-build]]></category>
		<category><![CDATA[go-install]]></category>
		<category><![CDATA[go.dev]]></category>
		<category><![CDATA[Go1]]></category>
		<category><![CDATA[go1.18]]></category>
		<category><![CDATA[go1.19]]></category>
		<category><![CDATA[go1.20]]></category>
		<category><![CDATA[Go2]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GOROOT]]></category>
		<category><![CDATA[gotest]]></category>
		<category><![CDATA[gRPC]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[library]]></category>
		<category><![CDATA[optimization]]></category>
		<category><![CDATA[optimize]]></category>
		<category><![CDATA[panic]]></category>
		<category><![CDATA[PGO]]></category>
		<category><![CDATA[pkg]]></category>
		<category><![CDATA[playground]]></category>
		<category><![CDATA[pprof]]></category>
		<category><![CDATA[proposal]]></category>
		<category><![CDATA[protobuf]]></category>
		<category><![CDATA[RFC3339]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[Slice]]></category>
		<category><![CDATA[stdlib]]></category>
		<category><![CDATA[strings]]></category>
		<category><![CDATA[template]]></category>
		<category><![CDATA[time]]></category>
		<category><![CDATA[unit-test]]></category>
		<category><![CDATA[Unwrap]]></category>
		<category><![CDATA[wrap]]></category>
		<category><![CDATA[优化]]></category>
		<category><![CDATA[内存管理]]></category>
		<category><![CDATA[切片]]></category>
		<category><![CDATA[发行版]]></category>
		<category><![CDATA[垃圾回收]]></category>
		<category><![CDATA[工具链]]></category>
		<category><![CDATA[数组]]></category>
		<category><![CDATA[标准库]]></category>
		<category><![CDATA[泛型]]></category>
		<category><![CDATA[编译器]]></category>
		<category><![CDATA[语法]]></category>
		<category><![CDATA[运行时]]></category>
		<category><![CDATA[链接器]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3725</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2022/11/17/go-1-20-foresight 在近期Russ Cox代表Go核心团队发表的“Go, 13周年”一文中，他提到了“在Go的第14个年头，Go团队将继续努力使Go成为用于大规模软件工程的最好的环境，将特别关注供应链安全，提高兼容性和结构化日志记录，当然还会有很多其他改进，包括profile-guided optimization等”。 当前正在开发的版本是Go 1.20，预计2023年2月正式发布，这个版本也将是Go在其第14个年头发布的第一个版本。很多人没想到Go真的会进入到Go 1.2x版本，而不是Go 2.x。记得Russ Cox曾说过可能永远也不会有Go2了，毕竟Go泛型语法落地这么大的语法改动也没有让Go1兼容性承诺失效。 目前Go 1.20版本正在如火如荼的开发中，很多gopher都好奇Go 1.20版本会带来哪些新特性？在这篇文章中，我就带大家一起去Go 1.20 milestone的issues列表中翻翻，提前看看究竟会有哪些新特性加入Go。 1. 语法变化 Go在其1.18版本迎来了自开源以来最大规模的语法变化，然后呢？就没有然后了。Go在语法演进上再次陷入沉寂，没错，这就是Go长期以来坚持的风格。 如果Go 1.20版本真有语法层面的变化，那估计就是这个issue了：“spec: allow conversion from slice to array”，即允许切片类型到数组类型的类型转换。 在Go 1.20版本之前，我们以Go 1.19版本为例写下下面代码： package main import "fmt" func main() { var sl = []int{1, 2, 3, 4, 5, 6, 7} var arr = [7]int(sl) // 编译器报错：cannot convert [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/go-1-20-foresight-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2022/11/17/go-1-20-foresight">本文永久链接</a> &#8211; https://tonybai.com/2022/11/17/go-1-20-foresight</p>
<hr />
<p>在近期Russ Cox代表Go核心团队发表的<a href="https://tonybai.com/2022/11/11/go-opensource-13-years">“Go, 13周年”</a>一文中，他提到了“在Go的第14个年头，Go团队将继续努力使Go成为用于大规模软件工程的最好的环境，将特别关注供应链安全，提高兼容性和结构化日志记录，当然还会有很多其他改进，包括profile-guided optimization等”。</p>
<p>当前正在开发的版本是Go 1.20，预计2023年2月正式发布，这个版本也将是Go在其第14个年头发布的第一个版本。很多人没想到Go真的会进入到Go 1.2x版本，而不是Go 2.x。记得Russ Cox曾说过可能永远也不会有Go2了，毕竟<a href="https://tonybai.com/2022/04/20/some-changes-in-go-1-18">Go泛型语法落地</a>这么大的语法改动也没有让<a href="https://go.dev/doc/go1compat">Go1兼容性承诺</a>失效。</p>
<p>目前Go 1.20版本正在如火如荼的开发中，很多gopher都好奇Go 1.20版本会带来哪些新特性？在这篇文章中，我就带大家一起去<a href="https://github.com/golang/go/milestone/250">Go 1.20 milestone</a>的issues列表中翻翻，提前看看究竟会有哪些新特性加入Go。</p>
<h3>1. 语法变化</h3>
<p>Go在其<a href="https://tonybai.com/2022/04/20/some-changes-in-go-1-18">1.18版本</a>迎来了自开源以来最大规模的语法变化，然后呢？就没有然后了。Go在语法演进上再次陷入沉寂，没错，这就是Go长期以来坚持的风格。</p>
<p>如果Go 1.20版本真有语法层面的变化，那估计就是这个issue了：<a href="https://github.com/golang/go/issues/46505">“spec: allow conversion from slice to array”</a>，即<strong>允许切片类型到数组类型的类型转换</strong>。</p>
<p>在Go 1.20版本之前，我们以<a href="https://tonybai.com/2022/08/22/some-changes-in-go-1-19">Go 1.19版本</a>为例写下下面代码：</p>
<pre><code>package main

import "fmt"

func main() {
    var sl = []int{1, 2, 3, 4, 5, 6, 7}
    var arr = [7]int(sl) // 编译器报错：cannot convert sl (variable of type []int) to type [7]int
    fmt.Println(sl)
    fmt.Println(arr)
}
</code></pre>
<p>这段代码中，我们进行了一个[]int到[7]int的类型转换，但在Go 1.19版本编译器针对这个转换会报错！即不支持将切片类型显式转换数组类型。</p>
<p>在Go 1.20版本之前如果要实现切片到数组的转换，是有trick的，看下面代码：</p>
<pre><code>func main() {
    var sl = []int{1, 2, 3, 4, 5, 6, 7}
    var parr = (*[7]int)(sl)
    var arr = *(*[7]int)(sl)
    fmt.Println(sl)  // [1 2 3 4 5 6 7]
    fmt.Println(arr) // [1 2 3 4 5 6 7]
    sl[0] = 11
    fmt.Println(sl)    // [11 2 3 4 5 6 7]
    fmt.Println(arr)   // [1 2 3 4 5 6 7]
    fmt.Println(*parr) // [11 2 3 4 5 6 7]
}
</code></pre>
<p>该trick的理论基础是Go允许获取切片的底层数组地址。在上面的例子中parr就是指向切片sl底层数组的指针，通过sl或parr对底层数组元素的修改都能在对方身上体现出来。但是arr则是底层数组的一个副本，后续通过sl对切片的修改或通过parr对底层数组的修改都不会影响arr，反之亦然。</p>
<p>不过这种trick语法还不是那么直观！于是上面那个“允许将切片直接转换为数组”的issue便提了出来。我们在<a href="https://go.dev/play">go playground</a>上选择“go dev branch”便可以使用最新go tip的代码，我们尝试一下最新语法：</p>
<pre><code>func main() {
    var sl = []int{1, 2, 3, 4, 5, 6, 7}
    var arr = [7]int(sl)
    var parr = (*[7]int)(sl)
    fmt.Println(sl)   // [1 2 3 4 5 6 7]
    fmt.Println(arr)  // [1 2 3 4 5 6 7]
    sl[0] = 11
    fmt.Println(arr)  // [1 2 3 4 5 6 7]
    fmt.Println(parr) // &amp;[11 2 3 4 5 6 7]
}
</code></pre>
<p>我们看到直接将sl转换为数组arr不再报错，但其语义与前面的“var arr = <em>(</em>[7]int)(sl)”语义是相同的，即返回一个切片底层数组的副本，arr不会受到后续切片元素变化的影响。</p>
<p>不过这里也有个约束，那就是转换后的数组长度要小于等于切片长度，否则会panic：</p>
<pre><code>var sl = []int{1, 2, 3, 4, 5, 6, 7}
var arr = [8]int(sl) // panic: runtime error: cannot convert slice with length 7 to array or pointer to array with length 8
</code></pre>
<p>在写本文时，该issue尚未close，不过进入最终Go 1.20版本应该不是大问题。</p>
<h3>2. 编译器/链接器和其他工具链</h3>
<h4>1) profile-guided optimization</h4>
<p>Go编译器团队一直致力于对Go编译器/链接器的优化，这次在Go 1.20版本中，该团队很大可能会给我们带来<a href="https://github.com/golang/proposal/blob/master/design/55022-pgo.md">“profile-guided optimization”</a>。</p>
<p>什么是“profile-guided optimization”呢？原先Go编译器实施的优化手段，比如<a href="https://tonybai.com/2022/10/17/understand-go-inlining-optimisations-by-example">内联</a>，都是基于固定规则决策的，所有信息都来自编译的Go源码。而这次的<a href="https://github.com/golang/go/issues/55022">“profile-guided optimization”</a>顾名思义，需要源码之外的信息做“制导”来决定实施哪些优化，这个源码之外的信息就是profile信息，即来自pprof工具在程序运行时采集的数据，如下图(图来自<a href="https://github.com/golang/proposal/blob/master/design/55022-pgo-implementation.md">profile-guided optimization设计文档</a>)所示:</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-1-20-foresight-2.png" alt="" /></p>
<p>因此pgo优化实际上是需要程序员参与的，程序员拿着程序到生产环境跑，程序生成的profile性能采集数据会被保存下来，然后这些profile采集数据会提供给Go编译器，以在下次构建同一个程序时辅助优化决策。由于这些profile是来自生产环境或模拟生产环境的数据，使得这种优化更有针对性。并且，Google数据中心其他语言(C/C++)实施PGO优化的效果显示，优化后的性能保守估计提升幅度在5%~15%。</p>
<p>和其他新引入的特性一样，Go 1.20将包含该特性，但默认并不开启，我们可以手动开启进行体验，未来版本，pgo特性才会默认为auto开启。</p>
<h4>2) 大幅减小Go发行版包的Size</h4>
<p>随着Go语言的演进，Go发行版的Size也在不断增加，从最初的几十M到如今的上百M。本地电脑里多安装几个Go版本，(解压后)几个G就没有了，此外Size大也让下载时间变得更长，尤其是一些网络环境不好的地区。</p>
<p>为什么Go发行版Size越来越大呢？这很大程度是因为Go发行版中包含了GOROOT下所有软件包的预编译.a文件，以go 1.19的macos版本为例，在\$GOROOT/pkg下，我们看到下面这些.a文件，用du查看一下占用的磁盘空间，达111M：</p>
<pre><code>$ls
archive/    database/   fmt.a       index/      mime/       plugin.a    strconv.a   time/
bufio.a     debug/      go/     internal/   mime.a      reflect/    strings.a   time.a
bytes.a     embed.a     hash/       io/     net/        reflect.a   sync/       unicode/
compress/   encoding/   hash.a      io.a        net.a       regexp/     sync.a      unicode.a
container/  encoding.a  html/       log/        os/     regexp.a    syscall.a   vendor/
context.a   errors.a    html.a      log.a       os.a        runtime/    testing/
crypto/     expvar.a    image/      math/       path/       runtime.a   testing.a
crypto.a    flag.a      image.a     math.a      path.a      sort.a      text/

$du -sh
111M    .
</code></pre>
<p>而整个pkg目录的size为341M，占Go 1.19版本总大小495M的近70%。</p>
<p>于是在Go社区提议下，<a href="https://github.com/golang/go/issues/47257">Go团队决定从Go 1.20开始发行版不再为GOROOT中的大多数软件包提供预编译的.a文件</a>，新版本将只包括GOROOT中使用cgo的几个软件包的.a文件。</p>
<p>因此Go 1.20版本中，GOROOT下的源码将像其他用户包那样在构建后被缓存到本机cache中。此外，go install也不会为GOROOT软件包安装.a文件，除非是那些使用cgo的软件包。这样Go发行版的size将最多减少三分之二。</p>
<p>取而代之的是，这些包将在需要时被构建并缓存在构建缓存中，就像已经为GOROOT之外的非主包所做的那样。此外，go install也不会为GOROOT软件包安装.a文件，除非是那些使用cgo的软件包。这些改变是为了减少Go发行版的大小，在某些情况下可以减少三分之二。</p>
<h4>3) 扩展代码覆盖率(coverage)报告到应用本身</h4>
<p>想必大家都用过go test的输出过代码覆盖率，go test会在unit test代码中<strong>注入代码</strong>以统计unit test覆盖的被测试包路径，下面是代码注入的举例：</p>
<pre><code>func ABC(x int) {
    if x &lt; 0 {
        bar()
    }
}
</code></pre>
<p>注入代码后：</p>
<pre><code>func ABC(x int) {GoCover_0_343662613637653164643337.Count[9] = 1;
  if x &lt; 0 {GoCover_0_343662613637653164643337.Count[10] = 1;
    bar()
  }
}
</code></pre>
<p>像GoCover_xxx这样的代码会被放置到每条分支路径下。</p>
<p>不过go test -cover也有一个问题，那就是它只是适合针对包收集数据并提供报告，它无法针对应用整体给出代码覆盖度报告。</p>
<p>Go 1.20版本中有关的<a href="https://github.com/golang/proposal/blob/master/design/51430-revamp-code-coverage.md">“extend code coverage testing to include applications”</a>的proposal就是来扩展代码覆盖率的，可以支持对应用整体的覆盖率统计和报告。</p>
<p>该特性在Go 1.20版本中也将作为实验性特性，默认是off的。该方案通过go build -cover方式生成注入了覆盖率统计代码的应用程序，在应用执行过程中，报告会被生成到指定目录下，我们依然可以通过go tool cover来查看这个整体性报告。</p>
<p>此外，新proposal在实现原理上与go test -cover差不多，都是source-to-source的方案，这样后续也可以统一维护。当然Go编译器也会有一些改动。</p>
<h4>4) 废弃-i flag</h4>
<p><a href="https://github.com/golang/go/issues/41696">这个是一个早计划好的“废弃动作”</a>。自从Go 1.10引入go build cache后，go build/install/test -i就不会再将编译好的包安装到\$GOPATH/pkg下面了。</p>
<h3>3. Go标准库</h3>
<h4>1) 支持wrap multiple errors</h4>
<p>Go 1.20<a href="https://github.com/golang/go/issues/53435#issuecomment-1191752789">增加了一种将多个error包装(wrap)为一个error的机制</a>，方便从打包后的错误的Error方法中一次性得到包含一系列关于该错误的相关错误的信息。</p>
<p>这个机制增加了一个(匿名)接口和一个函数：</p>
<pre><code>interface {
    Unwrap() []error
}

func Join(errs ...error) error
</code></pre>
<p>同时增强了像fmt.Errorf这样的函数的语义，当在Errorf中使用多个%w verb时，比如：</p>
<pre><code>e := errors.Errorf("%w, %w, %w", e1, e2, e3)
</code></pre>
<p>Errorf将返回一个将e1, e2, e3打包完的且实现了上述带有Unwrap() &#91;&#93;error方法的接口的错误类型实例。</p>
<p>Join函数的语义是将传入的所有err打包成一个错误类型实例，该实例同样实现了上述带有Unwrap() &#91;&#93;error方法的接口，且该错误实例的类型的Error方法会返回换行符间隔的错误列表。</p>
<p>我们看一下下面这个例子：</p>
<pre><code>package main

import (
    "errors"
    "fmt"
)

type MyError struct {
    s string
}

func (e *MyError) Error() string {
    return e.s
}

func main() {
    e1 := errors.New("error1")
    e2 := errors.New("error2")
    e3 := errors.New("error3")
    e4 := &amp;MyError{
        s: "error4",
    }
    e := fmt.Errorf("%w, %w, %w, %w", e1, e2, e3, e4)

    fmt.Printf("e = %s\n", e.Error()) // error1 error2, error3, error4
    fmt.Println(errors.Is(e, e1)) // true

    var ne *MyError
    fmt.Println(errors.As(e, &amp;ne)) // true
    fmt.Println(ne == e4) // true
}
</code></pre>
<p>我们首先在Go 1.19编译运行上面程序：</p>
<pre><code>e = error1 %!w(*errors.errorString=&amp;{error2}), %!w(*errors.errorString=&amp;{error3}), %!w(*main.MyError=&amp;{error4})
false
false
false
</code></pre>
<p>显然Go 1.19的fmt.Errorf函数尚不支持多%w verb。</p>
<p>而Go 1.20编译上面程序的运行结果为：</p>
<pre><code>e = error1 error2, error3, error4
true
true
true
</code></pre>
<p>将fmt.Errorf一行换为：</p>
<pre><code>e := errors.Join(e1, e2, e3, e4)
</code></pre>
<p>再运行一次的结果为：</p>
<pre><code>e = error1
error2
error3
error4
true
true
true
</code></pre>
<p>即Join函数打包后的错误类型实例类型的Error方法会返回换行符间隔的错误列表。</p>
<h4>2) 新增arena实验包</h4>
<p>Go是带GC语言，虽然Go GC近几年持续改进，绝大多数场合都不是大问题了。但是在一些性能敏感的领域，GC过程占用的可观算力还是让应用吃不消。</p>
<p>降GC消耗，主要思路就是减少堆内存分配、减少反复的分配与释放。Go社区的某些项目为了减少内存GC压力，在mmaped内存上又建立一套GC无法感知到的简单内存管理机制并在适当场合应用。但这些自实现的、脱离GC的内存管理都有各自的问题。</p>
<p>Go 1.18版本发布前，<a href="https://github.com/golang/go/issues/51317">arena这个proposal</a>就被提上了日程，arena包又是google内部的一个实验包，据说效果还不错的(在改进grpc的protobuf反序列化实验上)，可以节省15%的cpu和内存消耗。但proposal一出，便收到了来自各方的comment，该proposal在Go 1.18和Go 1.19一度处于hold状态，直到Go 1.20才纳入到试验特性，我们可以通过GOEXPERIMENT=arena开启该机制。</p>
<p>arena包主要思路其实是“整体分配，零碎使用，再整体释放”，以最大程度减少对GC的压力。关于arena包，等进一步完善后，后续可能会有专门文章分析。</p>
<h4>3) time包变化</h4>
<p><a href="https://github.com/golang/go/issues/52746">time包增加了三个时间layout格式常量</a>，相信不用解释，大家也知道如何使用：</p>
<pre><code>    DateTime   = "2006-01-02 15:04:05"
    DateOnly   = "2006-01-02"
    TimeOnly   = "15:04:05"
</code></pre>
<p><a href="https://github.com/golang/go/issues/50770">time包还为Time增加了Compare方法</a>，适用于time之间的>=和&lt;=比较：</p>
<pre><code>// Compare returns -1 if t1 is before t2, 0 if t1 equals t2 or 1 if t1 is after t2.
func (t1 Time) Compare(t2 Time) int
</code></pre>
<p>此外，time包的RFC3339时间格式是使用最广泛的时间格式，<a href="https://github.com/golang/go/issues/50770">其解析性能在Go 1.20中得到优化，提升了70%左右，格式化性能提升30%</a>。</p>
<h3>4. 其他</h3>
<ul>
<li>Go 1.17版本将作为Go 1.20的bootstrap编译器;</li>
<li><a href="https://go-review.googlesource.com/c/go/+/432897">Go编译器性能提升3%</a>；</li>
<li>Go工具链将根据GO&#91;arch&#93;环境变量的设置<a href="https://github.com/golang/go/issues/45454">自动设置相关build tags</a>；</li>
<li>标准库<a href="https://github.com/golang/go/issues/52221">增加crypto/ecdh包</a>，提供安全的、基于byte切片的ECDH API；</li>
<li><a href="https://github.com/golang/go/issues/45038">bytes, strings包增加Clone函数</a>；</li>
<li><a href="https://github.com/golang/go/issues/42537">strings包增加CutPrefix和CutSuffix函数</a>；</li>
<li><a href="https://github.com/golang/go/issues/53261">text/template的解析性能提升40%</a>。</li>
</ul>
<h3>5. 参考资料</h3>
<ul>
<li>Go 1.20 milestone &#8211; https://github.com/golang/go/milestone/250</li>
<li>Exploring Go&#8217;s Profile-Guided Optimizations &#8211; https://www.polarsignals.com/blog/posts/2022/09/exploring-go-profile-guided-optimizations/</li>
<li>What&#8217;s coming to go 1.20 &#8211; https://twitter.com/mvdan_/status/1588242469577117696</li>
</ul>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p><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>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2022, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2022/11/17/go-1-20-foresight/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>如何查看历史版本的Go文档？嘘！答案我只告诉你！</title>
		<link>https://tonybai.com/2020/12/15/how-to-see-the-manual-of-go-history-version/</link>
		<comments>https://tonybai.com/2020/12/15/how-to-see-the-manual-of-go-history-version/#comments</comments>
		<pubDate>Tue, 15 Dec 2020 04:18:59 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-doc]]></category>
		<category><![CDATA[go1.15]]></category>
		<category><![CDATA[go1.9]]></category>
		<category><![CDATA[godoc]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[golang.org]]></category>
		<category><![CDATA[GOPATH]]></category>
		<category><![CDATA[Gopher部落]]></category>
		<category><![CDATA[GOROOT]]></category>
		<category><![CDATA[Go进阶]]></category>
		<category><![CDATA[Web]]></category>
		<category><![CDATA[标准库]]></category>
		<category><![CDATA[自带电池]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3031</guid>
		<description><![CDATA[Go语言自开源至今已经11个年头了！截至本文发稿时，目前最新的Go版本为1.15，Go核心开发团队正紧锣密鼓的进行着Go 1.16版本的开发（现阶段主要是修复bug），该版本将在2021年2月份正式发布。 Go以“自带电池(battery included)”而为人所知，除了Go标准库的全面和强大之外，Go工具链的丰富和易用在主流编程语言中也是位列“执牛耳者”之列的，而Go文档查看工具就在Go工具链之列中。 查看文档是Gopher们日常必不可少的开发活动之一。Go语言从诞生那天起就十分重视项目文档的建设，除了在Go官方网站(https://golang.org)可以查看到最新稳定发布版（当前是Go 1.15）的文档之外，在tip.golang.org上还可以查看到项目主线分支（master）上最新开发版本（非稳定版）的文档。 那么问题来了！如果想查看Go的某个历史版本（比如：Go 1.9）的文档，我应该如何做呢？别急！在这篇文章中，我将告诉你答案。 1. 利用go doc，可行，但非最优 从Go发布1.0版本开始，Go就将整个Go项目文档加入到Go发行版中，这样开发人员在本地安装Go的同时也拥有了一份完整的Go项目文档。 除了在发行版中集成所有文档，从1.5版本开始，Go还将文档查看工具集成到其工具链当中(即go doc)，使之成为Go工具链不可分割的一部分，这也再次体现了文档在Go语言中的重要性。自go doc在1.5版本加入Go工具链之后，它就和go get、go build一样成为了Gopher们每日必用的go命令，也成为了Go包文档的“百科全书”。 不过利用go doc，我们只能查看当前本地的go版本的文档。如果当前本地环境的Go版本为Go 1.14，那么所有go doc输出的文档内容均来自Go 1.14版本。如果我们要查看Go 1.9版本的文档，我们需要将本地环境的Go版本“切换”到Go 1.9才可以。 “切换”的方法有很多，笔者习惯通过重新设置\$GOROOT的方式在多个Go版本间切换： $export GOROOT=~/.bin/go1.9.7 $export PATH=$GOROOT/bin:$GOPATH $go version go version go1.9.7 darwin/amd64 $go doc http.Request 我们看到：“切换”后再执行的go doc的输出结果均来自Go 1.9版本了。 不过这种方法比较繁琐，需要切换本地环境中的go版本，并且没法像官方站点那样通过图形化的方式查阅go文档，这显然不是最优方案，我们继续往下看。 2. 使用godoc建立历史版本的Web化文档中心 很多接触Go语言较早的gopher都知道，在go doc之前，还有一个像gofmt一样随着Go安装包一起发布的文档查看工具，它就是godoc，也就是说godoc在Go世界的存在历史比go doc还要悠久。在Go 1.5版本增加go doc工具后，godoc与go doc就一直并存在Go中。这种情况一直持续到Go 1.13版本。在Go 1.13版本中，godoc就不再和go、gofmt一起内置在Go安装包中发布了。godoc被挪到Go扩展工具链中，我们可以通过下面命令单独安装godoc： $go get golang.org/x/tools/cmd/godoc [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/how-to-see-the-manual-of-go-history-version-1.png" alt="img{512x368}" /></p>
<p><a href="https://mp.weixin.qq.com/s/woQeEQUhOLJ7KSE5rm5q6g">Go语言自开源至今已经11个年头了</a>！截至本文发稿时，目前<a href="https://mp.weixin.qq.com/s/B5onfyP7BPYCh_rMSBtfcQ">最新的Go版本为1.15</a>，Go核心开发团队正紧锣密鼓的进行着<a href="https://mp.weixin.qq.com/s/JzAQ3r9lDBad8PO6iAerqw">Go 1.16版本</a>的开发（现阶段主要是修复bug），该版本将在2021年2月份正式发布。</p>
<p>Go以“自带电池(battery included)”而为人所知，除了Go标准库的全面和强大之外，Go工具链的丰富和易用在主流编程语言中也是位列“执牛耳者”之列的，而Go文档查看工具就在Go工具链之列中。</p>
<p>查看文档是Gopher们日常必不可少的开发活动之一。Go语言从诞生那天起就十分重视项目文档的建设，除了在Go官方网站(https://golang.org)可以查看到最新稳定发布版（当前是<a href="https://mp.weixin.qq.com/s/B5onfyP7BPYCh_rMSBtfcQ">Go 1.15</a>）的文档之外，在<strong>tip.golang.org</strong>上还可以查看到项目主线分支（master）上最新开发版本（非稳定版）的文档。</p>
<p><strong>那么问题来了！如果想查看Go的某个历史版本（比如：<a href="https://tonybai.com/2017/07/14/some-changes-in-go-1-9">Go 1.9</a>）的文档，我应该如何做呢</strong>？别急！在这篇文章中，我将告诉你答案。</p>
<h3>1. 利用go doc，可行，但非最优</h3>
<p>从Go发布1.0版本开始，Go就将整个Go项目文档加入到Go发行版中，这样开发人员在本地安装Go的同时也拥有了一份完整的Go项目文档。</p>
<p>除了在发行版中集成所有文档，从<a href="https://tonybai.com/2015/07/10/some-changes-in-go-1-5">1.5版本</a>开始，Go还将文档查看工具集成到其工具链当中(即<strong>go doc</strong>)，使之成为Go工具链不可分割的一部分，这也再次体现了文档在Go语言中的重要性。自go doc在1.5版本加入Go工具链之后，它就和go get、go build一样成为了Gopher们每日必用的go命令，也成为了<strong>Go包文档的“百科全书”</strong>。</p>
<p>不过利用go doc，我们只能查看当前本地的go版本的文档。如果当前本地环境的Go版本为<a href="https://tonybai.com/2020/03/08/some-changes-in-go-1-14/">Go 1.14</a>，那么所有go doc输出的文档内容均来自Go 1.14版本。如果我们要查看Go 1.9版本的文档，我们需要将本地环境的Go版本“切换”到Go 1.9才可以。</p>
<p>“切换”的方法有很多，笔者习惯通过重新设置\$GOROOT的方式在多个Go版本间切换：</p>
<pre><code>$export GOROOT=~/.bin/go1.9.7
$export PATH=$GOROOT/bin:$GOPATH
$go version
go version go1.9.7 darwin/amd64
$go doc http.Request
</code></pre>
<p>我们看到：“切换”后再执行的go doc的输出结果均来自Go 1.9版本了。</p>
<p>不过这种方法比较繁琐，需要切换本地环境中的go版本，并且没法像官方站点那样通过图形化的方式查阅go文档，这显然不是最优方案，我们继续往下看。</p>
<h3>2. 使用godoc建立历史版本的Web化文档中心</h3>
<p>很多接触Go语言较早的gopher都知道，在go doc之前，还有一个像gofmt一样随着Go安装包一起发布的文档查看工具，它就是<strong>godoc</strong>，也就是说godoc在Go世界的存在历史比go doc还要悠久。在Go 1.5版本增加go doc工具后，godoc与go doc就一直并存在Go中。这种情况一直持续到<a href="https://tonybai.com/2019/10/27/some-changes-in-go-1-13/">Go 1.13版本</a>。在Go 1.13版本中，godoc就不再和go、gofmt一起内置在Go安装包中发布了。godoc被挪到Go扩展工具链中，我们可以通过下面命令单独安装godoc：</p>
<pre><code>$go get golang.org/x/tools/cmd/godoc
</code></pre>
<p>和命令行go doc工具不同的是，godoc实质上是一个web服务，它会在本地建立起一个web形式的Go文档中心，当我们执行下面命令时这个文档中心服务就启动了：</p>
<pre><code>$godoc -http=localhost:6060
</code></pre>
<p>在浏览器地址栏输入<code>http://localhost:6060</code>，打开Go文档中心首页：</p>
<p><img src="https://tonybai.com/wp-content/uploads/how-to-see-the-manual-of-go-history-version-2.png" alt="img{512x368}" /></p>
<p>我们看到godoc将\$GOROOT下面的内容以web页面的形似呈现给开发者，同时我们看到首页顶部的菜单与Go官方主页的菜单也基本如出一辙。点击“Packages”可以打开Go包参考文档页面：</p>
<p><img src="https://tonybai.com/wp-content/uploads/how-to-see-the-manual-of-go-history-version-3.png" alt="img{512x368}" /></p>
<p>Go包参考文档页面将包分为几类：标准库包(Standard library)、第三方包(Third party)和其它包(Other packages)，其中的第三方包就是本地\$GOPATH下面的各个包。</p>
<p>不过，默认情况下godoc建立的Go文档中心对应的文档版本也是本地环境当前的Go版本，即如果当前本地安装的Go版本为go 1.14，那么godoc所呈现的就是go 1.14稳定版对应的文档。 那么如果我想查看一下旧版本的文档，比如Go 1.9.7版本的文档，我该如何做呢？首先我们需要下载go 1.9.7版本的安装包（因为正如前面所说的，go文档是随着安装包一起发布的），将其解压到本地目录下，比如是/Users/tonybai/.bin/go1.9.7，接下来我们执行如下命令：</p>
<pre><code>$godoc -goroot /Users/tonybai/.bin/go1.9.7 -http=localhost:6060
</code></pre>
<p>我们用<strong>-goroot</strong>命令行选项显式告诉godoc从哪个路径加载Go文档数据，这样godoc建立的文档中心中的文档版本就是Go 1.9.7的了。</p>
<p>我们看到，通过godoc，我们不仅无需切换本地环境中的go版本，我们还建立起了像golang.org那样的图形化的历史go版本的文档中心。这也是截至目前为止查看Go历史版本版本文档的<strong>最佳方法</strong>了！</p>
<p>更多关于常用Go工具链的<strong>高级用法</strong>的内容请参看我在慕课网上出品的Go进阶技术专栏<a href="https://www.imooc.com/read/87">《改善Go语言编程质量的50个有效实践》</a>。本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订阅！</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-column-pgo-with-qr-and-text.png" alt="img{512x368}" /></p>
<hr />
<p><strong>“Gopher部落”知识星球开球了！</strong>高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！星球首开，福利自然是少不了的！2020年年底之前，8.8折(很吉利吧^_^)加入星球，下方图片扫起来吧！</p>
<p><img src="http://image.tonybai.com/img/202011/gopher-tribe-zsxq.png" alt="" /></p>
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网热卖中，欢迎小伙伴们订阅学习！</p>
<p><img src="https://tonybai.com/wp-content/uploads/k8s-practice-with-qr-and-text.png" alt="img{512x368}" /></p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</p>
<p>2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博：https://weibo.com/bigwhite20xx</li>
<li>微信公众号：iamtonybai</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>“Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544</li>
</ul>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2020, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2020/12/15/how-to-see-the-manual-of-go-history-version/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go 1.15中值得关注的几个变化</title>
		<link>https://tonybai.com/2020/10/11/some-changes-in-go-1-15/</link>
		<comments>https://tonybai.com/2020/10/11/some-changes-in-go-1-15/#comments</comments>
		<pubDate>Sun, 11 Oct 2020 03:10:06 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[arm64]]></category>
		<category><![CDATA[cache]]></category>
		<category><![CDATA[Darwin]]></category>
		<category><![CDATA[GNU]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.11]]></category>
		<category><![CDATA[go1.13]]></category>
		<category><![CDATA[go1.14]]></category>
		<category><![CDATA[go1.15]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[GOPATH]]></category>
		<category><![CDATA[GOPROXY]]></category>
		<category><![CDATA[GOROOT]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[linker]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Module]]></category>
		<category><![CDATA[objdump]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[panic]]></category>
		<category><![CDATA[Reflect]]></category>
		<category><![CDATA[RISC-V]]></category>
		<category><![CDATA[tzdata]]></category>
		<category><![CDATA[unsafe]]></category>
		<category><![CDATA[Windows]]></category>
		<category><![CDATA[zoneinfo]]></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=2955</guid>
		<description><![CDATA[Go 1.15版本在8月12日就正式发布了，给我的感觉就是发布的挺痛快^_^。这种感觉来自与之前版本发布时间的对比：Go 1.13版本发布于当年的9月4日，更早的Go 1.11版本发布于当年的8月25日。 不过这个时间恰与我家二宝出生和老婆月子时期有重叠，每天照顾孩子团团转的我实在抽不出时间研究Go 1.15的变化:(。如今，我逐渐从照顾二宝的工作中脱离出来^_^，于是“Go x.xx版本值得关注的几个变化”系列将继续下去。关注Go语言的演变对掌握和精通Go语言大有裨益，凡是致力于成为一名高级Gopher的读者都应该密切关注Go的演进。 截至写稿时，Go 1.15最新版是Go 1.15.2。Go 1.15一如既往的遵循Go1兼容性承诺。语言规范方面没有任何变化。可以说这是一个“面子”上变化较小的一个版本，但“里子”的变化还是不少的，在本文中我就和各位读者一起就重要变化逐一了解一下。 一. 平台移植性 Go 1.15版本不再对darwin/386和darwin/arm两个32位平台提供支持了。Go 1.15及以后版本仅对darwin/amd64和darwin/arm64版本提供支持。并且不再对macOS 10.12版本之前的版本提供支持。 Go 1.14版本中，Go编译器在被传入-race和-msan的情况下，默认会执行-d=checkptr，即对unsafe.Pointer的使用进行合法性检查。-d=checkptr主要检查两项内容： 当将unsafe.Pointer转型为&#42;T时，T的内存对齐系数不能高于原地址的； 做完指针算术后，转换后的unsafe.Pointer仍应指向原先Go堆对象 但在Go 1.14中，这个检查并不适用于Windows操作系统。Go 1.15中增加了对windows系统的支持。 对于RISC-V架构，Go社区展现出十分积极的姿态，早在Go 1.11版本，Go就为RISC-V cpu架构预留了GOARCH值：riscv和riscv64。Go 1.14版本则为64bit RISC-V提供了在linux上的实验性支持(GOOS=linux, GOARCH=riscv64)。在Go 1.15版本中，Go在GOOS=linux, GOARCH=riscv64的环境下的稳定性和性能得到持续提升，并且已经可以支持goroutine异步抢占式调度了。 二. 工具链 1. GOPROXY新增以管道符为分隔符的代理列表值 在Go 1.13版本中，GOPROXY支持设置为多个proxy的列表，多个proxy之间采用逗号分隔。Go工具链会按顺序尝试列表中的proxy以获取依赖包数据，但是当有proxy server服务不可达或者是返回的http状态码不是404也不是410时，go会终止数据获取。但是当列表中的proxy server返回其他错误时，Go命令不会向GOPROXY列表中的下一个值所代表的的proxy server发起请求，这种行为模式没能让所有gopher满意，很多Gopher认为Go工具链应该向后面的proxy server请求，直到所有proxy server都返回失败。Go 1.15版本满足了Go社区的需求，新增以管道符“&#124;”为分隔符的代理列表值。如果GOPROXY配置的proxy server列表值以管道符分隔，则无论某个proxy server返回什么错误码，Go命令都会向列表中的下一个proxy server发起新的尝试请求。 注：Go 1.15版本中GOPROXY环境变量的默认值依旧为https://proxy.golang.org,direct。 2. module cache的存储路径可设置 Go module机制自打在Go 1.11版本中以试验特性的方式引入时就将module的本地缓存默认放在了\$GOPATH/pkg/mod下（如果没有显式设置GOPATH，那么默认值将是~/go；如果GOPATH下面配置了多个路径，那么选择第一个路径），一直到Go [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/go-1.15-1.png" alt="img{512x368}" /></p>
<p><a href="https://tip.golang.org/doc/go1.15">Go 1.15版本</a>在8月12日就正式发布了，给我的感觉就是发布的挺痛快^_^。这种感觉来自与之前版本发布时间的对比：<a href="https://tonybai.com/2019/10/27/some-changes-in-go-1-13/">Go 1.13版本</a>发布于当年的9月4日，更早的<a href="https://tonybai.com/2018/11/19/some-changes-in-go-1-11/">Go 1.11版本</a>发布于当年的8月25日。</p>
<p>不过这个时间恰与我家<a href="https://tonybai.com/2020/07/29/my-second-daughter-was-born/">二宝出生</a>和老婆月子时期有重叠，每天照顾孩子团团转的我实在抽不出时间研究Go 1.15的变化:(。如今，我逐渐从照顾二宝的工作中脱离出来^_^，于是“Go x.xx版本值得关注的几个变化”系列将继续下去。关注Go语言的演变对掌握和精通Go语言大有裨益，凡是致力于成为一名高级Gopher的读者都应该密切关注Go的演进。<br />
截至写稿时，Go 1.15最新版是Go 1.15.2。Go 1.15一如既往的遵循<a href="https://tip.golang.org/doc/go1compat.html">Go1兼容性承诺</a>。<a href="https://tip.golang.org/ref/spec">语言规范</a>方面没有任何变化。可以说这是一个“面子”上变化较小的一个版本，但“里子”的变化还是不少的，在本文中我就和各位读者一起就重要变化逐一了解一下。</p>
<h3>一. 平台移植性</h3>
<p>Go 1.15版本不再对darwin/386和darwin/arm两个32位平台提供支持了。Go 1.15及以后版本仅对darwin/amd64和darwin/arm64版本提供支持。并且不再对macOS 10.12版本之前的版本提供支持。</p>
<p><a href="https://tonybai.com/2020/03/08/some-changes-in-go-1-14/">Go 1.14版本</a>中，Go编译器在被传入-race和-msan的情况下，默认会执行<strong>-d=checkptr</strong>，即对unsafe.Pointer的使用进行<a href="https://github.com/golang/go/issues/34964">合法性检查</a>。<strong>-d=checkptr</strong>主要检查两项内容：</p>
<ul>
<li>
<p>当将unsafe.Pointer转型为&#42;T时，T的内存对齐系数不能高于原地址的；</p>
</li>
<li>
<p>做完指针算术后，转换后的unsafe.Pointer仍应指向原先Go堆对象</p>
</li>
</ul>
<p>但在Go 1.14中，这个检查并不适用于Windows操作系统。Go 1.15中增加了对windows系统的支持。</p>
<p>对于<a href="https://riscv.org">RISC-V</a>架构，Go社区展现出十分积极的姿态，早在<a href="https://tonybai.com/2018/11/19/some-changes-in-go-1-11/">Go 1.11版本</a>，Go就为RISC-V cpu架构预留了GOARCH值：riscv和riscv64。<a href="https://tonybai.com/2020/03/08/some-changes-in-go-1-14/">Go 1.14版本</a>则为64bit RISC-V提供了在linux上的实验性支持(GOOS=linux, GOARCH=riscv64)。在Go 1.15版本中，Go在GOOS=linux, GOARCH=riscv64的环境下的稳定性和性能得到持续提升，并且已经可以支持goroutine异步抢占式调度了。</p>
<h3>二. 工具链</h3>
<h4>1. GOPROXY新增以管道符为分隔符的代理列表值</h4>
<p>在<a href="https://tonybai.com/2019/10/27/some-changes-in-go-1-13/">Go 1.13版本</a>中，<a href="https://tonybai.com/2018/11/26/hello-go-module-proxy/">GOPROXY</a>支持设置为多个proxy的列表，多个proxy之间采用逗号分隔。Go工具链会按顺序尝试列表中的proxy以获取依赖包数据，但是当有proxy server服务不可达或者是返回的http状态码不是404也不是410时，go会终止数据获取。但是当列表中的proxy server返回其他错误时，Go命令不会向GOPROXY列表中的下一个值所代表的的proxy server发起请求，这种行为模式没能让所有gopher满意，<strong>很多Gopher认为Go工具链应该向后面的proxy server请求，直到所有proxy server都返回失败</strong>。Go 1.15版本满足了Go社区的需求，新增以管道符“|”为分隔符的代理列表值。如果GOPROXY配置的proxy server列表值以管道符分隔，则无论某个proxy server返回什么错误码，Go命令都会向列表中的下一个proxy server发起新的尝试请求。</p>
<blockquote>
<p>注：Go 1.15版本中GOPROXY环境变量的默认值依旧为<strong>https://proxy.golang.org,direct</strong>。</p>
</blockquote>
<h4>2. module cache的存储路径可设置</h4>
<p><a href="https://tonybai.com/2019/06/03/the-practice-of-upgrading-major-version-under-go-module/">Go module机制</a>自打在<a href="https://tonybai.com/2018/11/19/some-changes-in-go-1-11/">Go 1.11版本</a>中以试验特性的方式引入时就将module的本地缓存默认放在了<strong>\$GOPATH/pkg/mod</strong>下（如果没有显式设置GOPATH，那么默认值将是<strong>~/go</strong>；如果GOPATH下面配置了多个路径，那么选择第一个路径），一直到Go 1.14版本，这个位置都是无法配置的。</p>
<p>Go module的引入为去除GOPATH提供了前提，于是module cache的位置也要尽量与GOPATH“脱钩”：Go 1.15提供了GOMODCACHE环境变量用于自定义module cache的存放位置。如果没有显式设置GOMODCACHE，那么module cache的默认存储路径依然是<strong>\$GOPATH/pkg/mod</strong>。</p>
<h3>三. 运行时、编译器和链接器</h3>
<h4>1. panic展现形式变化</h4>
<p>在Go 1.15之前，如果传给panic的值是bool, complex64, complex128, float32, float64, int, int8, int16, int32, int64, string, uint, uint8, uint16, uint32, uint64, uintptr等原生类型的值，那么panic在触发时会输出具体的值，比如：</p>
<pre><code>// go1.15-examples/runtime/panic.go

package main

func foo() {
    var i uint32 = 17
    panic(i)
}

func main() {
    foo()
}
</code></pre>
<p>使用Go 1.14运行上述代码，得到如下结果：</p>
<pre><code>$go run panic.go
panic: 17

goroutine 1 [running]:
main.foo(...)
    /Users/tonybai/go/src/github.com/bigwhite/experiments/go1.15-examples/runtime/panic.go:5
main.main()
    /Users/tonybai/go/src/github.com/bigwhite/experiments/go1.15-examples/runtime/panic.go:9 +0x39
exit status 2
</code></pre>
<p>Go 1.15版本亦是如此。但是对于派生于上述原生类型的自定义类型而言，Go 1.14只是输出变量地址：</p>
<pre><code>// go1.15-examples/runtime/panic.go

package main

type myint uint32

func bar() {
    var i myint = 27
    panic(i)
}

func main() {
    bar()
}
</code></pre>
<p>使用Go 1.14运行上述代码：</p>
<pre><code>$go run panic.go
panic: (main.myint) (0x105fca0,0xc00008e000)

goroutine 1 [running]:
main.bar(...)
    /Users/tonybai/go/src/github.com/bigwhite/experiments/go1.15-examples/runtime/panic.go:12
main.main()
    /Users/tonybai/go/src/github.com/bigwhite/experiments/go1.15-examples/runtime/panic.go:17 +0x39
exit status 2

</code></pre>
<p>Go 1.15针对此情况作了展示优化，即便是派生于这些原生类型的自定义类型变量，panic也可以输出其值。使用Go 1.15运行上述代码的结果如下：</p>
<pre><code>$go run panic.go
panic: main.myint(27)

goroutine 1 [running]:
main.bar(...)
    /Users/tonybai/go/src/github.com/bigwhite/experiments/go1.15-examples/runtime/panic.go:12
main.main()
    /Users/tonybai/go/src/github.com/bigwhite/experiments/go1.15-examples/runtime/panic.go:17 +0x39
exit status 2

</code></pre>
<h4>2. 将小整数([0,255])转换为interface类型值时将不会额外分配内存</h4>
<p>Go 1.15在runtime/iface.go中做了一些优化改动：增加一个名为staticuint64s的数组，预先为[0,255]这256个数分配了内存。然后在convT16、convT32等运行时转换函数中判断要转换的整型值是否小于256(len(staticuint64s))，如果小于，则返回staticuint64s数组中对应的值的地址；否则调用mallocgc分配新内存。</p>
<pre><code>$GOROOT/src/runtime/iface.go

// staticuint64s is used to avoid allocating in convTx for small integer values.
var staticuint64s = [...]uint64{
        0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
        0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
        0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
        0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
        0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27,
        0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
        0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,

        ... ...

        0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7,
        0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff,

}

func convT16(val uint16) (x unsafe.Pointer) {
        if val &lt; uint16(len(staticuint64s)) {
                x = unsafe.Pointer(&amp;staticuint64s[val])
                if sys.BigEndian {
                        x = add(x, 6)
                }
        } else {
                x = mallocgc(2, uint16Type, false)
                *(*uint16)(x) = val
        }
        return
}

func convT32(val uint32) (x unsafe.Pointer) {
        if val &lt; uint32(len(staticuint64s)) {
                x = unsafe.Pointer(&amp;staticuint64s[val])
                if sys.BigEndian {
                        x = add(x, 4)
                }
        } else {
                x = mallocgc(4, uint32Type, false)
                *(*uint32)(x) = val
        }
        return
}

</code></pre>
<p>我们可以用下面例子来验证一下：</p>
<pre><code>// go1.15-examples/runtime/tinyint2interface.go

package main

import (
    "math/rand"
)

func convertSmallInteger() interface{} {
    i := rand.Intn(256)
    var j interface{} = i
    return j
}

func main() {
    for i := 0; i &lt; 100000000; i++ {
        convertSmallInteger()
    }
}

</code></pre>
<p>我们分别用go 1.14和go 1.15.2编译这个源文件（注意关闭内联等优化，否则很可能看不出效果）：</p>
<pre><code>// go 1.14

go build  -gcflags="-N -l" -o tinyint2interface-go14 tinyint2interface.go

// go 1.15.2

go build  -gcflags="-N -l" -o tinyint2interface-go15 tinyint2interface.go

</code></pre>
<p>我们使用下面命令输出程序执行时每次GC的信息：</p>
<pre><code>$env GODEBUG=gctrace=1 ./tinyint2interface-go14
gc 1 @0.025s 0%: 0.009+0.18+0.021 ms clock, 0.079+0.079/0/0.20+0.17 ms cpu, 4-&gt;4-&gt;0 MB, 5 MB goal, 8 P
gc 2 @0.047s 0%: 0.003+0.14+0.013 ms clock, 0.031+0.099/0.064/0.037+0.10 ms cpu, 4-&gt;4-&gt;0 MB, 5 MB goal, 8 P
gc 3 @0.064s 0%: 0.008+0.20+0.016 ms clock, 0.071+0.071/0.018/0.081+0.13 ms cpu, 4-&gt;4-&gt;0 MB, 5 MB goal, 8 P
gc 4 @0.081s 0%: 0.005+0.14+0.013 ms clock, 0.047+0.059/0.023/0.032+0.10 ms cpu, 4-&gt;4-&gt;0 MB, 5 MB goal, 8 P
gc 5 @0.098s 0%: 0.005+0.10+0.017 ms clock, 0.042+0.073/0.027/0.080+0.13 ms cpu, 4-&gt;4-&gt;0 MB, 5 MB goal, 8 P

... ...

gc 192 @3.264s 0%: 0.003+0.11+0.013 ms clock, 0.024+0.060/0.005/0.035+0.11 ms cpu, 4-&gt;4-&gt;0 MB, 5 MB goal, 8 P
gc 193 @3.281s 0%: 0.005+0.13+0.032 ms clock, 0.042+0.075/0.041/0.050+0.25 ms cpu, 4-&gt;4-&gt;0 MB, 5 MB goal, 8 P
gc 194 @3.298s 0%: 0.004+0.12+0.013 ms clock, 0.033+0.072/0.030/0.033+0.10 ms cpu, 4-&gt;4-&gt;0 MB, 5 MB goal, 8 P
gc 195 @3.315s 0%: 0.003+0.17+0.023 ms clock, 0.029+0.062/0.055/0.024+0.18 ms cpu, 4-&gt;4-&gt;0 MB, 5 MB goal, 8 P

$env GODEBUG=gctrace=1 ./tinyint2interface-go15
</code></pre>
<p>我们看到和go 1.14编译的程序不断分配内存，不断导致GC相比，go1.15.2没有输出GC信息，间接证实了小整数转interface变量值时不会触发内存分配。</p>
<h4>3. 加入更现代化的链接器(linker)</h4>
<p>一个新版的<a href="https://golang.org/s/better-linker">现代化linker</a>正在逐渐加入到Go中，Go 1.15是新版linker的起点。后续若干版本，linker优化会逐步加入进来。在Go 1.15中，对于大型项目，新链接器的性能要提高20%，内存占用减少30%。</p>
<h4>4. objdump支持输出GNU汇编语法</h4>
<p>go 1.15为objdump工具增加了-gnu选项，<strong>以在Go汇编的后面，辅助输出GNU汇编，便于对照</strong>：</p>
<pre><code>// go 1.14：

$go tool objdump -S tinyint2interface-go15|more
TEXT go.buildid(SB)

  0x1001000             ff20                    JMP 0(AX)
  0x1001002             476f                    OUTSD DS:0(SI), DX
  0x1001004             206275                  ANDB AH, 0x75(DX)
  0x1001007             696c642049443a20        IMULL $0x203a4449, 0x20(SP), BP
... ...

//go 1.15.2：

$go tool objdump  -S -gnu tinyint2interface-go15|more
TEXT go.buildid(SB)

  0x1001000             ff20                    JMP 0(AX)                            // jmpq *(%rax)           

  0x1001002             476f                    OUTSD DS:0(SI), DX                   // rex.RXB outsl %ds:(%rsi),(%dx)
  0x1001004             206275                  ANDB AH, 0x75(DX)                    // and %ah,0x75(%rdx)     

  0x1001007             696c642049443a20        IMULL $0x203a4449, 0x20(SP), BP      // imul $0x203a4449,0x20(%rsp,%riz,2),%ebp

... ...

</code></pre>
<h3>四. 标准库</h3>
<p>和以往发布的版本一样，标准库有大量小改动，这里挑出几个笔者感兴趣的和大家一起看一下。</p>
<h4>1. 增加tzdata包</h4>
<p>Go time包中很多方法依赖时区数据，但不是所有平台上都自带时区数据。Go time包会以下面顺序搜寻时区数据：</p>
<pre><code>- ZONEINFO环境变量指示的路径中

- 在类Unix系统中一些常见的存放时区数据的路径（zoneinfo_unix.go中的zoneSources数组变量中存放这些常见路径）：

    "/usr/share/zoneinfo/",
    "/usr/share/lib/zoneinfo/",
    "/usr/lib/locale/TZ/"

- 如果平台没有，则尝试使用$GOROOT/lib/time/zoneinfo.zip这个随着go发布包一起发布的时区数据。但在应用部署的环境中，很大可能不会进行go安装。

</code></pre>
<p>如果go应用找不到时区数据，那么go应用运行将会受到影响，就如下面这个例子：</p>
<pre><code>// go1.15-examples/stdlib/tzdata.go

package main

import (
    "fmt"
    "time"
)

func main() {
    loc, err := time.LoadLocation("America/New_York")
    if err != nil {
        fmt.Println("LoadLocation error:", err)
        return
    }
    fmt.Println("LoadLocation is:", loc)
}

</code></pre>
<p>我们移除系统的时区数据(比如将/usr/share/zoneinfo改名)和Go安装包自带的zoneinfo.zip(改个名)后，在Go 1.15.2下运行该示例：</p>
<pre><code>$ go run tzdata.go
LoadLocation error: unknown time zone America/New_York

</code></pre>
<p>为此，Go 1.15提供了一个将时区数据嵌入到Go应用二进制文件中的方法：<strong>导入time/tzdata包</strong>：</p>
<pre><code>// go1.15-examples/stdlib/tzdata.go

package main

import (
    "fmt"
    "time"
    _ "time/tzdata"
)

func main() {
    loc, err := time.LoadLocation("America/New_York")
    if err != nil {
        fmt.Println("LoadLocation error:", err)
        return
    }
    fmt.Println("LoadLocation is:", loc)
}

</code></pre>
<p>我们再用go 1.15.2运行一下上述导入tzdata包的例子：</p>
<pre><code>$go run testtimezone.go
LoadLocation is: America/New_York

</code></pre>
<p>不过由于附带tzdata数据，应用二进制文件的size会增大大约800k，下面是在ubuntu下的实测值：</p>
<pre><code>-rwxr-xr-x 1 root root 2.0M Oct 11 02:42 tzdata-withouttzdata*
-rwxr-xr-x 1 root root 2.8M Oct 11 02:42 tzdata-withtzdata*

</code></pre>
<h4>2. 增加json解码限制</h4>
<p>json包是日常使用最多的go标准库包之一，在Go 1.15中，go按照json规范的要求，为json的解码增加了一层限制：</p>
<pre><code>// json规范要求

//https://tools.ietf.org/html/rfc7159#section-9

A JSON parser transforms a JSON text into another representation.  A
   JSON parser MUST accept all texts that conform to the JSON grammar.
   A JSON parser MAY accept non-JSON forms or extensions.

   An implementation may set limits on the size of texts that it
   accepts.  An implementation may set limits on the maximum depth of
   nesting.  An implementation may set limits on the range and precision
   of numbers.  An implementation may set limits on the length and
   character contents of strings.

</code></pre>
<p>这个限制就是增加了一个对json文本最大缩进深度值：</p>
<pre><code>// $GOROOT/src/encoding/json/scanner.go

// This limits the max nesting depth to prevent stack overflow.
// This is permitted by https://tools.ietf.org/html/rfc7159#section-9
const maxNestingDepth = 10000
</code></pre>
<p>如果一旦传入的json文本数据缩进深度超过maxNestingDepth，那json包就会panic。当然，绝大多数情况下，我们是碰不到缩进10000层的超大json文本的。因此，该limit对于99.9999%的gopher都没啥影响。</p>
<h4>3. reflect包</h4>
<p>Go 1.15版本之前reflect包<a href="https://github.com/golang/go/issues/38521">存在一处行为不一致的问题</a>，我们看下面例子(例子来源于https://play.golang.org/p/Jnga2_6Rmdf)：</p>
<pre><code>// go1.15-examples/stdlib/reflect.go

package main

import "reflect"

type u struct{}

func (u) M() { println("M") }

type t struct {
    u
    u2 u
}

func call(v reflect.Value) {
    defer func() {
        if err := recover(); err != nil {
            println(err.(string))
        }
    }()
    v.Method(0).Call(nil)
}

func main() {
    v := reflect.ValueOf(t{}) // v := t{}
    call(v)                   // v.M()
    call(v.Field(0))          // v.u.M()
    call(v.Field(1))          // v.u2.M()
}

</code></pre>
<p>我们使用Go 1.14版本运行该示例：</p>
<pre><code>$go run reflect.go
M
M
reflect: reflect.flag.mustBeExported using value obtained using unexported field

</code></pre>
<p>我们看到同为类型t中的非导出字段(field)的u和u2(u是以嵌入类型方式称为类型t的字段的)，通过reflect包可以调用字段u的导出方法(如输出中的第二行的M)，却无法调用非导出字段u2的导出方法（如输出中的第三行的panic信息）。</p>
<p>这种不一致在Go 1.15版本中被修复，我们使用Go 1.15.2运行上述示例：</p>
<pre><code>$go run reflect.go
M
reflect: reflect.Value.Call using value obtained using unexported field
reflect: reflect.Value.Call using value obtained using unexported field

</code></pre>
<p>我们看到reflect无法调用非导出字段u和u2的导出方法了。但是reflect依然可以通过提升到类型t的方法来间接使用u的导出方法，正如运行结果中的第一行输出。<br />
<strong>这一改动可能会影响到遗留代码中使用reflect调用以类型嵌入形式存在的非导出字段方法的代码</strong>，如果你的代码中存在这样的问题，可以直接通过提升(promote)到包裹类型(如例子中的t)中的方法（如例子中的call(v)）来替代之前的方式。</p>
<h3>五. 小结</h3>
<p>由于Go 1.15删除了一些GC元数据和一些无用的类型元数据，Go 1.15编译出的二进制文件size会减少5%左右。我用一个中等规模的go项目实测了一下：</p>
<pre><code>-rwxr-xr-x   1 tonybai  staff    23M 10 10 16:54 yunxind*
-rwxr-xr-x   1 tonybai  staff    24M  9 30 11:20 yunxind-go14*

</code></pre>
<p>二进制文件size的确有变小，大约4%-5%。</p>
<p><strong>如果你还没有升级到Go 1.15，那么现在正是时候</strong>。</p>
<p>本文中涉及的代码可以在<a href="https://github.com/bigwhite/experiments/tree/master/go1.15-examples">这里</a>下载。https://github.com/bigwhite/experiments/tree/master/go1.15-examples</p>
<hr />
<p>我的Go技术专栏：“<a href="https://www.imooc.com/read/87">改善Go语⾔编程质量的50个有效实践</a>”上线了，欢迎大家订阅学习！</p>
<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>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>
<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/10/11/some-changes-in-go-1-15/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go 1.14中值得关注的几个变化</title>
		<link>https://tonybai.com/2020/03/08/some-changes-in-go-1-14/</link>
		<comments>https://tonybai.com/2020/03/08/some-changes-in-go-1-14/#comments</comments>
		<pubDate>Sun, 08 Mar 2020 09:17:28 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[cleanup]]></category>
		<category><![CDATA[Compiler]]></category>
		<category><![CDATA[defer]]></category>
		<category><![CDATA[epoll]]></category>
		<category><![CDATA[escape-analysis]]></category>
		<category><![CDATA[fuzz-test]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-fuzz]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[go1.12]]></category>
		<category><![CDATA[go1.13]]></category>
		<category><![CDATA[go1.14]]></category>
		<category><![CDATA[GOARCH]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GOOS]]></category>
		<category><![CDATA[GOPATH]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[GOROOT]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[https]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Module]]></category>
		<category><![CDATA[netpoll]]></category>
		<category><![CDATA[Performance]]></category>
		<category><![CDATA[Pointer]]></category>
		<category><![CDATA[RISC-V]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[Scheduler]]></category>
		<category><![CDATA[String]]></category>
		<category><![CDATA[struct]]></category>
		<category><![CDATA[syscall]]></category>
		<category><![CDATA[Testing]]></category>
		<category><![CDATA[TIBOE]]></category>
		<category><![CDATA[Timer]]></category>
		<category><![CDATA[uintptr]]></category>
		<category><![CDATA[Unicode]]></category>
		<category><![CDATA[Unix]]></category>
		<category><![CDATA[unsafe]]></category>
		<category><![CDATA[信号]]></category>
		<category><![CDATA[垃圾回收]]></category>
		<category><![CDATA[定时器]]></category>
		<category><![CDATA[性能]]></category>
		<category><![CDATA[抢占]]></category>
		<category><![CDATA[接口]]></category>
		<category><![CDATA[标准库]]></category>
		<category><![CDATA[结构体]]></category>
		<category><![CDATA[编译器]]></category>
		<category><![CDATA[调度器]]></category>
		<category><![CDATA[逃逸分析]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=2854</guid>
		<description><![CDATA[可能是得益于2020年2月26日Go 1.14的发布，在2020年3月份的TIOBE编程语言排行榜上，Go重新进入TOP 10，而去年同期Go仅排行在第18位。虽然Go语言以及其他主流语言在榜单上的“上蹿下跳”让这个榜单的权威性饱受质疑:)，但Go在这样的一个时间节点能进入TOP 10，对于Gopher和Go社区来说，总还是一个不错的结果。并且在一定层度上说明：Go在努力耕耘十年后，已经在世界主流编程语言之林中牢牢占据了自己的一个位置。 图：TIOBE编程语言排行榜2020.3月榜单，Go语言重入TOP10 Go自从宣布Go1 Compatible后，直到这次的Go 1.14发布，Go的语法和核心库都没有做出不兼容的变化。这让很多其他主流语言的拥趸们觉得Go很“无趣”。但这种承诺恰恰是Go团队背后努力付出的结果，因此Go的每个发布版本都值得广大gopher尊重，每个发布版本都是Go团队能拿出的最好版本。 下面我们就来解读一下Go 1.14的变化，看看这个新版本中有哪些值得我们重点关注的变化。 一. 语言规范 和其他主流语言相比，Go语言的语法规范的变化那是极其少的（广大Gopher们已经习惯了这个节奏:)），偶尔发布一个变化，那自然是要引起广大Gopher严重关注的:)。不过事先说明：只要Go版本依然是1.x，那么这个规范变化也是backward-compitable的。 Go 1.14新增的语法变化是：嵌入接口的方法集可重叠。这个变化背后的朴素思想是这样的。看下面代码(来自这里)： type I interface { f(); String() string } type J interface { g(); String() string } type IJ interface { I; J } ----- (1) type IJ interface { f(); g(); String() string } ---- (2) 代码中已知定义的I和J两个接口的方法集中都包含有String() string这个方法。在这样的情况下，我们如果想定义一个方法集合为Union(I, J)的新接口IJ，我们在Go 1.13及之前的版本中只能使用第(2)种方式，即只能在新接口IJ中重新书写一遍所有的方法原型，而无法像第(1)种方式那样使用嵌入接口的简洁方式进行。 [...]]]></description>
			<content:encoded><![CDATA[<p>可能是得益于2020年2月26日<a href="https://blog.golang.org/go1.14">Go 1.14的发布</a>，在2020年3月份的<a href="https://tiobe.com/tiobe-index/">TIOBE编程语言排行榜</a>上，Go重新进入TOP 10，而去年同期Go仅排行在第18位。虽然<a href="https://tonybai.com/tag/go">Go语言</a>以及其他主流语言在榜单上的“上蹿下跳”让这个榜单的权威性饱受质疑:)，但Go在这样的一个时间节点能进入TOP 10，对于Gopher和Go社区来说，总还是一个不错的结果。并且在一定层度上说明：<a href="https://tonybai.com/2019/11/09/go-opensource-10-years/">Go在努力耕耘十年</a>后，已经在世界主流编程语言之林中牢牢占据了自己的一个位置。</p>
<p><img src="https://tonybai.com/wp-content/uploads/some-changes-in-go-1-14-1.png" alt="img{512x368}" /></p>
<p><center> 图：TIOBE编程语言排行榜2020.3月榜单，Go语言重入TOP10</center></p>
<p>Go自从宣布<a href="https://tip.golang.org/doc/go1compat.html">Go1 Compatible</a>后，直到这次的Go 1.14发布，Go的语法和核心库都没有做出不兼容的变化。这让很多其他主流语言的拥趸们觉得Go很“无趣”。但这种承诺恰恰是Go团队背后努力付出的结果，因此Go的每个发布版本都值得广大gopher尊重，<strong>每个发布版本都是Go团队能拿出的最好版本</strong>。</p>
<p>下面我们就来解读一下Go 1.14的变化，看看这个新版本中有哪些值得我们重点关注的变化。</p>
<h2>一. 语言规范</h2>
<p>和其他主流语言相比，Go语言的语法规范的变化那是极其少的（广大Gopher们已经习惯了这个节奏:)），偶尔发布一个变化，那自然是要引起广大Gopher严重关注的:)。不过事先说明：<strong>只要Go版本依然是1.x，那么这个规范变化也是backward-compitable的</strong>。</p>
<p>Go 1.14新增的语法变化是：<a href="https://github.com/golang/proposal/blob/master/design/6977-overlapping-interfaces.md">嵌入接口的方法集可重叠</a>。这个变化背后的朴素思想是这样的。看下面代码(来自<a href="https://github.com/golang/go/issues/6977">这里</a>)：</p>
<pre><code>type I interface { f(); String() string }
type J interface { g(); String() string }

type IJ interface { I; J }  ----- (1)
type IJ interface { f(); g(); String() string }  ---- (2)

</code></pre>
<p>代码中已知定义的I和J两个接口的方法集中都包含有<code>String() string</code>这个方法。在这样的情况下，我们如果想定义一个方法集合为Union(I, J)的新接口<code>IJ</code>，我们在Go 1.13及之前的版本中只能使用第(2)种方式，即只能在新接口<code>IJ</code>中重新书写一遍所有的方法原型，而无法像第(1)种方式那样使用嵌入接口的简洁方式进行。</p>
<p>Go 1.14通过支持<a href="https://github.com/golang/proposal/blob/master/design/6977-overlapping-interfaces.md">嵌入接口的方法集可重叠</a>解决了这个问题：</p>
<pre><code>// go1.14-examples/overlapping_interface.go
package foo

type I interface {
    f()
    String() string
}
type J interface {
    g()
    String() string
}

type IJ interface {
    I
    J
}

在go 1.13.6上运行：

$go build overlapping_interface.go
# command-line-arguments
./overlapping_interface.go:14:2: duplicate method String

但在go 1.14上运行：

$go build overlapping_interface.go

// 一切ok，无报错

</code></pre>
<p>不过对overlapping interface的支持仅限于接口定义中，如果你要在struct定义中嵌入interface，比如像下面这样：</p>
<pre><code>// go1.14-examples/overlapping_interface1.go
package main

type I interface {
    f()
    String() string
}

type implOfI struct{}

func (implOfI) f() {}
func (implOfI) String() string {
    return "implOfI"
}

type J interface {
    g()
    String() string
}

type implOfJ struct{}

func (implOfJ) g() {}
func (implOfJ) String() string {
    return "implOfJ"
}

type Foo struct {
    I
    J
}

func main() {
    f := Foo{
        I: implOfI{},
        J: implOfJ{},
    }
    println(f.String())
}

</code></pre>
<p>虽然Go编译器<strong>没有直接指出结构体Foo中嵌入的两个接口I和J存在方法的重叠</strong>，但在使用Foo结构体时，下面的编译器错误肯定还是会给出的：</p>
<pre><code>$ go run overlapping_interface1.go
# command-line-arguments
./overlapping_interface1.go:37:11: ambiguous selector f.String

</code></pre>
<p>对于结构体中嵌入的接口的方法集是否存在overlap，go编译器似乎并没有严格做“实时”检查，这个检查被延迟到为结构体实例选择method的执行者环节了，就像上面例子那样。如果我们此时让Foo结构体 override一个String方法，那么即便I和J的方法集存在overlap也是无关紧要的，因为编译器不会再模棱两可，可以正确的为Foo实例选出究竟执行哪个String方法：</p>
<pre><code>// go1.14-examples/overlapping_interface2.go

.... ....

func (Foo) String() string {
        return "Foo"
}

func main() {
        f := Foo{
                I: implOfI{},
                J: implOfJ{},
        }
        println(f.String())
}

运行该代码：

$go run overlapping_interface2.go
Foo

</code></pre>
<h2>二.  Go运行时</h2>
<h3>1. 支持异步抢占式调度</h3>
<p>在<a href="https://tonybai.com/2017/11/23/the-simple-analysis-of-goroutine-schedule-examples">《Goroutine调度实例简要分析》</a>一文中，我曾提到过这样一个例子：</p>
<pre><code>// go1.14-examples/preemption_scheduler.go
package main

import (
    "fmt"
    "runtime"
    "time"
)

func deadloop() {
    for {
    }
}

func main() {
    runtime.GOMAXPROCS(1)
    go deadloop()
    for {
        time.Sleep(time.Second * 1)
        fmt.Println("I got scheduled!")
    }
}

</code></pre>
<p>在只有一个<code>P</code>的情况下，上面的代码中deadloop所在goroutine将持续占据该<code>P</code>，使得main goroutine中的代码得不到调度(GOMAXPROCS=1的情况下)，因此我们无法看到<code>I got scheduled!</code>字样输出。这是因为Go 1.13及以前的版本的抢占是”协作式“的，只在有函数调用的地方才能插入“抢占”代码(埋点)，而deadloop没有给编译器插入抢占代码的机会。这会导致GC在等待所有goroutine停止时等待时间过长，从而<a href="https://github.com/golang/go/issues/10958">导致GC延迟</a>；甚至在一些特殊情况下，导致在STW（stop the world）时死锁。</p>
<p>Go 1.14采用了基于系统信号的异步抢占调度，这样上面的deadloop所在的goroutine也可以被抢占了：</p>
<pre><code>// 使用Go 1.14版本编译器运行上述代码

$go run preemption_scheduler.go
I got scheduled!
I got scheduled!
I got scheduled!

</code></pre>
<p>不过由于系统信号可能在代码执行到任意地方发生，在Go runtime能cover到的地方，Go runtime自然会处理好这些系统信号。但是如果你是通过<code>syscall</code>包或<code>golang.org/x/sys/unix</code>在Unix/Linux/Mac上直接进行系统调用，那么一旦在系统调用执行过程中进程收到系统中断信号，这些系统调用就会失败，并以<strong>EINTR错误</strong>返回，尤其是低速系统调用，包括：读写特定类型文件(管道、终端设备、网络设备)、进程间通信等。在这样的情况下，我们就需要自己处理<strong>EINTR错误</strong>。一个最常见的错误处理方式就是重试。对于可重入的系统调用来说，在<a href="http://man7.org/linux/man-pages/man7/signal.7.html">收到EINTR信号后的重试是安全的</a>。如果你没有自己调用syscall包，那么异步抢占调度对你已有的代码几乎无影响。</p>
<p>Go 1.14的异步抢占调度在windows/arm, darwin/arm, js/wasm, and plan9/&#42;上依然尚未支持，Go团队<a href="https://github.com/golang/go/issues/36365">计划在Go 1.15中解决掉这些问题</a>。</p>
<h3>2. defer性能得以继续优化</h3>
<p>在<a href="https://tonybai.com/2019/10/27/some-changes-in-go-1-13/">Go 1.13</a>中，defer性能得到理论上30%的提升。我们还用那个例子来看看go 1.14与go 1.13版本相比defer性能又有多少提升，同时再看看使用defer和不使用defer的对比：</p>
<pre><code>// go1.14-examples/defer_benchmark_test.go
package defer_test

import "testing"

func sum(max int) int {
    total := 0
    for i := 0; i &lt; max; i++ {
        total += i
    }

    return total
}

func foo() {
    defer func() {
        sum(10)
    }()

    sum(100)
}

func Bar() {
    sum(100)
    sum(10)
}

func BenchmarkDefer(b *testing.B) {
    for i := 0; i &lt; b.N; i++ {
        foo()
    }
}
func BenchmarkWithoutDefer(b *testing.B) {
    for i := 0; i &lt; b.N; i++ {
        Bar()
    }
}

</code></pre>
<p>我们分别用Go 1.13和Go 1.14运行上面的基准测试代码：</p>
<pre><code>Go 1.13:

$go test -bench . defer_benchmark_test.go
goos: darwin
goarch: amd64
BenchmarkDefer-8              17873574            66.7 ns/op
BenchmarkWithoutDefer-8       26935401            43.7 ns/op
PASS
ok      command-line-arguments    2.491s

Go 1.14:

$go test -bench . defer_benchmark_test.go
goos: darwin
goarch: amd64
BenchmarkDefer-8              26179819            45.1 ns/op
BenchmarkWithoutDefer-8       26116602            43.5 ns/op
PASS
ok      command-line-arguments    2.418s
</code></pre>
<p>我们看到，Go 1.14的defer性能照比Go 1.13还有大幅提升，并且已经与不使用defer的性能相差无几了，这也是Go官方鼓励大家在性能敏感的代码执行路径上也大胆使用defer的原因。</p>
<p><img src="https://tonybai.com/wp-content/uploads/some-changes-in-go-1-14-3.png" alt="img{512x368}" /></p>
<p><center>图：各个Go版本defer性能对比(图来自于https://twitter.com/janiszt/status/1215601972281253888)</center></p>
<h3>3. internal timer的重新实现</h3>
<p>鉴于go timer<a href="https://github.com/golang/go/issues/6239">长期以来性能不能令人满意</a>，Go 1.14几乎重新实现了runtime层的timer。其实现思路遵循了<a href="https://github.com/golang/go/issues/6239#issuecomment-206361959">Dmitry Vyukov几年前提出的实现逻辑</a>：将timer分配到每个P上，降低锁竞争；去掉timer thread，减少上下文切换开销；使用netpoll的timeout实现timer机制。</p>
<pre><code>// $GOROOT/src/runtime/time.go

type timer struct {
        // If this timer is on a heap, which P's heap it is on.
        // puintptr rather than *p to match uintptr in the versions
        // of this struct defined in other packages.
        pp puintptr

}

// addtimer adds a timer to the current P.
// This should only be called with a newly created timer.
// That avoids the risk of changing the when field of a timer in some P's heap,
// which could cause the heap to become unsorted.

func addtimer(t *timer) {
        // when must never be negative; otherwise runtimer will overflow
        // during its delta calculation and never expire other runtime timers.
        if t.when &lt; 0 {
                t.when = maxWhen
        }
        if t.status != timerNoStatus {
                badTimer()
        }
        t.status = timerWaiting

        addInitializedTimer(t)
}

// addInitializedTimer adds an initialized timer to the current P.
func addInitializedTimer(t *timer) {
        when := t.when

        pp := getg().m.p.ptr()
        lock(&amp;pp.timersLock)
        ok := cleantimers(pp) &amp;&amp; doaddtimer(pp, t)
        unlock(&amp;pp.timersLock)
        if !ok {
                badTimer()
        }

        wakeNetPoller(when)
}
... ...

</code></pre>
<p>这样你的程序中如果大量使用time.After、time.Tick或者在处理网络连接时大量使用SetDeadline，使用Go 1.14编译后，你的应用将得到timer性能的<a href="https://github.com/golang/go/commit/6becb033341602f2df9d7c55cc23e64b925bbee2">自然提升</a>。</p>
<p><img src="https://tonybai.com/wp-content/uploads/some-changes-in-go-1-14-2.png" alt="img{512x368}" /></p>
<p><center>图：切换到新timer实现后的各Benchmark数据</center></p>
<h2>三. Go module已经production ready了</h2>
<p>Go 1.14中带来的关于<a href="https://tonybai.com/2019/12/21/go-modules-minimal-version-selection/">go module</a>的最大惊喜就是Go module已经production ready了，这意味着关于go module的运作机制，go tool的各种命令和其参数形式、行为特征已趋稳定了。笔者从<a href="https://tonybai.com/2018/11/19/some-changes-in-go-1-11/">Go 1.11</a>引入<a href="https://tonybai.com/2018/07/15/hello-go-module/">go module</a>以来就一直关注和使用Go module，尤其是<a href="https://tonybai.com/2019/10/27/some-changes-in-go-1-13/">Go 1.13</a>中增加go module proxy的支持，使得中国大陆的gopher再也不用为获取类似<code>golang.org/x/xxx</code>路径下的module而苦恼了。</p>
<p>Go 1.14中go module的主要变动如下：</p>
<p>a) module-aware模式下对vendor的处理：如果go.mod中go version是go 1.14及以上，且当前repo顶层目录下有vendor目录，那么go工具链将默认使用vendor(即-mod=vendor)中的package，而不是module cache中的($GOPATH/pkg/mod下)。同时在这种模式下，go 工具会校验vendor/modules.txt与go.mod文件，它们需要保持同步，否则报错。</p>
<p>在上述前提下，如要非要使用module cache构建，则需要为go工具链显式传入<code>-mod=mod</code> ，比如：<code>go build -mod=mod ./...</code>。</p>
<p>b) 增加GOINSECURE，可以不再要求非得以https获取module，或者即便使用https，也不再对server证书进行校验。</p>
<p>c) 在module-aware模式下，如果没有建立go.mod或go工具链无法找到go.mod，那么你必须显式传入要处理的go源文件列表，否则go tools将需要你明确go.mod。比如：在一个没有go.mod的目录下，要编译一个hello.go，我们需要使用go build hello.go(hello.go需要显式放在命令后面），如果你执行<code>go build .</code>就会得到类似如下错误信息：</p>
<pre><code>$go build .
go: cannot find main module, but found .git/config in /Users/tonybai
    to create a module there, run:
    cd .. &amp;&amp; go mod init

</code></pre>
<p>也就是说在没有go.mod的情况下，go工具链的功能是受限的。</p>
<p>d) go module支持subversion仓库了，不过subversion使用应该很“小众”了。</p>
<p>要系统全面的了解go module的当前行为机制，建议还是通读一遍<a href="https://tip.golang.org/cmd/go/">Go command手册</a>中关于module的说明以及官方<a href="https://github.com/golang/go/wiki/Modules">go module wiki</a>。</p>
<h2>四. 编译器</h2>
<p>Go 1.14 go编译器在-race和-msan的情况下，默认会执行<code>-d=checkptr</code>，即对unsafe.Pointer的使用进行<a href="https://github.com/golang/go/issues/34964">合法性检查</a>，主要检查两项内容：</p>
<ul>
<li>当将unsafe.Pointer转型为<code>*T</code>时，T的内存对齐系数不能高于原地址的</li>
</ul>
<p>比如下面代码：</p>
<pre><code>// go1.14-examples/compiler_checkptr1.go
package main

import (
    "fmt"
    "unsafe"
)

func main() {
    var byteArray = [10]byte{'a', 'b', 'c'}
    var p *int64 = (*int64)(unsafe.Pointer(&amp;byteArray[1]))
    fmt.Println(*p)
}

</code></pre>
<p>以-race运行上述代码：</p>
<pre><code>$go run -race compiler_checkptr1.go
fatal error: checkptr: unsafe pointer conversion

goroutine 1 [running]:
runtime.throw(0x11646fd, 0x23)
    /Users/tonybai/.bin/go1.14/src/runtime/panic.go:1112 +0x72 fp=0xc00004cee8 sp=0xc00004ceb8 pc=0x106d152
runtime.checkptrAlignment(0xc00004cf5f, 0x1136880, 0x1)
    /Users/tonybai/.bin/go1.14/src/runtime/checkptr.go:13 +0xd0 fp=0xc00004cf18 sp=0xc00004cee8 pc=0x1043b70
main.main()
    /Users/tonybai/go/src/github.com/bigwhite/experiments/go1.14-examples/compiler_checkptr1.go:10 +0x70 fp=0xc00004cf88 sp=0xc00004cf18 pc=0x11283b0
runtime.main()
    /Users/tonybai/.bin/go1.14/src/runtime/proc.go:203 +0x212 fp=0xc00004cfe0 sp=0xc00004cf88 pc=0x106f7a2
runtime.goexit()
    /Users/tonybai/.bin/go1.14/src/runtime/asm_amd64.s:1373 +0x1 fp=0xc00004cfe8 sp=0xc00004cfe0 pc=0x109b801
exit status 2

</code></pre>
<p>checkptr检测到：转换后的int64类型的内存对齐系数严格程度要高于转化前的原地址(一个byte变量的地址)。int64对齐系数为8，而一个byte变量地址对齐系数仅为1。</p>
<ul>
<li>做完指针算术后，转换后的unsafe.Pointer仍应指向原先Go堆对象</li>
</ul>
<pre><code>compiler_checkptr2.go
package main

import (
    "unsafe"
)

func main() {
    var n = 5
    b := make([]byte, n)
    end := unsafe.Pointer(uintptr(unsafe.Pointer(&amp;b[0])) + uintptr(n+10))
    _ = end
}

</code></pre>
<p>运行上述代码：</p>
<pre><code>$go run  -race compiler_checkptr2.go
fatal error: checkptr: unsafe pointer arithmetic

goroutine 1 [running]:
runtime.throw(0x10b618b, 0x23)
    /Users/tonybai/.bin/go1.14/src/runtime/panic.go:1112 +0x72 fp=0xc00003e720 sp=0xc00003e6f0 pc=0x1067192
runtime.checkptrArithmetic(0xc0000180b7, 0xc00003e770, 0x1, 0x1)
    /Users/tonybai/.bin/go1.14/src/runtime/checkptr.go:41 +0xb5 fp=0xc00003e750 sp=0xc00003e720 pc=0x1043055
main.main()
    /Users/tonybai/go/src/github.com/bigwhite/experiments/go1.14-examples/compiler_checkptr2.go:10 +0x8d fp=0xc00003e788 sp=0xc00003e750 pc=0x1096ced
runtime.main()
    /Users/tonybai/.bin/go1.14/src/runtime/proc.go:203 +0x212 fp=0xc00003e7e0 sp=0xc00003e788 pc=0x10697e2
runtime.goexit()
    /Users/tonybai/.bin/go1.14/src/runtime/asm_amd64.s:1373 +0x1 fp=0xc00003e7e8 sp=0xc00003e7e0 pc=0x1092581
exit status 2

</code></pre>
<p>checkptr检测到转换后的unsafe.Pointer已经超出原先heap object: b的范围了，于是报错。</p>
<p>不过目前Go标准库依然<a href="https://github.com/golang/go/issues/34972">尚未能完全通过checkptr的检查</a>，因为有些库代码显然违反了<a href="https://tip.golang.org/pkg/unsafe/#Pointer">unsafe.Pointer的使用规则</a>。</p>
<p><a href="https://tonybai.com/2019/10/27/some-changes-in-go-1-13/">Go 1.13</a>引入了新的Escape Analysis，Go 1.14中我们可以通过<code>-m=2</code>查看详细的逃逸分析过程日志，比如：</p>
<pre><code>$go run  -gcflags '-m=2' compiler_checkptr2.go
# command-line-arguments
./compiler_checkptr2.go:7:6: can inline main as: func() { var n int; n = 5; b := make([]byte, n); end := unsafe.Pointer(uintptr(unsafe.Pointer(&amp;b[0])) + uintptr(n + 100)); _ = end }
./compiler_checkptr2.go:9:11: make([]byte, n) escapes to heap:
./compiler_checkptr2.go:9:11:   flow: {heap} = &amp;{storage for make([]byte, n)}:
./compiler_checkptr2.go:9:11:     from make([]byte, n) (non-constant size) at ./compiler_checkptr2.go:9:11
./compiler_checkptr2.go:9:11: make([]byte, n) escapes to heap

</code></pre>
<h2>五. 标准库</h2>
<p>每个Go版本，变化最多的就是标准库，这里我们挑一个可能影响后续我们编写单元测试行为方式的变化说说，那就是testing包的T和B类型都增加了自己的<a href="https://tip.golang.org/pkg/testing/#T.Cleanup">Cleanup方法</a>。我们通过代码来看一下Cleanup方法的作用：</p>
<pre><code>// go1.14-examples/testing_cleanup_test.go
package main

import "testing"

func TestCase1(t *testing.T) {

    t.Run("A=1", func(t *testing.T) {
        t.Logf("subtest1 in testcase1")

    })
    t.Run("A=2", func(t *testing.T) {
        t.Logf("subtest2 in testcase1")
    })
    t.Cleanup(func() {
        t.Logf("cleanup1 in testcase1")
    })
    t.Cleanup(func() {
        t.Logf("cleanup2 in testcase1")
    })
}

func TestCase2(t *testing.T) {
    t.Cleanup(func() {
        t.Logf("cleanup1 in testcase2")
    })
    t.Cleanup(func() {
        t.Logf("cleanup2 in testcase2")
    })
}
</code></pre>
<p>运行上面测试：</p>
<pre><code>$go test -v testing_cleanup_test.go
=== RUN   TestCase1
=== RUN   TestCase1/A=1
    TestCase1/A=1: testing_cleanup_test.go:8: subtest1 in testcase1
=== RUN   TestCase1/A=2
    TestCase1/A=2: testing_cleanup_test.go:12: subtest2 in testcase1
    TestCase1: testing_cleanup_test.go:18: cleanup2 in testcase1
    TestCase1: testing_cleanup_test.go:15: cleanup1 in testcase1
--- PASS: TestCase1 (0.00s)
    --- PASS: TestCase1/A=1 (0.00s)
    --- PASS: TestCase1/A=2 (0.00s)
=== RUN   TestCase2
    TestCase2: testing_cleanup_test.go:27: cleanup2 in testcase2
    TestCase2: testing_cleanup_test.go:24: cleanup1 in testcase2
--- PASS: TestCase2 (0.00s)
PASS
ok      command-line-arguments    0.005s
</code></pre>
<p>我们看到：</p>
<ul>
<li>
<p>Cleanup方法运行于所有测试以及其子测试完成之后。</p>
</li>
<li>
<p>Cleanup方法类似于defer，先注册的cleanup函数后执行（比如上面例子中各个case的cleanup1和cleanup2）。</p>
</li>
</ul>
<p>在拥有Cleanup方法前，我们经常像下面这样做：</p>
<pre><code>// go1.14-examples/old_testing_cleanup_test.go
package main

import "testing"

func setup(t *testing.T) func() {
    t.Logf("setup before test")
    return func() {
        t.Logf("teardown/cleanup after test")
    }
}

func TestCase1(t *testing.T) {
    f := setup(t)
    defer f()
    t.Logf("test the testcase")
}
</code></pre>
<p>运行上面测试：</p>
<pre><code>$go test -v old_testing_cleanup_test.go
=== RUN   TestCase1
    TestCase1: old_testing_cleanup_test.go:6: setup before test
    TestCase1: old_testing_cleanup_test.go:15: test the testcase
    TestCase1: old_testing_cleanup_test.go:8: teardown/cleanup after test
--- PASS: TestCase1 (0.00s)
PASS
ok      command-line-arguments    0.005s
</code></pre>
<p>有了Cleanup方法后，我们就不需要再像上面那样单独编写一个返回cleanup函数的setup函数了。</p>
<p>此次Go 1.14还将对unicode标准的支持从unicode 11 升级到 unicode 12 ，共增加了554个新字符。</p>
<h2>六. 其他</h2>
<p>超强的<a href="http://tonybai.com/2017/06/27/an-intro-about-go-portability/">可移植性</a>是Go的一个知名标签，在新平台支持方面，Go向来是“急先锋”。Go 1.14为64bit RISC-V提供了在linux上的实验性支持(GOOS=linux, GOARCH=riscv64)。</p>
<p>rust语言已经通过<a href="https://github.com/rust-fuzz/cargo-fuzz">cargo-fuzz</a>从工具层面为fuzz test提供了基础支持。Go 1.14也在这方面<a href="https://github.com/golang/go/issues/14565">做出了努力</a>，并且Go已经在向<a href="https://github.com/golang/go/issues/19109">将fuzz test变成Go test的一等公民</a>而努力。</p>
<h2>七. 小结</h2>
<p>Go 1.14的详细变更说明在<a href="https://tip.golang.org/doc/go1.14">这里</a>可以查看。整个版本的milestone对应的issue集合在<a href="https://github.com/golang/go/milestone/95?closed=1">这里</a>。</p>
<p>不过目前Go 1.14在特定版本linux内核上会出现crash的问题，当然这个问题源于这些内核的一个已知bug。在<a href="https://github.com/golang/go/issues/37436#issuecomment-591327351">这个issue</a>中有关于这个问题的详细说明，涉及到的Linux内核版本包括：5.2.x, 5.3.0-5.3.14, 5.4.0-5.4.1。<br />
本篇博客涉及的代码在<a href="https://github.com/bigwhite/experiments/tree/master/go1.14-examples">这里</a>可以下载。</p>
<hr />
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网上线了，感谢小伙伴们学习支持！</p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<p>微博：https://weibo.com/bigwhite20xx<br />
微信公众号：iamtonybai<br />
博客：tonybai.com<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2020, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2020/03/08/some-changes-in-go-1-14/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>Go 1.10中值得关注的几个变化</title>
		<link>https://tonybai.com/2018/02/17/some-changes-in-go-1-10/</link>
		<comments>https://tonybai.com/2018/02/17/some-changes-in-go-1-10/#comments</comments>
		<pubDate>Fri, 16 Feb 2018 22:37:49 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[alias]]></category>
		<category><![CDATA[benchmark]]></category>
		<category><![CDATA[bytes]]></category>
		<category><![CDATA[cache]]></category>
		<category><![CDATA[Compiler]]></category>
		<category><![CDATA[FlameGraph]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Go1]]></category>
		<category><![CDATA[Go1.10]]></category>
		<category><![CDATA[go1.4]]></category>
		<category><![CDATA[Go1.5]]></category>
		<category><![CDATA[Go1.6]]></category>
		<category><![CDATA[Go1.7]]></category>
		<category><![CDATA[go1.8]]></category>
		<category><![CDATA[go1.9]]></category>
		<category><![CDATA[Go2]]></category>
		<category><![CDATA[GOCACHE]]></category>
		<category><![CDATA[gocmpp]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[GOPATH]]></category>
		<category><![CDATA[GopherCon]]></category>
		<category><![CDATA[GOROOT]]></category>
		<category><![CDATA[GOTMPDIR]]></category>
		<category><![CDATA[iso]]></category>
		<category><![CDATA[nm]]></category>
		<category><![CDATA[pprof]]></category>
		<category><![CDATA[RobertGriesemer]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[RussCox]]></category>
		<category><![CDATA[Slice]]></category>
		<category><![CDATA[SSA]]></category>
		<category><![CDATA[strings]]></category>
		<category><![CDATA[Unicode]]></category>
		<category><![CDATA[unsafe]]></category>
		<category><![CDATA[vet]]></category>
		<category><![CDATA[切片]]></category>
		<category><![CDATA[垃圾回收]]></category>
		<category><![CDATA[增量构建]]></category>
		<category><![CDATA[常量]]></category>
		<category><![CDATA[移位操作]]></category>
		<category><![CDATA[缓存]]></category>
		<category><![CDATA[编译器]]></category>
		<category><![CDATA[自举]]></category>
		<category><![CDATA[语言规范]]></category>
		<category><![CDATA[运行时]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2558</guid>
		<description><![CDATA[又到了Go语言新版本的发布时间窗口了！这次的主角是Go 1.10。 曾几何时， 这是很多Gopher在Go 1.8、Go 1.9时猜测是否存在的那个版本，毕竟minor version即将进化到两位数。从Go语言第一封设计mail发出到现在的十年间，尤其是Go语言经历了近几年的爆发式增长，基本奠定了云原生第一语言的位置之后，人们对Go语言有了更多新的、更为深刻的认知，同时对这门编程语言也有了更多的改进和优化的期望。Go2在Gopher心中的位置日益提升，直到Russ Cox在GopherCon 2017上公布了Go core team对Go2的开发策略，我们才意识到：哦，Go1还将继续一段时间，甚至是一段很长的时间。2018年2月，我们将迎来Go 1.10版本。 从Go 1.4版本开始，我自己都没想到我能将“Go x.x中值得关注的几个变化”这个系列一直写到Go 1.10。不过现在看来，这个系列还会继续，以后可能还有Go 1.11、Go 1.12&#8230;，甚至是进化到Go2之后的各个版本。 Go从1.0版本发布之日起，便遵守着自己“变与不变”的哲学。不变的是对Go对“Go1 promise of compatibility”的严格遵守，变化的则是对语言性能、运行时、GC、工具以及标准库更为精细和耐心地打磨。这次发布的Go 1.10依然延续着这种理念，将重点的改进放在了运行时、工具以及标准库上。接下来，我就和大家一起看看即将发布的Go 1.10都有哪些值得重点关注的变化。 一、语言 Go language Spec是当前Go语言的唯一语言规范标准，虽然其严谨性与那些以ISO标准形式编写成的语言规范（比如：C语言、C++语言的规范）还有一定差距。因此，对go spec的优化，就是在严谨性方面下功夫。当前spec的主要修订者是Go语言三个设计者之一的Robert Griesemer，他在Go 1.10周期对spec做了较多语言概念严谨性方面的改进。 1、显式定义Representability（可表示性） 在Properties of types and values章节下，Robert Griesemer显式引入了一个新的术语Representability，这里译为可表示性。这一术语的引入并未带来语法的变化，只是为了更精确的阐释规范。Representability的定义明确了当规范中出现“a constant x is representable by a value of type T”时成立的几种条件，尤其是针对浮点类型和复数类型。这里摘录（不翻译）： A constant x is representable by a [...]]]></description>
			<content:encoded><![CDATA[<p>又到了Go语言新版本的发布时间窗口了！这次的主角是<a href="https://tip.golang.org/doc/go1.10">Go 1.10</a>。</p>
<p><img src="http://tonybai.com/wp-content/uploads/go-1.10-release.png" alt="img{512x368}" /></p>
<p>曾几何时， 这是很多Gopher在<a href="http://tonybai.com/2017/02/03/some-changes-in-go-1-8/">Go 1.8</a>、<a href="http://tonybai.com/2017/07/14/some-changes-in-go-1-9/">Go 1.9</a>时猜测是否存在的那个版本，毕竟minor version即将进化到两位数。从Go语言第一封设计mail发出到现在的<a href="http://tonybai.com/2017/09/24/go-ten-years-and-climbing/">十年间</a>，尤其是Go语言经历了近几年的爆发式增长，基本奠定了云原生第一语言的位置之后，人们对Go语言有了更多新的、更为深刻的认知，同时对这门编程语言也有了更多的改进和优化的期望。Go2在Gopher心中的位置日益提升，直到<a href="https://swtch.com/~rsc/">Russ Cox</a>在<a href="https://github.com/gophercon/2017-talks">GopherCon 2017</a>上公布了Go core team对<a href="https://blog.golang.org/toward-go2">Go2的开发策略</a>，我们才意识到：<strong>哦，Go1还将继续一段时间，甚至是一段很长的时间。2018年2月，我们将迎来Go 1.10版本</strong>。</p>
<p>从<a href="http://tonybai.com/2014/11/04/some-changes-in-go-1-4/">Go 1.4版本</a>开始，我自己都没想到我能将<strong>“Go x.x中值得关注的几个变化”</strong>这个系列一直写到Go 1.10。不过现在看来，这个系列还会继续，以后可能还有Go 1.11、Go 1.12&#8230;，甚至是进化到Go2之后的各个版本。</p>
<p>Go从<a href="https://blog.golang.org/go-version-1-is-released">1.0版本发布</a>之日起，便遵守着自己<strong>“变与不变”</strong>的哲学。不变的是对Go对<a href="https://tip.golang.org/doc/go1compat.html">“Go1 promise of compatibility”</a>的严格遵守，变化的则是对语言性能、运行时、GC、工具以及标准库更为精细和耐心地打磨。这次发布的Go 1.10依然延续着这种理念，将重点的改进放在了运行时、工具以及标准库上。接下来，我就和大家一起看看即将发布的Go 1.10都有哪些值得重点关注的变化。</p>
<h2>一、语言</h2>
<p><a href="https://golang.org/ref/spec">Go language Spec</a>是当前<a href="http://tonybai.com/tag/go">Go语言</a>的唯一语言规范标准，虽然其严谨性与那些以ISO标准形式编写成的语言规范（比如：C语言、C++语言的规范）还有一定差距。因此，对go spec的优化，就是在严谨性方面下功夫。当前spec的主要修订者是Go语言三个设计者之一的<a href="https://github.com/griesemer">Robert Griesemer</a>，他在Go 1.10周期对spec<strong>做了较多语言概念严谨性方面的改进</strong>。</p>
<h3>1、显式定义Representability（可表示性）</h3>
<p>在<a href="https://tip.golang.org/ref/spec#Properties_of_types_and_values">Properties of types and values</a>章节下，Robert Griesemer显式引入了一个新的术语<a href="https://tip.golang.org/ref/spec#Representability">Representability</a>，这里译为<strong>可表示性</strong>。这一术语的<a href="https://github.com/golang/go/commit/b40831b115e46e9de719bddccf2d27d7d4940756#diff-94792ed6b8d17736ee79af0ca532d6fe">引入</a>并未带来语法的变化，只是为了更精确的阐释规范。Representability的定义明确了当规范中出现“a constant x is representable by a value of type T”时成立的几种条件，尤其是针对浮点类型和复数类型。这里摘录（不翻译）：</p>
<pre><code>A constant x is representable by a value of type T if one of the following conditions applies:

- x is in the set of values determined by T.
- T is a floating-point type and x can be rounded to T's precision without overflow. Rounding uses IEEE 754 round-to-even rules but with an IEEE negative zero further simplified to an unsigned zero. Note that constant values never result in an IEEE negative zero, NaN, or infinity.
- T is a complex type, and x's components real(x) and imag(x) are representable by values of T's component type (float32 or float64).
</code></pre>
<h3>2、澄清未指定类型的常量作为shift(移位)非常量位操作的左操作数时在某些特定上下文中的类型</h3>
<p>虽然不及ISO标准规范严谨，但凡是language spec，理解起来都是有门槛的。这个<a href="https://github.com/golang/go/commit/9690d245d56d547c40dac269140dcddc6eb80904#diff-94792ed6b8d17736ee79af0ca532d6fe">改进针对的是那些未指定类型的常量</a>，在作为shift非常量位操作的左操作数时，在shift表达式结果作为下标表达式中的下标、切片表达式下标或者make函数调用中的size参数时，这个常量将被赋予int类型。我们还是看个例子更加直观：</p>
<pre><code>// go1.10-examples/spec/untypedconst.go
package main

var (
    s uint = 2
)

func main() {
    a := make([]int, 10)
    a[1.0&lt;&lt;s] = 4
}

</code></pre>
<p>上面的例子中，重点看<strong>a[1.0 &lt;&lt; s] = 4</strong>这一行，这一行恰好满足了几个条件：</p>
<ul>
<li>1.0 &lt;&lt; s 是一个shift表达式，且作为slide表达式的下标；</li>
<li>shift表达式所移动的位数为s，s是一个变量，非常量，因此这是一个非常量位的移位操作；</li>
<li>1.0是未指定类型的常量(untyped const)，且作为shift表达式左操作数</li>
</ul>
<p>在Go 1.9.2下面，上面的程序编译结果如下：</p>
<pre><code>// go 1.9.2编译器build：

$go build untypedconst.go
# command-line-arguments
./untypedconst.go:9:7: invalid operation: 1 &lt;&lt; s (shift of type float64)
</code></pre>
<p>在Go 1.9.2下，1.0这个常量被compiler赋予了float64类型，导致编译出错。在Go 1.10下，根据最新的spec，1.0被赋予了int型，编译则顺利通过。</p>
<p>但一旦脱离了下标这个上下文环境，1.0这个常量依旧会被compiler识别为float64类型，比如下面代码中1.0&lt;&lt;s作为Println的参数就是不符合语法的：</p>
<pre><code>// go1.10-examples/spec/untypedconst.go
package main

import "fmt"

var (
    s uint = 2
)

func main() {
    a := make([]int, 10)
    a[1.0&lt;&lt;s] = 4
    fmt.Println(1.0&lt;&lt;s)
}

// go 1.10rc2编译器build：
 $go build untypedconst.go
# command-line-arguments
./untypedconst.go:12:17: invalid operation: 1 &lt;&lt; s (shift of type float64)
./untypedconst.go:12:17: cannot use 1 &lt;&lt; s as type interface {} in argument to fmt.Println
</code></pre>
<h3>3、明确预声明类型(predeclared type)是defined type还是alias type</h3>
<p>Go在<a href="http://tonybai.com/2017/07/14/some-changes-in-go-1-9/">1.9版本</a>中引入了alias语法，同时引入defined type(以替代named type)和alias type，并使用alias语法对某些predeclared type的实现进行了调整。在Go 1.10 spec中，Griesemer进一步<a href="https://github.com/golang/go/commit/4a2391e7c965f7ed4d1ec189087293c1f6bae43c#diff-94792ed6b8d17736ee79af0ca532d6fe">明确了哪些predeclared type是alias type</a>：</p>
<pre><code>目前内置的predeclared type只有两个类型是alias type:

byte        alias for uint8
rune        alias for int32
</code></pre>
<p>其余的predeclared type都是defined type。</p>
<h3>4、移除spec中对method expression: T.m中T的类型的限制</h3>
<p>这次是spec落伍于compiler了。Go 1.9.2就可以顺利编译运行下面的代码：</p>
<pre><code>//go1.10-examples/spec/methodexpression.go
package main

import "fmt"

type foo struct{}
func (foo)f() {
    fmt.Println("i am foo")
}

func main() {
    interface{f()}.f(foo{})
}

</code></pre>
<p>但在Go 1.9.2的spec中，对Method expression的定义如下：</p>
<pre><code>Go 1.9.2 spec:

MethodExpr    = ReceiverType "." MethodName .
ReceiverType  = TypeName | "(" "*" TypeName ")" | "(" ReceiverType ")" .
</code></pre>
<p>Go 1.9.2的spec说，method expression形式:T.m中的T仅能使用Typename，而非上述代码中type实现。Go 1.10的spec中<a href="https://github.com/golang/go/commit/f2d52519e1fad35566afb46ef521934cf0f5e5fd#diff-94792ed6b8d17736ee79af0ca532d6fe">放开了对method expression中T的限制</a>，使得type的实现也可以作为T调用method，与编译器的实际实现行为同步：</p>
<pre><code>Go 1.10rc2 spec:

MethodExpr    = ReceiverType "." MethodName .
ReceiverType  = Type .
</code></pre>
<p>不过目前Go 1.10 rc2 compiler还存在<a href="https://github.com/golang/go/issues/22444">一个问题</a>，我们看一下下面的代码：</p>
<pre><code>//go1.10-examples/spec/methodexpression1.go
package main

func main() {
    (*struct{ error }).Error(nil)
}
</code></pre>
<p>使用Go 110rc2构建该源码，得到如下错误：</p>
<pre><code>$go build methodexpression1.go
# command-line-arguments
go.(*struct { error }).Error: call to external function
main.main: relocation target go.(*struct { error }).Error not defined
main.main: undefined: "go.(*struct { error }).Error"
</code></pre>
<p>该问题目前已经有<a href="https://github.com/golang/go/issues/22444">issue</a>对应，状态还是Open。</p>
<h2>二、工具</h2>
<p>Go语言有着让其他主流编程语言羡慕的工具集，每次Go版本更新，工具集都会得到进一步的加强，无论是功能还是从开发者体验方面，都有提升。</p>
<h3>1、默认的GOROOT</h3>
<p>继<a href="http://tonybai.com/2017/02/03/some-changes-in-go-1-8/">Go 1.8版本</a>引入默认的GOPATH后，Go 1.10版本为继续改进Go工具的开发者体验，进一步降低新手的使用门槛，引入了默认GOROOT：即开发者无需显式设置GOROOT环境变量，go程序会自动根据自己所在路径推导出GOROOT的路径。这样一来，Gopher们就可以将下载的Go预编译好的安装包解压放置到任意本地路径下，唯一要做的就是将go二进制程序路径放置到PATH环境变量中。比如我们将go1.10rc2的安装包解压到下面路径下：</p>
<pre><code>➜  /Users/tony/.bin/go1.10rc2 $ls
AUTHORS            LICENSE            VERSION            blog/            lib/            robots.txt
CONTRIBUTING.md        PATENTS            api/            doc/            misc/            src/
CONTRIBUTORS        README.md        bin/            favicon.ico        pkg/            test/
</code></pre>
<p>在设置为PATH后，我们通过go env命令查看go自动推导的GOROOT以及其他相关变量的值：</p>
<pre><code>$go env
GOARCH="amd64"
GOBIN=""
GOCACHE="/Users/tony/Library/Caches/go-build"
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOOS="darwin"
GOPATH="/Users/tony/go"
GORACE=""
GOROOT="/Users/tony/.bin/go1.10rc2"
GOTMPDIR=""
GOTOOLDIR="/Users/tony/.bin/go1.10rc2/pkg/tool/darwin_amd64"
GCCGO="gccgo"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -gno-record-gcc-switches -fno-common"
</code></pre>
<p>从输出结果看到，go正确找到了安装路径，并得到了GOROOT信息。</p>
<h3>2、增加GOTMPDIR变量</h3>
<p>在上面的go env命令输出内容中，我们发现了一个陌生的变量：GOTMPDIR，其值默认为空串。这个<a href="https://go-review.googlesource.com/c/go/+/75475">GOTMPDIR变量</a>是Go 1.10新引入的变量，用于设置Go tool创建和使用的临时文件的路径的。有人可能会说：这个变量看似没什么必要，直接用系统的/tmp路径就好了啊。但是在/tmp路径中编译和执行编译后的程序至少有两点问题，<a href="https://github.com/golang/go/issues/8451">这些问题</a>实际上在go的issues历史中已经存在许久了：</p>
<ul>
<li>有些机器上/tmp路径下被设置了<a href="https://github.com/golang/go/issues/8451">无执行权限(set noexec)</a></li>
<li>有些机器上/tmp下空间有限</li>
</ul>
<p>我们知道默认情况下，go build和go run都会在/tmp下设置一个临时WORK目录来编译源码和执行编译后的程序的，从下面的一个最简单的helloworld源码的编译执行过程输出(WORK变量)，我们就能看到这点：</p>
<pre><code>// on ubuntu 16.04

# go run -x hello.go
WORK=/tmp/go-build001434392
mkdir -p $WORK/b001/
... ...
mkdir -p $WORK/b001/exe/
cd .
/root/.bin/go1.10rc2/pkg/tool/linux_amd64/link -o $WORK/b001/exe/hello -importcfg $WORK/b001/importcfg.link -s -w -buildmode=exe -buildid=fcYMWp_1J2Xqgzc_Vdga/UpnEUti07R2GzG8dUU3x/MLkSlJVesZhf2kQUaDUU/fcYMWp_1J2Xqgzc_Vdga -extld=gcc /root/.cache/go-build/9f/9f34be2dbcc3f8a62dd6efd6d35be18ecdcbc49e3c8b52b003ecd72b6264e19e-d
$WORK/b001/exe/hello
</code></pre>
<p>我个人就遇到过由于IaaS供应商提供的系统盘（不允许定制和修改）过小，导致系统盘空间满，使得Go应用构建和执行失败的问题。我们来设置一下GOTMPDIR，看看效果。我们将GOTMPDIR设置为~/.gotmp，生效后，重新build上面的那个helloworld代码：</p>
<pre><code># go build -x hello.go
WORK=/root/.gotmp/go-build452283009
mkdir -p $WORK/b001/
cat &gt;$WORK/b001/importcfg &lt;&lt; 'EOF' # internal
... ...
mkdir -p $WORK/b001/exe/
cd .
/root/.bin/go1.10rc2/pkg/tool/linux_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=kO-wBNzMZmfHCKzMDziw/jCGBCt7bcrS5NEN-cR4H/8-du6iTQz8uPH3UC-FtB/kO-wBNzMZmfHCKzMDziw -extld=gcc $WORK/b001/_pkg_.a
/root/.bin/go1.10rc2/pkg/tool/linux_amd64/buildid -w $WORK/b001/exe/a.out # internal
mv $WORK/b001/exe/a.out hello
rm -r $WORK/b001/
</code></pre>
<p>可以看到，go tool转移到我们设置的GOTMPDIR下构建和执行了。</p>
<h3>3、通过cache实现增量构建，提高go tools性能</h3>
<p>Go语言具有较高的编译性能是Go语言最初设计时就确定下来的目标，Go编译器的性能在<a href="http://tonybai.com/2014/11/04/some-changes-in-go-1-4/">Go 1.4.3版本</a>达到顶峰，这虽然是得益于其使用C语言实现，但更重要的是其为高性能构建而定义的便于依赖分析的语言构建模型，同时避免了像C/C++那样的重复多次扫描大量头文件的负担。随着<a href="http://tonybai.com/2015/07/10/some-changes-in-go-1-5/">Go自举的实现</a>，使用Go语言实现的go compiler性能有较大下降，但即便这样，其编译速度在主流编程语言中仍然是数一数二的。在经过了<a href="http://tonybai.com/2016/02/21/some-changes-in-go-1-6/">Go 1.6</a>到<a href="http://tonybai.com/2017/07/14/some-changes-in-go-1-9/">Go1.9</a>等多个版本对compiler的优化后，go compiler的编译速度已经<a href="https://dave.cheney.net/2016/11/19/go-1-8-toolchain-improvements">恢复到Go 1.4.3 compiler的2/3左右</a>或是更为接近的水平。在<a href="http://tonybai.com/2017/07/14/some-changes-in-go-1-9/">Go 1.9版本</a>引入并行编译后，Go team在提升工具性能方面的思路发生了些许变化：不再是一味地进行代码级的性能优化，而是选择通过Cache，重复利用中间结果，实现增量构建，来减少编译构建所用的时间。因此，笔者觉得这个功能是<strong>本次Go 1.10最大的变化之一</strong>。</p>
<h4>1) 概述</h4>
<p>Go 1.10版本以前，我们经常通过go build -i来加快Go项目源码的编译速度，其原因在于go build -i首次执行时会将目标所依赖的package安装到$GOPATH/pkg下面(.a文件)，这样后续执行go build时，构建过程将不会重新编译目标文件的依赖包，而是直接链接首次执行build -i时安装的依赖包，以实现加速编译！以gocmpp/examples/client为例，第二次构建所需时间仅为首次构建的四分之一左右：</p>
<pre><code>➜  $GOPATH/src/github.com/bigwhite/gocmpp/examples/client git:(master) ✗ $time go build -i client.go
go build -i client.go  1.34s user 0.34s system 131% cpu 1.274 total
➜  $GOPATH/src/github.com/bigwhite/gocmpp/examples/client git:(master) ✗ $time go build -i client.go
go build -i client.go  0.38s user 0.16s system 116% cpu 0.465 total
</code></pre>
<p>只有当目标文件的依赖包的源文件发生变化时（比对源文件的修改时间与.a文件的修改时间作为是否重新编译的判断依据），才会重新编译安装这些依赖包。这有些像<a href="http://tonybai.com/2011/05/19/use-command-line-vars-of-make/">Makefile</a>的原理：make工具会比较targets文件和prerequisites文件的修改日期，如果prerequisites文件的日期要比targets文件的日期要新，或者target不存在的话，那么，make就会执行后续定义的命令。</p>
<p>不过即便这样，依然至少有两个问题困扰着Go team和广大Gopher：</p>
<ul>
<li>
<p>基于时间戳的比对，并不“合理”<br />
当某个目标文件的依赖包的源文件内容并未真正发生变化，但“修改时间”发生变化了，比如：添加了一行，保存了；然后又删除了这一行，保存。在这样的情况下，理想的操作是不需要重新编译安装这个依赖包，但目前的go build -i机制会<strong>重新编译并安装这个依赖包</strong>。</p>
</li>
<li>
<p><strong>增量构建</strong>并未实现“常态化”<br />
以前版本中，默认的不带命令行参数的go build命令是不会安装依赖包的，因此每次执行go build，都会重新编译一次依赖包的源码，并将结果放入临时目录以供最终链接使用。也就是说最为常用的go build并未实现增量编译，社区需要常态化的“增量编译”，进一步提高效率。</p>
</li>
</ul>
<p>Go 1.10引入cache机制来解决上述问题。从1.10版本开始，go build tool将维护一个package编译结果的缓存以及一些元数据，缓存默认位于操作系统指定的用户缓存目录中，其中数据用于后续构建重用；不仅go build支持“常态化”的增量构建，go test也支持在特定条件下缓存test结果，从而加快执行测试的速度。</p>
<h4>b) go build with cache</h4>
<p>我们先来直观的看看go 1.10 build带来的效果，初始情况cache为空：</p>
<p>以我的一个小项目gocmpp为例，用go 1.10第一次build该项目：</p>
<pre><code>➜  $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ✗ $time go build
go build  1.22s user 0.43s system 175% cpu 0.939 total
</code></pre>
<p>我们再来构建一次：</p>
<pre><code>➜  $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ✗ $time go build
go build  0.12s user 0.16s system 155% cpu 0.182 total
</code></pre>
<p>0.12s vs. 1.22s！通过cache进行的build将构建时间压缩为原来的1/10！为了弄清楚go build幕后行为，我们清除一下cache(go clean -cache)，再重新build，这次我们通过-v -x 输出详细构建过程：</p>
<p>首次编译的详细输出信息：</p>
<pre><code>➜  $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ✗ $go build -x -v
WORK=/var/folders/2h/xr2tmnxx6qxc4w4w13m01fsh0000gn/T/go-build735203690
github.com/bigwhite/gocmpp/vendor/golang.org/x/text/encoding/internal/identifier
mkdir -p $WORK/b033/
cat &gt;$WORK/b033/importcfg &lt;&lt; 'EOF' # internal
# import config
EOF
cd $(GOPATH)/src/github.com/bigwhite/gocmpp/vendor/golang.org/x/text/encoding/internal/identifier
/Users/tony/.bin/go1.10rc2/pkg/tool/darwin_amd64/compile -o $WORK/b033/_pkg_.a -trimpath $WORK/b033 -p github.com/bigwhite/gocmpp/vendor/golang.org/x/text/encoding/internal/identifier -complete -buildid iZWJNg2FYmWoSCXb640o/iZWJNg2FYmWoSCXb640o -goversion go1.10rc2 -D "" -importcfg $WORK/b033/importcfg -pack -c=4 ./identifier.go ./mib.go
/Users/tony/.bin/go1.10rc2/pkg/tool/darwin_amd64/buildid -w $WORK/b033/_pkg_.a # internal
cp $WORK/b033/_pkg_.a /Users/tony/Library/Caches/go-build/14/14223040d851359359b0e531555a47e22f5dbd4bf434acc136a7c70c1fc3663f-d # internal
github.com/bigwhite/gocmpp/vendor/golang.org/x/text/transform
mkdir -p $WORK/b031/
cat &gt;$WORK/b031/importcfg &lt;&lt; 'EOF' # internal
# import config
packagefile bytes=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/bytes.a
packagefile errors=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/errors.a
packagefile io=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/io.a
packagefile unicode/utf8=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/unicode/utf8.a
EOF

.... ....

cd $(GOPATH)/src/github.com/bigwhite/gocmpp
/Users/tony/.bin/go1.10rc2/pkg/tool/darwin_amd64/compile -o $WORK/b001/_pkg_.a -trimpath $WORK/b001 -p github.com/bigwhite/gocmpp -complete -buildid 6LaoHtjkFhandbEhv7zD/6LaoHtjkFhandbEhv7zD -goversion go1.10rc2 -D "" -importcfg $WORK/b001/importcfg -pack -c=4 ./activetest.go ./client.go ./conn.go ./connect.go ./deliver.go ./fwd.go ./packet.go ./receipt.go ./server.go ./submit.go ./terminate.go
/Users/tony/.bin/go1.10rc2/pkg/tool/darwin_amd64/buildid -w $WORK/b001/_pkg_.a # internal
cp $WORK/b001/_pkg_.a /Users/tony/Library/Caches/go-build/e0/e02a5fec0835ca540b62053fdea82589e686e88bf48f18355ed38d41ad19f334-d # internal
</code></pre>
<p>再次编译的详细输出信息：</p>
<pre><code>$go build -x -v
WORK=/var/folders/2h/xr2tmnxx6qxc4w4w13m01fsh0000gn/T/go-build906548554
</code></pre>
<p>我们来分析一下。首次构建时，我们看到gocmpp依赖的每个包以及自身的包都会被编译，并被copy到/Users/tony/Library/Caches/go-build/下面的某个目录下，包括最终的gocmpp包也是这样。第二次build时，我们看到仅仅输出一行信息，这是因为go compiler在cache中找到了gocmpp包对应的编译好的缓存结果，无需进行实际的编译了。</p>
<p>前面说过，go 1.10 compiler决定是否重新编译包是content based的，而不是依照时间戳比对来决策。我们来修改一个gocmpp包中的文件fwd.go，删除一个空行，再恢复这个空行，保存退出。我们再来编译一下gocmpp:</p>
<pre><code>➜  $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ✗ $go build -x -v
WORK=/var/folders/2h/xr2tmnxx6qxc4w4w13m01fsh0000gn/T/go-build857409409
</code></pre>
<p>可以看到go compiler并没有重新编译任何包。如果我们真实改变了fwd.go的内容，比如删除一个空行，保存后再次编译：</p>
<pre><code>➜  $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ✗ $go build -x -v
WORK=/var/folders/2h/xr2tmnxx6qxc4w4w13m01fsh0000gn/T/go-build437927548
github.com/bigwhite/gocmpp
mkdir -p $WORK/b001/
cat &gt;$WORK/b001/importcfg &lt;&lt; 'EOF' # internal
# import config
packagefile bytes=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/bytes.a
packagefile crypto/md5=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/crypto/md5.a
packagefile encoding/binary=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/encoding/binary.a
packagefile errors=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/errors.a
packagefile fmt=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/fmt.a
packagefile github.com/bigwhite/gocmpp/utils=/Users/tony/Test/GoToolsProjects/pkg/darwin_amd64/github.com/bigwhite/gocmpp/utils.a
packagefile io=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/io.a
packagefile log=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/log.a
packagefile net=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/net.a
packagefile os=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/os.a
packagefile strconv=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/strconv.a
packagefile strings=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/strings.a
packagefile sync=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/sync.a
packagefile sync/atomic=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/sync/atomic.a
packagefile time=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/time.a
EOF
cd /Users/tony/Test/GoToolsProjects/src/github.com/bigwhite/gocmpp
/Users/tony/.bin/go1.10rc2/pkg/tool/darwin_amd64/compile -o $WORK/b001/_pkg_.a -trimpath $WORK/b001 -p github.com/bigwhite/gocmpp -complete -buildid trn5lvvRTk_UP3LcT5CC/trn5lvvRTk_UP3LcT5CC -goversion go1.10rc2 -D "" -importcfg $WORK/b001/importcfg -pack -c=4 ./activetest.go ./client.go ./conn.go ./connect.go ./deliver.go ./fwd.go ./packet.go ./receipt.go ./server.go ./submit.go ./terminate.go
/Users/tony/.bin/go1.10rc2/pkg/tool/darwin_amd64/buildid -w $WORK/b001/_pkg_.a # internal
cp $WORK/b001/_pkg_.a /Users/tony/Library/Caches/go-build/7a/7a5671578ed30b125257fd16d0f0b8ceaefd0acc3e44f082ffeecea9f1895499-d # internal
</code></pre>
<p>Go compiler发现了内容的变动，对gocmpp包的变动内容进行了重新compile。</p>
<h4>c) 缓存目录探索</h4>
<p>在增加cache机制时，go tools增加了GOCACHE变量，通过go env GOCACHE查看变量值：</p>
<pre><code>$go env GOCACHE
/Users/tony/Library/Caches/go-build
</code></pre>
<p>如果未重设环境变量GOCACHE，那么默认在Linux上，GOCACHE=”~/.cache/go-build”; 在Mac OS X上，GOCACHE=”/Users/UserName/Library/Caches/go-build”。在 OS X上，我们进入$GOCACHE目录，映入眼帘的是：</p>
<pre><code>➜  /Users/tony/Library/Caches/go-build $ls
00/        18/        30/        48/        60/        78/        90/        a7/        bf/        d7/        ef/
01/        19/        31/        49/        61/        79/        91/        a8/        c0/        d8/        f0/
02/        1a/        32/        4a/        62/        7a/        92/        a9/        c1/        d9/        f1/
03/        1b/        33/        4b/        63/        7b/        93/        aa/        c2/        da/        f2/
04/        1c/        34/        4c/        64/        7c/        94/        ab/        c3/        db/        f3/
05/        1d/        35/        4d/        65/        7d/        95/        ac/        c4/        dc/        f4/
06/        1e/        36/        4e/        66/        7e/        96/        ad/        c5/        dd/        f5/
07/        1f/        37/        4f/        67/        7f/        97/        ae/        c6/        de/        f6/
08/        20/        38/        50/        68/        80/        98/        af/        c7/        df/        f7/
09/        21/        39/        51/        69/        81/        99/        b0/        c8/        e0/        f8/
0a/        22/        3a/        52/        6a/        82/        9a/        b1/        c9/        e1/        f9/
0b/        23/        3b/        53/        6b/        83/        9b/        b2/        ca/        e2/        fa/
0c/        24/        3c/        54/        6c/        84/        9c/        b3/        cb/        e3/        fb/
0d/        25/        3d/        55/        6d/        85/        9d/        b4/        cc/        e4/        fc/
0e/        26/        3e/        56/        6e/        86/        9e/        b5/        cd/        e5/        fd/
0f/        27/        3f/        57/        6f/        87/        9f/        b6/        ce/        e6/        fe/
10/        28/        40/        58/        70/        88/        README        b7/        cf/        e7/        ff/
11/        29/        41/        59/        71/        89/        a0/        b8/        d0/        e8/        log.txt
12/        2a/        42/        5a/        72/        8a/        a1/        b9/        d1/        e9/        trim.txt
13/        2b/        43/        5b/        73/        8b/        a2/        ba/        d2/        ea/
14/        2c/        44/        5c/        74/        8c/        a3/        bb/        d3/        eb/
15/        2d/        45/        5d/        75/        8d/        a4/        bc/        d4/        ec/
16/        2e/        46/        5e/        76/        8e/        a5/        bd/        d5/        ed/
17/        2f/        47/        5f/        77/        8f/        a6/        be/        d6/        ee/

</code></pre>
<p>熟悉git原理的朋友一定觉得这个目录组织结构似曾相识！没错，在每个git项目的./git/object目录下，我们也能看到下面的结果：</p>
<pre><code>.git/objects git:(master) $ls
00/    0c/    18/    24/    30/    3c/    48/    54/    60/    6c/    78/    84/    90/    9c/    a8/    b4/    c0/    cc/    d8/    e4/    f0/    fc/
01/    0d/    19/    25/    31/    3d/    49/    55/    61/    6d/    79/    85/    91/    9d/    a9/    b5/    c1/    cd/    d9/    e5/    f1/    fd/
02/    0e/    1a/    26/    32/    3e/    4a/    56/    62/    6e/    7a/    86/    92/    9e/    aa/    b6/    c2/    ce/    da/    e6/    f2/    fe/
03/    0f/    1b/    27/    33/    3f/    4b/    57/    63/    6f/    7b/    87/    93/    9f/    ab/    b7/    c3/    cf/    db/    e7/    f3/    ff/
04/    10/    1c/    28/    34/    40/    4c/    58/    64/    70/    7c/    88/    94/    a0/    ac/    b8/    c4/    d0/    dc/    e8/    f4/    info/
05/    11/    1d/    29/    35/    41/    4d/    59/    65/    71/    7d/    89/    95/    a1/    ad/    b9/    c5/    d1/    dd/    e9/    f5/    pack/
06/    12/    1e/    2a/    36/    42/    4e/    5a/    66/    72/    7e/    8a/    96/    a2/    ae/    ba/    c6/    d2/    de/    ea/    f6/
07/    13/    1f/    2b/    37/    43/    4f/    5b/    67/    73/    7f/    8b/    97/    a3/    af/    bb/    c7/    d3/    df/    eb/    f7/
08/    14/    20/    2c/    38/    44/    50/    5c/    68/    74/    80/    8c/    98/    a4/    b0/    bc/    c8/    d4/    e0/    ec/    f8/
09/    15/    21/    2d/    39/    45/    51/    5d/    69/    75/    81/    8d/    99/    a5/    b1/    bd/    c9/    d5/    e1/    ed/    f9/
0a/    16/    22/    2e/    3a/    46/    52/    5e/    6a/    76/    82/    8e/    9a/    a6/    b2/    be/    ca/    d6/    e2/    ee/    fa/
0b/    17/    23/    2f/    3b/    47/    53/    5f/    6b/    77/    83/    8f/    9b/    a7/    b3/    bf/    cb/    d7/    e3/    ef/    fb/
</code></pre>
<p>这里猜测go 1.10使用的应该是与git一类内容摘要算法以及组织存储模式。在前面的build详细输出中，我们找到这一行：</p>
<pre><code>cp $WORK/b001/_pkg_.a /Users/tony/Library/Caches/go-build/7a/7a5671578ed30b125257fd16d0f0b8ceaefd0acc3e44f082ffeecea9f1895499-d # internal
</code></pre>
<p>这行命令是将gocmpp包复制到cache下，我们到cache的7a目录下一查究竟：</p>
<pre><code>➜  /Users/tony/Library/Caches/go-build/7a $tree
.
└── 7a5671578ed30b125257fd16d0f0b8ceaefd0acc3e44f082ffeecea9f1895499-d

0 directories, 1 file
</code></pre>
<p>我们用nm命令查看一下该文件：</p>
<pre><code>$go tool nm 7a5671578ed30b125257fd16d0f0b8ceaefd0acc3e44f082ffeecea9f1895499-d|more
1c319 T %22%22.(*Client).Connect
   2e279 T %22%22.(*Client).Connect.func1
   3fc22 R %22%22.(*Client).Connect.func1·f
   1c79f T %22%22.(*Client).Disconnect
   1c979 T %22%22.(*Client).RecvAndUnpackPkt
   1c807 T %22%22.(*Client).SendReqPkt
   1c8e2 T %22%22.(*Client).SendRspPkt
   1e417 T %22%22.(*Cmpp2ConnRspPkt).Pack
... ...
</code></pre>
<p>这个文件的确就是gocmpp.a文件。通过比对该文件size与go install后的文件size也可以证实这一点：</p>
<pre><code>➜  /Users/tony/Library/Caches/go-build/7a $
-rw-r--r--    1 tony  staff  445856 Feb 15 22:34 7a5671578ed30b125257fd16d0f0b8ceaefd0acc3e44f082ffeecea9f1895499-d

vs.

➜  $GOPATH/pkg/darwin_amd64/github.com/bigwhite $ll
-rw-r--r--  1 tony  staff  445856 Feb 15 23:27 gocmpp.a
</code></pre>
<p>也就是说go compiler将编译后的package的.a文件求取摘要值后，将.a文件存储在$GOCACHE下的某个目录中，这个目录名即为摘要值的前两位（比如”7a”），.a文件名字被换成其摘要值，以便后续查找并做比对。</p>
<p>cache目录下还有一个重要文件：log.txt，这个文件是用来记录缓存管理日志的，其内容格式如下：</p>
<pre><code>//log.txt
... ...

1518705271 get 7533a063cd8c37888b19674bf4a4bb7e25fa422041082566530d58538c031516
1518705271 miss b6b9f996fbd14e4fd43f72dc4f9082946cddd0d61d6c6143c88502c8a4001666
1518705271 put b6b9f996fbd14e4fd43f72dc4f9082946cddd0d61d6c6143c88502c8a4001666 7a5671578ed30b125257fd16d0f0b8ceaefd0acc3e44f082ffeecea9f1895499 445856
1518705271 put f5a641ca081a0d2d794b0b54aa9f89014dbb6ff8d14d26543846e1676eca4c21 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0
1518708456 get 899589360d856265a84825dbeb8d283ca84e12f154eefc12ba84870af13e1f63
1518708456 get 8a7fcd97a5f36bd00ef084856c63e4e2facedce33d19a5b557cc67f219787661
</code></pre>
<p>该日志文件更多的用途是帮助Russ Cox对其开发的cache进行调试和问题诊断的。当然，如果您对于cache的机制原理也很精通，那么也可以让log.txt帮你诊断涉及cache的问题。</p>
<h4>d) go test with cache</h4>
<p>go 1.10版的go test也会维护一个cache，这个cache缓存了go test执行的测试结果。同时在go 1.10中，go test被分为两种执行模式：local directory mode和package list mode，在不同模式下，cache机制的介入是不同的。</p>
<p>local directory mode，即go test以整个当前目录作为隐式参数的执行模式，比如在某个目录下执行”go test”，go test后面不带任何显式的package列表参数（当然可以带着其他命令行flag参数，如-v）。在这种模式下，cache机制不会介入，go test的执行过程与go 1.10版本之前没有两样。还是以gocmpp这个项目为例，我们以local directory mode执行go test：</p>
<pre><code>➜  $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ✗ $go test
PASS
ok      github.com/bigwhite/gocmpp    0.011s
</code></pre>
<p>如果缓存机制介入，输出的test结果中会出现cached字样，显然上面的go test执行过程并没有使用test cache。</p>
<p>package list mode，即go test后面显式传入了package列表，比如：go test math、go test .、go test ./&#8230;等，在这种模式下，test cache机制会介入。我们连续两次在gocmpp目录下执行go test .：</p>
<pre><code>➜  $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ✗ $go test .
ok      github.com/bigwhite/gocmpp    0.011s
➜  $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ✗ $go test .
ok      github.com/bigwhite/gocmpp    (cached)
</code></pre>
<p>如果你此时想进一步看看go test执行的详细输出，你可以会执行go test -v .：</p>
<pre><code>➜  $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ✗ $go test -v .
=== RUN   TestTypeString
--- PASS: TestTypeString (0.00s)
=== RUN   TestCommandIdString
--- PASS: TestCommandIdString (0.00s)
=== RUN   TestOpError
--- PASS: TestOpError (0.00s)
... ...
=== RUN   TestCmppTerminateRspPktPack
--- PASS: TestCmppTerminateRspPktPack (0.00s)
=== RUN   TestCmppTerminateRspUnpack
--- PASS: TestCmppTerminateRspUnpack (0.00s)
PASS
ok      github.com/bigwhite/gocmpp    0.017s
</code></pre>
<p>你会发现，这次go test并没有使用cache。如果你再执行一次go test -v .：</p>
<pre><code>➜  $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ✗ $go test -v .
=== RUN   TestTypeString
--- PASS: TestTypeString (0.00s)
=== RUN   TestCommandIdString
--- PASS: TestCommandIdString (0.00s)
=== RUN   TestOpError
--- PASS: TestOpError (0.00s)
... ...
=== RUN   TestCmppTerminateRspPktPack
--- PASS: TestCmppTerminateRspPktPack (0.00s)
=== RUN   TestCmppTerminateRspUnpack
--- PASS: TestCmppTerminateRspUnpack (0.00s)
PASS
ok      github.com/bigwhite/gocmpp    (cached)

</code></pre>
<p>test cache又起了作用。似乎cache对于go test .和go test -v .是独立的。没错，release note中给出的go test cache的介入条件如下：</p>
<ul>
<li>本次测试的执行程序以及命令行（及参数）与之前的一次test运行匹配；（这就能解释为何go test -v .没有使用go test .执行的cache了）；</li>
<li>上次测试执行时的文件和环境变量在本次没有发生变化；</li>
<li>测试结果是成功的；</li>
<li>以package list node运行测试；</li>
<li>go test的命令行参数使用”-cpu, -list, -parallel, -run, -short和 -v”的一个子集时</li>
</ul>
<p>就像前面我们看到的，cache介入的go test结果不会显示test消耗的时间，而是以(cached)字样替代。</p>
<p>绝大多数Gopher都是喜欢test with cache的，但总有一些情况，cache是不受欢迎的。其实前面的条件已经明确告知gopher们什么条件下test cache是可以不介入的。<strong>一个惯用的关闭test cache的方法</strong>是使用-count=1：</p>
<pre><code>➜  $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ✗ $go test -count=1  -v .
=== RUN   TestTypeString
--- PASS: TestTypeString (0.00s)
=== RUN   TestCommandIdString
--- PASS: TestCommandIdString (0.00s)
=== RUN   TestOpError
--- PASS: TestOpError (0.00s)
... ...
=== RUN   TestCmppTerminateRspPktPack
--- PASS: TestCmppTerminateRspPktPack (0.00s)
=== RUN   TestCmppTerminateRspUnpack
--- PASS: TestCmppTerminateRspUnpack (0.00s)
PASS
ok      github.com/bigwhite/gocmpp    0.012s

</code></pre>
<p>go 1.10中的go test与之前版本还有一个不同，那就是go test在真正执行test前会自动对被测试的包执行go vet，但这个vet只会识别那些最为明显的问题。并且一旦发现问题，go test将会视这些问题与build error同级别，阻断test的执行，并让其出现在test failure中。当然gopher可以通过go test -vet=off关闭这个前置于测试的vet检查。</p>
<h3>4. pprof</h3>
<p>go tool pprof做了一个较大的改变：增加了Web UI，以后可以和go trace一起通过图形化的方法对Go程序进行调优了。可视化的pprof使用起来十分简单，我们以gocmpp为例，试用一下go 1.10的pprof，首先我们生成cpu profile文件：</p>
<pre><code>➜  $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ✗ $go test -run=^$ -bench=. -cpuprofile=profile.out
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/gocmpp
BenchmarkRecvAndUnpackPkt-4                1000000          1534 ns/op
BenchmarkCmppConnReqPktPack-4              1000000          1398 ns/op
BenchmarkCmppConnReqPktUnpack-4            3000000           450 ns/op
BenchmarkCmpp2DeliverReqPktPack-4          1000000          1156 ns/op
BenchmarkCmpp2DeliverReqPktUnpack-4        3000000           567 ns/op
BenchmarkCmpp3DeliverReqPktPack-4          1000000          1173 ns/op
BenchmarkCmpp3DeliverReqPktUnpack-4        3000000           465 ns/op
BenchmarkCmpp2FwdReqPktPack-4              1000000          2079 ns/op
BenchmarkCmpp2FwdReqPktUnpack-4            1000000          1276 ns/op
BenchmarkCmpp3FwdReqPktPack-4              1000000          2507 ns/op
BenchmarkCmpp3FwdReqPktUnpack-4            1000000          1286 ns/op
BenchmarkCmpp2SubmitReqPktPack-4           1000000          1845 ns/op
BenchmarkCmpp2SubmitReqPktUnpack-4         1000000          1251 ns/op
BenchmarkCmpp3SubmitReqPktPack-4           1000000          1863 ns/op
BenchmarkCmpp3SubmitReqPktUnpack-4         2000000           656 ns/op
PASS
ok      github.com/bigwhite/gocmpp    26.621s
</code></pre>
<p>启动pprof web ui：</p>
<pre><code>$go tool pprof -http=:8080 profile.out
</code></pre>
<p>pprof会自动打开默认浏览器，进入下面页面：</p>
<p><img src="http://tonybai.com/wp-content/uploads/go-1.10-pprof-web-ui.png" alt="img{512x368}" /></p>
<p>在view菜单中，我们可以看到”top”、”graph”、”peek”、”source”和”disassemble”几个选项，这些选项可以帮助你在各种视图间切换，默认初始为graph view。不过目前view菜单中并没有”Flame Graph(火焰图)”选项，要想使用Flame Graph，我们需要使用原生的pprof工具，该工具可通过go get -u github.com/google/pprof获取，install后原生pprof将出现在$GOROOT/bin下面。</p>
<p>使用原生pprof启动Web UI：</p>
<pre><code>$pprof -http=:8080 profile.out
</code></pre>
<p>原生pprof同样会自动打开浏览器，进入下面页面：</p>
<p><img src="http://tonybai.com/wp-content/uploads/go-1.10-native-pprof-web-ui.png" alt="img{512x368}" /></p>
<p>原生的pprof的web ui看起来比go 1.10 tool中的pprof更为精致，且最大的不同在于VIEW菜单下出现了”Flame Graph”菜单项！我们点击该菜单项，一幅Flame Graph便呈现在眼前：</p>
<p><img src="http://tonybai.com/wp-content/uploads/go-1.10-native-pprof-flame-graph.png" alt="img{512x368}" /></p>
<p>关于如何做火焰图分析不是这里的主要任务，请各位Gopher自行脑补。更多关于Go性能调优问题，可以参考<a href="https://tip.golang.org/doc/diagnostics.html">Go官方提供的诊断手册</a>。</p>
<h2>四、标准库</h2>
<p>和之前的每次Go版本发布一样，标准库的改变是多且细碎的，这里不能一一举例说明。并且很多涉“专业领域”的包，比如加解密，需要一定专业深度，因此这里仅列举几个“通用”的变化^0^。</p>
<h3>1、strings.Builder</h3>
<p>strings包增加一个新的类型：Builder，用于在“拼字符串”场景中替代bytes.Buffer，由于使用了一些unsafe包的黑科技，在用户调用Builder.String()返回最终拼成的字符串时，避免了一些重复的、不必要的内存copy，提升了处理性能，优化了内存分配。我们用一个demo来看看这种场景下Builder的优势：</p>
<pre><code>//go1.10-examples/stdlib/stringsbuilder/builer.go
package builder

import (
    "bytes"
    "strings"
)

type BuilderByBytesBuffer struct {
    b bytes.Buffer
}
func (b *BuilderByBytesBuffer) WriteString(s string) error {
    _, err := b.b.WriteString(s)
    return err
}
func (b *BuilderByBytesBuffer) String() string{
    return b.b.String()
}

type BuilderByStringsBuilder struct {
    b strings.Builder
}

func (b *BuilderByStringsBuilder) WriteString(s string) error {
    _, err := b.b.WriteString(s)
    return err
}
func (b *BuilderByStringsBuilder) String() string{
    return b.b.String()
}
</code></pre>
<p>针对上面代码中的BuilderByBytesBuffer和BuilderByStringsBuilder进行Benchmark的Test源文件如下：</p>
<pre><code>//go1.10-examples/stdlib/stringsbuilder/builer_test.go
package builder

import "testing"

func BenchmarkBuildStringWithBytesBuffer(b *testing.B) {
    var builder BuilderByBytesBuffer

    for i := 0; i &lt; b.N; i++ {
        builder.WriteString("Hello, ")
        builder.WriteString("Go")
        builder.WriteString("-1.10")
        _ = builder.String()
    }

}
func BenchmarkBuildStringWithStringsBuilder(b *testing.B) {

    var builder BuilderByStringsBuilder

    for i := 0; i &lt; b.N; i++ {
        builder.WriteString("Hello, ")
        builder.WriteString("Go")
        builder.WriteString("-1.10")
        _ = builder.String()
    }
}
</code></pre>
<p>执行该Benchmark，查看结果：</p>
<pre><code>$go test -bench . -benchmem
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/experiments/go1.10-examples/stdlib/stringsbuilder
BenchmarkBuildStringWithBytesBuffer-4            100000        108471 ns/op      704073 B/op           1 allocs/op
BenchmarkBuildStringWithStringsBuilder-4       20000000           122 ns/op          80 B/op           0 allocs/op
PASS
ok      github.com/bigwhite/experiments/go1.10-examples/stdlib/stringsbuilder    13.616s
</code></pre>
<p>可以看到StringsBuilder在处理速度和分配优化上都全面强于bytes.Buffer，真实的差距就在Builder.String这个方法上。</p>
<h3>2、bytes包</h3>
<p>bytes包的几个方法Fields, FieldsFunc, Split和SplitAfter在底层实现上有变化，使得外部展现的行为有所变化，我们通过一个例子直观的感受一下：</p>
<pre><code>// go1.10-examples/stdlib/bytessplit/main.go
package main

import (
    "bytes"
    "fmt"
)

// 来自github.com/campoy/gotalks/blob/master/go1.10/bytes/fields.go
func desc(b []byte) string {
    return fmt.Sprintf("len: %2d | cap: %2d | %q\n", len(b), cap(b), b)
}

func main() {
    text := []byte("Hello, Go1.10 is coming!")
    fmt.Printf("text:  %s", desc(text))

    subslices := bytes.Split(text, []byte(" "))
    fmt.Printf("subslice 0:  %s", desc(subslices[0]))
    fmt.Printf("subslice 1:  %s", desc(subslices[1]))
    fmt.Printf("subslice 2:  %s", desc(subslices[2]))
    fmt.Printf("subslice 3:  %s", desc(subslices[3]))
}
</code></pre>
<p>我们先用Go 1.9.2编译运行一下该demo:</p>
<pre><code>$go run main.go
text:  len: 24 | cap: 32 | "Hello, Go1.10 is coming!"
subslice 0:  len:  6 | cap: 32 | "Hello,"
subslice 1:  len:  6 | cap: 25 | "Go1.10"
subslice 2:  len:  2 | cap: 18 | "is"
subslice 3:  len:  7 | cap: 15 | "coming!"
</code></pre>
<p>我们再用go 1.10rc2运行一下该demo：</p>
<pre><code>$go run main.go
text:  len: 24 | cap: 32 | "Hello, Go1.10 is coming!"
subslice 0:  len:  6 | cap:  6 | "Hello,"
subslice 1:  len:  6 | cap:  6 | "Go1.10"
subslice 2:  len:  2 | cap:  2 | "is"
subslice 3:  len:  7 | cap: 15 | "coming!"
</code></pre>
<p>对比两次输出结果中cap那一列，你会发现go 1.10输出的结果中的<strong>每个subslice(除了最后一个)的len与cap值都是相等的</strong>，而不是将原slice剩下所有cap都作为subslice的cap。这个行为的改变是出于安全的考虑，防止共享一个underlying slice的各个subslice的修改对相邻的subslice造成影响，因此限制它们的capacity。</p>
<p>在Fields, FieldsFunc, Split和SplitAfter这几个方法的具体实现上，Go 1.10使用了我们平时并不经常使用的”<a href="https://golang.org/ref/spec#Slice_expressions">Full slice expression</a>“，即：a[low, high, max]来指定subslice的cap。</p>
<h2>五、性能</h2>
<p>对于静态编译类型语言Go来说，性能也一直是其重点关注的设计目标，这两年来发布的Go版本，几乎每个都给Gopher们带来惊喜。谈到Go性能，Gopher们一般关心的有如下这么几个方面：</p>
<h3>1、编译性能</h3>
<p>Go 1.10的编译性能正如我们前面所说的那样，最大的改变在于cache机制的实现。事实证明cache机制的使用在日常开发过程中，会很大程度上提升你的工作效率，越是规模较大的项目越是如此。</p>
<h3>2、目标代码的性能</h3>
<p>这些年Go team在不断优化编译器生成的目标代码的性能，比如在<a href="http://tonybai.com/2016/06/21/some-changes-in-go-1-7/">Go 1.7版本</a>中引入ssa后端。Go 1.10延续着对目标代码生成的进一步优化，虽说动作远不如引入ssa这么大。</p>
<h3>3、GC性能</h3>
<p>GC的性能一直是广大Gopher密切关注的事情，Go 1.10在减少内存分配延迟以及GC运行时的负担两个方面做了许多工作，但从整体上来看，Go 1.10并没有引入传说中的<a href="https://groups.google.com/forum/#!topic/golang-dev/WcZaqTE51ZU">TOC(Transaction Oritented Collector)</a>，因此宏观上来看，GC变化不是很大。Twitter上的GC性能测试“专家”<a href="https://twitter.com/brianhatfield">Brian Hatfield</a>在对Go 1.10rc1的<a href="http://tonybai.com/2017/07/04/setup-go-runtime-metrics-for-yourself/">GC测试</a>后，也表示与Go 1.9相比，变化不是很显著。</p>
<h2>六、小结</h2>
<p>Go 1.10版本又是一个Go team和Gopher社区共同努力的结果，让全世界Gopher都对Go保持着极大的热情和期望。当然Go 1.10中的变化还有许多许多，诸如：</p>
<ul>
<li>对Unicode规范的支持升级到<a href="http://www.unicode.org/versions/Unicode10.0.0/">10.0</a>；</li>
<li>在不同的平台上，Assembler支持更多高性能的指令；</li>
<li>plugin支持darwin/amd64等；</li>
<li>gofmt、go doc在输出格式上进一步优化和提升gopher开发者体验；</li>
<li>cgo支持直接传递go string到C代码中;<br />
&#8230; &#8230;</li>
</ul>
<p>很多很多！这里限于篇幅原因，不能一一详解了。通读一遍<a href="https://golang.org/doc/go1.10">Go 1.10 Release Note</a>是每个Gopher都应该做的。</p>
<p>以上验证在mac OS X, go 1.10rc2上测试，demo源码可以在<a href="https://github.com/bigwhite/experiments/tree/master/go1.10-examples">这里下载</a>。</p>
<h2>七、参考资料</h2>
<ul>
<li><a href="https://golang.org/doc/go1.10">Go 1.10 Release Notes</a></li>
<li><a href="https://speakerdeck.com/campoy/the-state-of-go-1-dot-10">The State of Go by campoy</a></li>
<li><a href="https://blog.gopheracademy.com/advent-2017/go-1.10/">Go 1.10</a></li>
<li><a href="https://rakyll.org/pprof-ui/">pprof user interface</a></li>
<li><a href="https://groups.google.com/forum/#!topic/golang-dev/EZtZsJG35ZM">cmd/go content-based staleness submitted</a></li>
<li><a href="https://groups.google.com/forum/#!topic/golang-dev/qfa3mHN4ZPA">Go 1.10 cmd/go: build cache, test cache, go install, go vet, test vet</a></li>
</ul>
<hr />
<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>微博：http://weibo.com/bigwhite20xx<br />
微信公众号：iamtonybai<br />
博客：tonybai.com<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="http://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作</p>
<p style='text-align:left'>&copy; 2018, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2018/02/17/some-changes-in-go-1-10/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>Go 1.8中值得关注的几个变化</title>
		<link>https://tonybai.com/2017/02/03/some-changes-in-go-1-8/</link>
		<comments>https://tonybai.com/2017/02/03/some-changes-in-go-1-8/#comments</comments>
		<pubDate>Fri, 03 Feb 2017 03:07:40 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AES]]></category>
		<category><![CDATA[buildmode]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Cgo]]></category>
		<category><![CDATA[ChaCha20]]></category>
		<category><![CDATA[cloudflare]]></category>
		<category><![CDATA[Compiler]]></category>
		<category><![CDATA[Context]]></category>
		<category><![CDATA[database/sql]]></category>
		<category><![CDATA[defer]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Go1]]></category>
		<category><![CDATA[go1.4]]></category>
		<category><![CDATA[Go1.7]]></category>
		<category><![CDATA[go1.8]]></category>
		<category><![CDATA[Go2]]></category>
		<category><![CDATA[gobug]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GOPATH]]></category>
		<category><![CDATA[GOROOT]]></category>
		<category><![CDATA[gotoolchain]]></category>
		<category><![CDATA[govendor]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[HTTP/2-Push]]></category>
		<category><![CDATA[HTTP2]]></category>
		<category><![CDATA[https]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[ldd]]></category>
		<category><![CDATA[Mutex]]></category>
		<category><![CDATA[nm]]></category>
		<category><![CDATA[Performance]]></category>
		<category><![CDATA[plugins]]></category>
		<category><![CDATA[Poly1305]]></category>
		<category><![CDATA[pprof]]></category>
		<category><![CDATA[Slice]]></category>
		<category><![CDATA[sort]]></category>
		<category><![CDATA[SSA]]></category>
		<category><![CDATA[struct]]></category>
		<category><![CDATA[TIOBE]]></category>
		<category><![CDATA[TLS]]></category>
		<category><![CDATA[X25519]]></category>
		<category><![CDATA[XML]]></category>
		<category><![CDATA[标准库]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2143</guid>
		<description><![CDATA[在已经过去的2016年，Go语言继在2009年之后再次成为编程语言界的明星- 问鼎TIOBE 2016年度语言。这与Go team、Go community和全世界的Gophers的努力是分不开的。按计划在这个2月份，Go team将正式发布Go 1.8版本(截至目前，Go的最新版本是Go 1.8rc3)。在这里我们一起来看一下在Go 1.8版本中都有哪些值得Gopher们关注的变化。 一、语言（Language） Go 1.8版本依旧坚守Go Team之前的承诺，即Go1兼容性：使用Go 1.7及以前版本编写的Go代码，理论上都可以通过Go 1.8进行编译并运行。因此在臆想中的Go 2.0变成现实之前，每个Go Release版本在语言这方面的“改变”都会是十分微小的。 1、仅tags不同的两个struct可以相互做显式类型转换 在Go 1.8版本以前，两个struct即便字段个数相同且每个字段类型均一样，但如果某个字段的tag描述不一样，这两个struct相互间也不能做显式类型转换，比如： //go18-examples/language/structtag.go package main import "fmt" type XmlEventRegRequest struct { AppID string `xml:"appid"` NeedReply int `xml:"Reply,omitempty"` } type JsonEventRegRequest struct { AppID string `json:"appid"` NeedReply int `json:"reply,omitempty"` } func convert(in *XmlEventRegRequest) *JsonEventRegRequest { out := &#38;JsonEventRegRequest{} [...]]]></description>
			<content:encoded><![CDATA[<p>在已经过去的<a href="http://tonybai.com/2017/01/03/2016-summary/">2016年</a>，<a href="http://tonybai.com/tag/golang">Go语言</a>继在2009年之后再次成为编程语言界的明星- 问鼎<a href="http://www.tiobe.com/tiobe-index/">TIOBE</a> 2016年度语言。这与Go team、Go community和全世界的Gophers的努力是分不开的。按计划在这个2月份，Go team将正式发布Go 1.8版本(截至目前，Go的最新版本是<a href="https://github.com/golang/go/releases/tag/go1.8rc3">Go 1.8rc3</a>)。在这里我们一起来看一下在Go 1.8版本中都有哪些值得Gopher们关注的变化。</p>
<h3>一、语言（Language）</h3>
<p>Go 1.8版本依旧坚守Go Team之前的承诺，即<a href="https://golang.org/doc/go1compat.html">Go1兼容性</a>：使用Go 1.7及以前版本编写的Go代码，理论上都可以通过Go 1.8进行编译并运行。因此在臆想中的<a href="https://dave.cheney.net/2016/10/25/introducing-go-2-0">Go 2.0</a>变成现实之前，每个Go Release版本在语言这方面的“改变”都会是十分微小的。</p>
<h4>1、仅tags不同的两个struct可以相互做显式类型转换</h4>
<p>在Go 1.8版本以前，两个struct即便字段个数相同且每个字段类型均一样，但如果某个字段的tag描述不一样，这两个struct相互间也不能做显式类型转换，比如：</p>
<pre><code>//go18-examples/language/structtag.go
package main

import "fmt"

type XmlEventRegRequest struct {
    AppID     string `xml:"appid"`
    NeedReply int    `xml:"Reply,omitempty"`
}

type JsonEventRegRequest struct {
    AppID     string `json:"appid"`
    NeedReply int    `json:"reply,omitempty"`
}

func convert(in *XmlEventRegRequest) *JsonEventRegRequest {
    out := &amp;JsonEventRegRequest{}
    *out = (JsonEventRegRequest)(*in)
    return out
}

func main() {
    in := XmlEventRegRequest{
        AppID:     "wx12345678",
        NeedReply: 1,
    }
    out := convert(&amp;in)
    fmt.Println(out)
}

</code></pre>
<p>采用Go 1.7.4版本go compiler进行编译，我们会得到如下错误输出：</p>
<pre><code>$go build structtag.go
# command-line-arguments
./structtag.go:17: cannot convert *in (type XmlEventRegRequest) to type JsonEventRegRequest
</code></pre>
<p>但在Go 1.8中，gc将忽略tag值的不同，使得显式类型转换成为可能：</p>
<pre><code>$go run structtag.go
&amp;{wx12345678 1}
</code></pre>
<p>改变虽小，但带来的便利却不小，否则针对上面代码中的convert，我们只能做逐一字段赋值了。</p>
<h4>2、浮点常量的指数部分至少支持16bits长</h4>
<p>在Go 1.8版本之前的<a href="https://golang.org/ref/spec">The Go Programming Language Specificaton</a>中，关于浮点数常量的指数部分的描述如下：</p>
<pre><code>Represent floating-point constants, including the parts of a complex constant, with a mantissa of at least 256 bits and a signed exponent of at least 32 bits.
</code></pre>
<p>在Go 1.8版本中，文档中对于浮点数常量指数部分的长度的实现的条件放宽了，由支持最少32bit，放宽到最少支持16bits：</p>
<pre><code>Represent floating-point constants, including the parts of a complex constant, with a mantissa of at least 256 bits and a signed binary exponent of at least 16 bits.
</code></pre>
<p>但Go 1.8版本go compiler实际仍然支持至少32bits的指数部分长度，因此这个改变对现存的所有Go源码不会造成影响。</p>
<h3>二、标准库（Standard Library）</h3>
<p>Go号称是一门”Batteries Included”编程语言。“Batteries Included”指的就是Go语言强大的标准库。使用Go标准库，你可以完成绝大部分你想要的功能，而无需再使用第三方库。Go语言的每次版本更新，都会在标准库环节增加强大的功能、提升性能或是提高使用上的便利性。每次版本更新，标准库也是改动最大的部分。这次也不例外，我们逐一来看。</p>
<h4>1、便于slice sort的sort.Slice函数</h4>
<p>在Go 1.8之前我们要对一个slice进行sort，需要定义出实现了下面接口的slice type：</p>
<pre><code>//$GOROOT/src/sort.go
... ...
type Interface interface {
    // Len is the number of elements in the collection.
    Len() int
    // Less reports whether the element with
    // index i should sort before the element with index j.
    Less(i, j int) bool
    // Swap swaps the elements with indexes i and j.
    Swap(i, j int)
}

</code></pre>
<p>标准库定义了一些应对常见类型slice的sort类型以及对应的函数：</p>
<pre><code>StringSlice -&gt; sort.Strings
IntSlice -&gt; sort.Ints
Float64Slice -&gt; sort.Float64s
</code></pre>
<p>但即便如此，对于用户定义的struct或其他自定义类型的slice进行排序仍需定义一个新type，比如下面这个例子中的TiboeIndexByRank：</p>
<pre><code>//go18-examples/stdlib/sort/sortslice-before-go18.go
package main

import (
    "fmt"
    "sort"
)

type Lang struct {
    Name string
    Rank int
}

type TiboeIndexByRank []Lang

func (l TiboeIndexByRank) Len() int           { return len(l) }
func (l TiboeIndexByRank) Less(i, j int) bool { return l[i].Rank &lt; l[j].Rank }
func (l TiboeIndexByRank) Swap(i, j int)      { l[i], l[j] = l[j], l[i] }

func main() {
    langs := []Lang{
        {"rust", 2},
        {"go", 1},
        {"swift", 3},
    }
    sort.Sort(TiboeIndexByRank(langs))
    fmt.Printf("%v\n", langs)
}

$go run sortslice-before-go18.go
[{go 1} {rust 2} {swift 3}]

</code></pre>
<p>从上面的例子可以看到，我们要对[]Lang这个slice进行排序，我们就需要为之定义一个专门用于排序的类型：这里是TiboeIndexByRank，并让其实现sort.Interface接口。使用过sort包的gophers们可能都意识到了，我们在为新的slice type实现sort.Interface接口时，那三个方法的Body几乎每次都是一样的。为了使得gopher们在排序slice时编码更为简化和便捷，减少copy&amp;paste，Go 1.8为slice type新增了三个函数：Slice、SliceStable和SliceIsSorted。我们重新用Go 1.8的sort.Slice函数实现上面例子中的排序需求，代码如下：</p>
<pre><code>//go18-examples/stdlib/sort/sortslice-in-go18.go
package main

import (
    "fmt"
    "sort"
)

type Lang struct {
    Name string
    Rank int
}

func main() {
    langs := []Lang{
        {"rust", 2},
        {"go", 1},
        {"swift", 3},
    }
    sort.Slice(langs, func(i, j int) bool { return langs[i].Rank &lt; langs[j].Rank })
    fmt.Printf("%v\n", langs)
}

$go run sortslice-in-go18.go
[{go 1} {rust 2} {swift 3}]
</code></pre>
<p>实现sort，需要三要素：Len、Swap和Less。在1.8之前，我们通过实现sort.Interface实现了这三个要素；而在1.8版本里，Slice函数通过reflect获取到swap和length，通过结合闭包实现的less参数让Less要素也具备了。我们从下面sort.Slice的源码可以看出这一点：</p>
<pre><code>// $GOROOT/src/sort/sort.go
... ...
func Slice(slice interface{}, less func(i, j int) bool) {
    rv := reflect.ValueOf(slice)
    swap := reflect.Swapper(slice)
    length := rv.Len()
    quickSort_func(lessSwap{less, swap}, 0, length, maxDepth(length))
}

</code></pre>
<h4>2、支持HTTP/2 Push</h4>
<p>继在<a href="http://tonybai.com/2016/02/21/some-changes-in-go-1-6/">Go 1.6版本</a>全面支持<a href="https://en.wikipedia.org/wiki/HTTP/2">HTTP/2</a>之后，Go 1.8又新增了对<a href="https://en.wikipedia.org/wiki/HTTP/2_Server_Push">HTTP/2 Push</a>的支持。<a href="https://en.wikipedia.org/wiki/HTTP/2">HTTP/2</a>是在<a href="http://tonybai.com/2015/04/30/go-and-https/">HTTPS</a>的基础上的下一代HTTP协议，虽然当前HTTPS的应用尚不是十分广泛。而<a href="https://tools.ietf.org/html/rfc7540#section-8.2">HTTP/2 Push</a>是HTTP/2的一个重要特性，无疑其提出的初衷也仍然是为了改善网络传输性能，提高Web服务的用户侧体验。这里我们可以借用知名网络提供商<a href="https://blog.cloudflare.com/announcing-support-for-http-2-server-push-2/">Cloudflare blog</a>上的一幅示意图来诠释HTTP/2 Push究竟是什么：</p>
<p><img src="http://tonybai.com/wp-content/uploads/cloudflare-http2-server-push.png" alt="img{512x368}" /></p>
<p>从上图中，我们可以看到：当Browser向Server发起Get page.html请求后，在同一条TCP Connection上，Server主动将style.css和image.png两个资源文件推送(Push)给了Browser。这是由于Server端启用了HTTP/2 Push机制，并预测判断Browser很可能会在接下来发起Get style.css和image.png两个资源的请求。这是一种典型的：“你可能会需要，但即使你不要，我也推给你”的处世哲学^0^。这种机制虽然在一定程度上能改善网络传输性能（减少Client发起Get的次数），但也可能造成带宽的浪费，因为这些主动推送给Browser的资源很可能是Browser所不需要的或是已经在Browser cache中存在的资源。</p>
<p>接下来，我们来看看Go 1.8是如何在net/http包中提供对HTTP/2 Push的支持的。由于HTTP/2是基于HTTPS的，因此我们先使用generate_cert.go生成程序所需的私钥和证书：</p>
<pre><code>// 在go18-examples/stdlib/http2-push目录下，执行：

$go run $GOROOT/src/crypto/tls/generate_cert.go --host 127.0.0.1
2017/01/27 10:58:01 written cert.pem
2017/01/27 10:58:01 written key.pem
</code></pre>
<p>支持HTTP/2 Push的server端代码如下：</p>
<pre><code>// go18-examples/stdlib/http2-push/server.go

package main

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

const mainJS = `document.write('Hello World!');`

func main() {
    http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        if r.URL.Path != "/" {
            http.NotFound(w, r)
            return
        }
        pusher, ok := w.(http.Pusher)
        if ok {
            // If it's a HTTP/2 Server.
            // Push is supported. Try pushing rather than waiting for the browser.
            if err := pusher.Push("/static/img/gopherizeme.png", nil); err != nil {
                log.Printf("Failed to push: %v", err)
            }
        }
        fmt.Fprintf(w, `&lt;html&gt;
&lt;head&gt;
&lt;title&gt;Hello Go 1.8&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;img src="/static/img/gopherizeme.png"&gt;&lt;/img&gt;
&lt;/body&gt;
&lt;/html&gt;
`)
    })
    log.Fatal(http.ListenAndServeTLS(":8080", "./cert.pem", "./key.pem", nil))
}
</code></pre>
<p>运行这段代码，打开Google Chrome浏览器，输入：https://127.0.0.1:8080，忽略浏览器的访问非受信网站的警告，继续浏览你就能看到下面的页面（这里打开了Chrome的“检查”功能）：</p>
<p><img src="http://tonybai.com/wp-content/uploads/http2-push-demo.png" alt="img{512x368}" /></p>
<p>从示例图中的“检查”窗口，我们可以看到<a href="https://gopherize.me/">gopherizeme.png</a>这个image资源就是Server主动推送给客户端的，这样浏览器在Get /后无需再发起一次Get /static/img/gopherizeme.png的请求了。</p>
<p>而这一切的背后，其实是HTTP/2的ResponseWriter实现了Go 1.8新增的http.Pusher interface：</p>
<pre><code>// $GOROOT/src/net/http/http.go

// Pusher is the interface implemented by ResponseWriters that support
// HTTP/2 server push. For more background, see
// https://tools.ietf.org/html/rfc7540#section-8.2.
type Pusher interface {
    ... ...
    Push(target string, opts *PushOptions) error
}
</code></pre>
<h4>3、支持HTTP Server优雅退出</h4>
<p>Go 1.8中增加对HTTP Server优雅退出(gracefullly exit)的支持，对应的新增方法为：</p>
<pre><code>func (srv *Server) Shutdown(ctx context.Context) error
</code></pre>
<p>和server.Close在调用时瞬间关闭所有active的Listeners和所有状态为New、Active或idle的connections不同，server.Shutdown首先关闭所有active Listeners和所有处于idle状态的Connections，然后无限等待那些处于active状态的connection变为idle状态后，关闭它们并server退出。如果有一个connection依然处于active状态，那么server将一直block在那里。因此Shutdown接受一个context参数，调用者可以通过context传入一个Shutdown等待的超时时间。一旦超时，Shutdown将直接返回。对于仍然处理active状态的Connection，就任其自生自灭（通常是进程退出后，自动关闭）。通过Shutdown的源码我们也可以看出大致的原理：</p>
<pre><code>// $GOROOT/src/net/http/server.go
... ...
func (srv *Server) Shutdown(ctx context.Context) error {
    atomic.AddInt32(&amp;srv.inShutdown, 1)
    defer atomic.AddInt32(&amp;srv.inShutdown, -1)

    srv.mu.Lock()
    lnerr := srv.closeListenersLocked()
    srv.closeDoneChanLocked()
    srv.mu.Unlock()

    ticker := time.NewTicker(shutdownPollInterval)
    defer ticker.Stop()
    for {
        if srv.closeIdleConns() {
            return lnerr
        }
        select {
        case &lt;-ctx.Done():
            return ctx.Err()
        case &lt;-ticker.C:
        }
    }
}

</code></pre>
<p>我们来编写一个例子：</p>
<pre><code>// go18-examples/stdlib/graceful/server.go

import (
    "context"
    "io"
    "log"
    "net/http"
    "os"
    "os/signal"
    "time"
)

func main() {
    exit := make(chan os.Signal)
    signal.Notify(exit, os.Interrupt)

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        log.Println("Handle a new request:", *r)
        time.Sleep(10 * time.Second)
        log.Println("Handle the request ok!")
        io.WriteString(w, "Finished!")
    })

    srv := &amp;http.Server{
        Addr:    ":8080",
        Handler: http.DefaultServeMux,
    }

    go func() {
        if err := srv.ListenAndServe(); err != nil {
            log.Printf("listen: %s\n", err)
        }
    }()

    &lt;-exit // wait for SIGINT
    log.Println("Shutting down server...")

    // Wait no longer than 30 seconds before halting
    ctx, _ := context.WithTimeout(context.Background(), 30*time.Second)
    err := srv.Shutdown(ctx)

    log.Println("Server gracefully stopped:", err)
}

</code></pre>
<p>在上述例子中，我们通过<a href="http://tonybai.com/2012/09/21/signal-handling-in-go/">设置Linux Signal的处理函数</a>来拦截Linux Interrupt信号并处理。我们通过context给Shutdown传入30s的超时参数，这样Shutdown在退出之前会给各个Active connections 30s的退出时间。下面分为几种情况run一下这个例子：</p>
<p>a)  当前无active connections</p>
<p>在这种情况下，我们run上述demo，ctrl + C后，上述demo直接退出：</p>
<pre><code>$go run server.go
^C2017/02/02 15:13:16 Shutting down server...
2017/02/02 15:13:16 Server gracefully stopped: &lt;nil&gt;
</code></pre>
<p>b) 当前有未处理完的active connections，ctx 超时</p>
<p>为了模拟这一情况，我们修改一下参数。让每个request handler的sleep时间为30s，而Shutdown ctx的超时时间改为10s。我们再来运行这个demo，并通过curl命令连接该server(curl  -v http://localhost:8080)，待连接成功后，再立即ctrl+c停止Server，待约10s后，我们得到如下日志：</p>
<pre><code>$go run server.go
2017/02/02 15:15:57 Handle a new request: {GET / HTTP/1.1 1 1 map[User-Agent:[curl/7.30.0] Accept:[*/*]] {} &lt;nil&gt; 0 [] false localhost:8080 map[] map[] &lt;nil&gt; map[] [::1]:52590 / &lt;nil&gt; &lt;nil&gt; &lt;nil&gt; 0xc420016700}
^C2017/02/02 15:15:59 Shutting down server...
2017/02/02 15:15:59 listen: http: Server closed
2017/02/02 15:16:09 Server gracefully stopped: context deadline exceeded
</code></pre>
<p>c) 当前有未处理完的active connections，ctx超时之前，这些connections处理ok了</p>
<p>我们将上述demo的参数还原，即request handler sleep 10s，而Shutdown ctx超时时间为30s，运行这个Demo后，通过curl命令连接该server，待连接成功后，再立即ctrl+c停止Server。等待约10s后，我们得到如下日志：</p>
<pre><code>$go run server.go
2017/02/02 15:19:56 Handle a new request: {GET / HTTP/1.1 1 1 map[User-Agent:[curl/7.30.0] Accept:[*/*]] {} &lt;nil&gt; 0 [] false localhost:8080 map[] map[] &lt;nil&gt; map[] [::1]:52605 / &lt;nil&gt; &lt;nil&gt; &lt;nil&gt; 0xc420078500}
^C2017/02/02 15:19:59 Shutting down server...
2017/02/02 15:19:59 listen: http: Server closed
2017/02/02 15:20:06 Handle the request ok!
2017/02/02 15:20:06 Server gracefully stopped: &lt;nil&gt;
</code></pre>
<p>可以看出，当ctx超时之前，request处理ok，connection关闭。这时不再有active connection和idle connection了，Shutdown成功返回，server立即退出。</p>
<h4>4、Mutex Contention Profiling</h4>
<p>Go 1.8中runtime新增了对Mutex和RWMutex的profiling(剖析)支持。golang team成员，负责从go user角度去看待go team的work是否满足用户需求的<a href="https://github.com/rakyll">Jaana B. Dogan</a>在其个人站点上写了一篇<a href="https://rakyll.org/mutexprofile/">介绍mutex profiling的文章</a>，这里借用一下其中的Demo：</p>
<pre><code>//go18-examples/stdlib/mutexprofile/mutexprofile.go

package main

import (
    "net/http"
    _ "net/http/pprof"
    "runtime"
    "sync"
)

func main() {
    var mu sync.Mutex
    var items = make(map[int]struct{})

    runtime.SetMutexProfileFraction(5)
    for i := 0; i &lt; 1000*1000; i++ {
        go func(i int) {
            mu.Lock()
            defer mu.Unlock()
            items[i] = struct{}{}
        }(i)
    }

    http.ListenAndServe(":8888", nil)
}
</code></pre>
<p>运行该程序后，在浏览器中输入：http://localhost:8888/debug/pprof/mutex，你就可以看到有关该程序的mutex profile（耐心等待一小会儿，因为数据的采样需要一点点时间^0^）：</p>
<pre><code>--- mutex:
cycles/second=2000012082
sampling period=5
378803564 776 @ 0x106c4d1 0x13112ab 0x1059991
</code></pre>
<p>构建该程序，然后通过下面命令：</p>
<pre><code>go build mutexprofile.go
./mutexprofile
go tool pprof mutexprofile http://localhost:8888/debug/pprof/mutex?debug=1
</code></pre>
<p>可以进入pprof交互界面，这个是所有用过<a href="http://tonybai.com/2015/08/25/go-debugging-profiling-optimization/">go pprof工具</a>的<a href="http://tonybai.com/2016/04/18/my-experience-of-gopherchina2016/">gophers</a>们所熟知的：</p>
<pre><code>$go tool pprof mutexprofile http://localhost:8888/debug/pprof/mutex?debug=1
Fetching profile from http://localhost:8888/debug/pprof/mutex?debug=1
Saved profile in /Users/tony/pprof/pprof.mutexprofile.localhost:8888.contentions.delay.003.pb.gz
Entering interactive mode (type "help" for commands)
(pprof) list
Total: 12.98s
ROUTINE ======================== main.main.func1 in /Users/tony/Test/GoToolsProjects/src/github.com/bigwhite/experiments/go18-examples/stdlib/mutexprofile/mutexprofile.go
         0     12.98s (flat, cum)   100% of Total
         .          .     17:            mu.Lock()
         .          .     18:            defer mu.Unlock()
         .          .     19:            items[i] = struct{}{}
         .          .     20:        }(i)
         .          .     21:    }
         .     12.98s     22:
         .          .     23:    http.ListenAndServe(":8888", nil)
         .          .     24:}
ROUTINE ======================== runtime.goexit in /Users/tony/.bin/go18rc2/src/runtime/asm_amd64.s
         0     12.98s (flat, cum)   100% of Total
         .          .   2192:    RET
         .          .   2193:
         .          .   2194:// The top-most function running on a goroutine
         .          .   2195:// returns to goexit+PCQuantum.
         .          .   2196:TEXT runtime·goexit(SB),NOSPLIT,$0-0
         .     12.98s   2197:    BYTE    $0x90    // NOP
         .          .   2198:    CALL    runtime·goexit1(SB)    // does not return
         .          .   2199:    // traceback from goexit1 must hit code range of goexit
         .          .   2200:    BYTE    $0x90    // NOP
         .          .   2201:
         .          .   2202:TEXT runtime·prefetcht0(SB),NOSPLIT,$0-8
ROUTINE ======================== sync.(*Mutex).Unlock in /Users/tony/.bin/go18rc2/src/sync/mutex.go
    12.98s     12.98s (flat, cum)   100% of Total
         .          .    121:            return
         .          .    122:        }
         .          .    123:        // Grab the right to wake someone.
         .          .    124:        new = (old - 1&lt;&lt;mutexWaiterShift) | mutexWoken
         .          .    125:        if atomic.CompareAndSwapInt32(&amp;m.state, old, new) {
    12.98s     12.98s    126:            runtime_Semrelease(&amp;m.sema)
         .          .    127:            return
         .          .    128:        }
         .          .    129:        old = m.state
         .          .    130:    }
         .          .    131:}
(pprof) top10
1.29s of 1.29s total (  100%)
      flat  flat%   sum%        cum   cum%
     1.29s   100%   100%      1.29s   100%  sync.(*Mutex).Unlock
         0     0%   100%      1.29s   100%  main.main.func1
         0     0%   100%      1.29s   100%  runtime.goexit

</code></pre>
<p>go pprof的另外一个用法就是在go test时，mutexprofile同样支持这一点：</p>
<pre><code>go test -mutexprofile=mutex.out
go tool pprof &lt;test.binary&gt; mutex.out
</code></pre>
<h4>5、其他重要改动</h4>
<p>Go 1.8标准库还有两个值得注意的改动，一个是：crypto/tls，另一个是database/sql。</p>
<p>在<a href="http://tonybai.com/2015/04/30/go-and-https/">HTTPS</a>逐渐成为主流的今天，各个编程语言对HTTPS连接的底层加密协议- <a href="https://en.wikipedia.org/wiki/Transport_Layer_Security">TLS协议</a>支持的成熟度日益被人们所关注。Go 1.8给广大Gophers们带来了一个更为成熟、性能更好、更为安全的TLS实现，同时也增加了对一些TLS领域最新协议规范的支持。无论你是实现TLS Server端，还是Client端，都将从中获益。</p>
<p>Go 1.8在crypto/tls中提供了基于ChaCha20-Poly1305的cipher suite，其中ChaCha20是一种stream cipher算法；而Poly1305则是一种code authenticator算法。它们共同组成一个TLS suite。使用这个suite，将使得你的web service或站点<a href="https://blog.cloudflare.com/do-the-chacha-better-mobile-performance-with-cryptography/">具有更好的mobile浏览性能</a>，这是因为传统的AES算法实现在没有<a href="http://en.wikipedia.org/wiki/AES_instruction_set">硬件支持</a>的情况下cost更多。因此，如果你在使用tls时没有指定cipher suite，那么Go 1.8会根据硬件支持情况（是否有AES的硬件支持），来决定是使用ChaCha20还是AES算法。除此之外，crypto/tls还实现了更为安全和高效的<a href="https://www.rfc-editor.org/rfc/rfc7748.txt">X25519</a>密钥交换算法等。</p>
<p><a href="http://tonybai.com/2014/11/04/some-changes-in-go-1-4/">Go 1.4</a>以来，database/sql包的变化很小，但对于该包的feature需求却在与日俱增。终于在Go 1.8这个dev cycle中，<a href="https://github.com/kardianos/govendor">govendor</a>的作者<a href="https://github.com/kardianos">Daniel Theophanes</a>在<a href="https://github.com/bradfitz">Brad Fitzpatrick</a>的“指导”下，开始对database/sql进行“大规模”的改善。在Go 1.8中，借助于context.Context的帮助，database/sql增加了Cancelable Queries、SQL Database Type、Multiple Result Sets、Database ping、Named Parameters和Transaction Isolation等新Features。在<a href="https://blog.gopheracademy.com/">GopherAcademy</a>的Advent 2016系列文章中，我们可以看到<a href="https://blog.gopheracademy.com/advent-2016/database_sql/">Daniel Theophanes亲手撰写的文章</a>，文章针对Go 1.8 database/sql包新增的features作了详细解释。</p>
<h3>三、Go工具链（Go Toolchain）</h3>
<p>在目前市面上的主流编程语言中，如果说Go的工具链在成熟度和完善度方面排第二，那没有语言敢称自己是第一吧^_^。Go 1.8在Go Toolchain上继续做着持续地改进，下面我们来逐一看看。</p>
<h4>1、Plugins</h4>
<p>Go在1.8版本中提供了对Plugin的初步支持，并且这种支持仅限于<a href="http://tonybai.com/tag/linux">Linux</a>。plugin这个术语在不同语言、不同情景上下文中有着不同的含义，那么什么是Go Plugin呢？</p>
<p>Go Plugin为Go程序提供了一种在运行时加载代码、执行代码以改变运行行为的能力，它实质上由两个部分组成：</p>
<ul>
<li>go build -buildmode=plugin xx.go 构建xx.so plugin文件</li>
<li>利用plugin包在运行时动态加载xx.so并执行xx.so中的代码</li>
</ul>
<p>C程序员看到这里肯定会有似曾相识的赶脚，因为这和传统的<a href="http://tonybai.com/2010/12/13/also-talk-about-shared-library/">动态共享库</a>在概念上十分类似：</p>
<pre><code>go build -buildmode=plugin xx.go 类似于 gcc -o xx.so -shared xx.c
go plugin包 类似于 linux上的dlopen/dlsym或windows上的LoadLibrary
</code></pre>
<p>我们来看一个例子！我们先来建立一个名为foo.so的go plugin：</p>
<pre><code>//go18-examples/gotoolchain/plugins/foo.go

package main

import "fmt"

var V int
var v int

func init() {
        V = 17
        v = 23
        fmt.Println("init function in plugin foo")
}

func Foo(in string) string {
        return "Hello, " + in
}

func foo(in string) string {
        return "hello, " + in
}

</code></pre>
<p>通过go build命令将foo.go编译为foo.so：</p>
<pre><code># go build -buildmode=plugin foo.go
# ldd foo.so
    linux-vdso.so.1 =&gt;  (0x00007ffe47f67000)
    libpthread.so.0 =&gt; /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f9d06f4b000)
    libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007f9d06b82000)
    /lib64/ld-linux-x86-64.so.2 (0x000055c69cfcf000)

# nm foo.so|grep Foo
0000000000150010 t local.plugin/unnamed-69e21ef38d16a3fee5eb7b9e515c27a389067879.Foo
0000000000150010 T plugin/unnamed-69e21ef38d16a3fee5eb7b9e515c27a389067879.Foo
000000000036a0dc D type..namedata.Foo.

</code></pre>
<p>我们看到go plugin的.so文件就是一个标准的Linux动态共享库文件，我们可以通过nm命令查看.so中定义的各种符号。接下来，我们来load这个.so，并查找并调用相应符号：</p>
<pre><code>//go18-examples/gotoolchain/plugins/main.go

package main

import (
        "fmt"
        "plugin"
        "time"
)

func init() {
        fmt.Println("init in main program")
}

func loadPlugin(i int) {
        fmt.Println("load plugin #", i)
        var err error
        fmt.Println("before opening the foo.so")

        p, err := plugin.Open("foo.so")
        if err != nil {
                fmt.Println("plugin Open error:", err)
                return
        }
        fmt.Println("after opening the foo.so")

        f, err := p.Lookup("Foo")
        if err != nil {
                fmt.Println("plugin Lookup symbol Foo error:", err)
        } else {
                fmt.Println(f.(func(string) string)("gophers"))
        }

        f, err = p.Lookup("foo")
        if err != nil {
                fmt.Println("plugin Lookup symbol foo error:", err)
        } else {
                fmt.Println(f.(func(string) string)("gophers"))
        }

        v, err := p.Lookup("V")
        if err != nil {
                fmt.Println("plugin Lookup symbol V error:", err)
        } else {
                fmt.Println(*v.(*int))
        }

        v, err = p.Lookup("v")
        if err != nil {
                fmt.Println("plugin Lookup symbol v error:", err)
        } else {
                fmt.Println(*v.(*int))
        }
        fmt.Println("load plugin #", i, "done")
}

func main() {
        var counter int = 1
        for {
                loadPlugin(counter)
                counter++
                time.Sleep(time.Second * 30)
        }
}

</code></pre>
<p>执行这个程序：</p>
<pre><code># go run main.go
init in main program
load plugin # 1
before opening the foo.so
init function in plugin foo
after opening the foo.so
Hello, gophers
plugin Lookup symbol foo error: plugin: symbol foo not found in plugin plugin/unnamed-69e21ef38d16a3fee5eb7b9e515c27a389067879
17
plugin Lookup symbol v error: plugin: symbol v not found in plugin plugin/unnamed-69e21ef38d16a3fee5eb7b9e515c27a389067879
load plugin # 1 done

load plugin # 2
before opening the foo.so
after opening the foo.so
Hello, gophers
plugin Lookup symbol foo error: plugin: symbol foo not found in plugin plugin/unnamed-69e21ef38d16a3fee5eb7b9e515c27a389067879
17
plugin Lookup symbol v error: plugin: symbol v not found in plugin plugin/unnamed-69e21ef38d16a3fee5eb7b9e515c27a389067879
load plugin # 2 done
... ...
</code></pre>
<p>我们来分析一下这个执行结果！</p>
<p>a) foo.go中的代码也包含在main package下，但只是当foo.so被第一次加载时，foo.go中的init函数才会被执行；<br />
b) foo.go中的exported function和variable才能被Lookup到，如Foo、V；查找unexported的变量和函数符号将得到error信息，如：“symbol foo not found in plugin”；<br />
c) Lookup返回的是plugin.Symbol类型的值，plugin.Symbol是一个指向plugin中变量或函数的指针；<br />
d) foo.go中的init在后续重复加载中并不会被执行。</p>
<p>注意：plugin.Lookup是goroutine-safe的。</p>
<p>在golang-dev group上，有人曾问过：buildmode=c-shared和buildmode=plugin有何差别？Go team member给出的答案如下：</p>
<pre><code>The difference is mainly on the program that loads the shared library.

For c-shared, we can't assume anything about the host, so the c-shared dynamic library must be self-contained, but for plugin, we know the host program will be a Go program built with the same runtime version, so the toolchain can omit at least the runtime package from the dynamic library, and possibly more if it's certain that some packages are linked into the host program. (This optimization hasn't be implemented yet, but we need the distinction to enable this kind of optimization in the future.)
</code></pre>
<h4>2、默认的GOPATH</h4>
<p>Go team在Go 1.8以及后续版本会更加注重”Go语言的亲民性”，即进一步降低Go的入门使用门槛，让大家更加Happy的使用Go。对于一个Go初学者来说，一上来就进行GOPATH的设置很可能让其感到有些迷惑，甚至有挫折感，就像建立Java开发环境需要设置JAVA_HOME和CLASSPATH一样。Gophers们期望能做到Go的安装即可用。因此Go 1.8就在这方面做出了改进：支持默认的GOPATH。</p>
<p>在Linux/Mac系下，默认的GOPATH为$HOME/go，在Windows下，GOPATH默认路径为：%USERPROFILE%/go。你可以通过下面命令查看到这一结果：</p>
<pre><code>$ go env
GOARCH="amd64"
GOBIN="/home/tonybai/.bin/go18rc3/bin"
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOOS="linux"
GOPATH="/home/tonybai/go"
GORACE=""
GOROOT="/home/tonybai/.bin/go18rc3"
GOTOOLDIR="/home/tonybai/.bin/go18rc3/pkg/tool/linux_amd64"
GCCGO="gccgo"
CC="gcc"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build313929093=/tmp/go-build -gno-record-gcc-switches"
CXX="g++"
CGO_ENABLED="1"
PKG_CONFIG="pkg-config"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"

</code></pre>
<p>BTW，在Linux/Mac下，默认的GOROOT为/usr/local/go，如果你的Go环境没有安装到这个路径下，在没有设置$GOROOT环境变量的情况下，当你执行go subcommand相关命令时，你会看到如下错误：</p>
<pre><code>$go env
go: cannot find GOROOT directory: /usr/local/go
</code></pre>
<h4>3、其他变化</h4>
<p>Go 1.8删除了<a href="http://tonybai.com/2016/06/21/some-changes-in-go-1-7/">Go 1.7</a>中增加的用于关闭ssa新后端的”-ssa=0” compiler flag，并且将ssa backend扩展到所有architecture中，对ssa后端也进一步做了优化。与此同时，为了将来进一步的性能优化打基础，Go 1.8还引入了一个新编译器前端，当然这对于普通Gopher的Go使用并没有什么影响。</p>
<p>Go 1.8还新增go bug子命令，该命令会自动使用默认浏览器打开new issue页面，并将采集到的issue提交者的系统信息填入issue模板，以帮助gopher提交符合要求的go issue，下面是go bug打开的issue page的图示：</p>
<p><img src="http://tonybai.com/wp-content/uploads/go18-go-bug-page.png" alt="img{512x368}" /></p>
<h3>四、性能变化（Performance Improvement）</h3>
<p>无论是Gotoolchain、还是runtime（包括GC）的性能，一直都是Go team重点关注的领域。本次Go 1.8依旧给广大Gophers们带来了性能提升方面的惊喜。</p>
<p>首先，Go <a href="https://en.wikipedia.org/wiki/Static_single_assignment_form">SSA</a>后端扩展到所有architecture和新编译器前端的引入，将会给除X86-64之外架构上运行的Go代码带来约20-30%的运行性能提升。对于x86-64，虽然Go 1.7就已经开启了SSA，但Go 1.8对SSA做了进一步优化，x86-64上的Go代码依旧可能会得到10%以内的性能提升。</p>
<p>其次，Go 1.8持续对Go compiler和linker做性能优化，和1.7相比，平均编译链接的性能提升幅度在15%左右。虽然依旧没有达到<a href="http://tonybai.com/2014/11/04/some-changes-in-go-1-4/">Go 1.4</a>的性能水准。不过，优化依旧在持续进行中，目标的达成是可期的。</p>
<p>再次，GC在低延迟方面的优化给了我们最大的惊喜。在Go 1.8中，由于消除了GC的“<a href="https://github.com/golang/proposal/blob/master/design/17503-eliminate-rescan.md">stop-the-world stack re-scanning</a>”，使得GC STW(stop-the-world)的时间通常低于100微秒，甚至经常低于10微秒。当然这或多或少是以牺牲“吞吐”作为代价的。因此在Go 1.9中，GC的改进将持续进行，会在吞吐和低延迟上做一个很好的平衡。</p>
<p>最后，defer的性能消耗在Go 1.8中下降了一半，与此下降幅度相同的还有通过cgo在go中调用C代码的性能消耗。</p>
<h3>五、小结兼参考资料</h3>
<p>Go 1.8的变化不仅仅是以上这些，更多变化以及详细的描述请参考下面参考资料中的“Go 1.8 Release Notes”：</p>
<ul>
<li><a href="https://blog.gopheracademy.com/advent-2016/go-1.8/">Go 1.8</a></li>
<li><a href="https://golang.org/doc/go1.8">Go 1.8 Release Notes</a></li>
<li><a href="https://beta.golang.org/doc/go1.8">Go 1.8 Release Notes(before release)</a></li>
<li><a href="https://blog.cloudflare.com/announcing-support-for-http-2-server-push-2/">Announcing Support for HTTP/2 Server Push</a></li>
<li><a href="https://tylerchr.blog/golang-18-whats-coming/">What&#8217;s coming in Go 1.8</a></li>
</ul>
<p>以上demo中的代码在<a href="https://github.com/bigwhite/experiments/tree/master/go18-examples">这里</a>可以找到。</p>
<p style='text-align:left'>&copy; 2017, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2017/02/03/some-changes-in-go-1-8/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>Go包导入与Java的差别</title>
		<link>https://tonybai.com/2016/09/13/package-import-in-golang-vs-in-java/</link>
		<comments>https://tonybai.com/2016/09/13/package-import-in-golang-vs-in-java/#comments</comments>
		<pubDate>Tue, 13 Sep 2016 14:05:16 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[alias]]></category>
		<category><![CDATA[class]]></category>
		<category><![CDATA[CLASSPATH]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GOPATH]]></category>
		<category><![CDATA[GOROOT]]></category>
		<category><![CDATA[import]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[别名]]></category>
		<category><![CDATA[包]]></category>
		<category><![CDATA[类]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2024</guid>
		<description><![CDATA[闲暇时翻阅了近期下载到的电子书《Go in Practice》 ，看到1.2.4 Package Management一节中的代码Demo，感觉作者对Go package导入的说法似乎不够精确：“Packages are imported by their name”(后续的说明将解释不精确的原因)。联想到前几天遇到的一个Java包导入的问题，让我隐约地感觉Java程序员很容易将两种语言的Package import机制搞混淆，于是打算在这里将Golang和Java的Package import机制做一个对比，对于Java转型到Golang的程序员将大有裨益:)。这里的重点在于与Java的对比，关于Golang的Package Import的细节可以参考我之前写过的一篇文章《理解Golang包导入》。 我们先来看两个功能等价的代码。 //TestDate.java import java.util.*; import java.text.DateFormat; public class TestDate { public static void main(String []args){ Date d = new Date(); String s = DateFormat.getDateInstance().format(d); System.out.println(s); } } 和 //testdate.go package main import ( "fmt" "time" ) func main() { t [...]]]></description>
			<content:encoded><![CDATA[<p>闲暇时翻阅了近期下载到的电子书<a href="https://book.douban.com/subject/26345890/">《Go in Practice》</a> ，看到1.2.4 Package Management一节中的代码Demo，感觉作者对Go package导入的说法似乎不够精确：“Packages are imported by their name”(后续的说明将解释不精确的原因)。联想到前几天遇到的一个Java包导入的问题，让我隐约地感觉Java程序员很容易将两种语言的Package import机制搞混淆，于是打算在这里将Golang和Java的Package import机制做一个对比，对于Java转型到Golang的程序员将大有裨益:)。这里的重点在于与Java的对比，关于Golang的Package Import的细节可以参考我之前写过的一篇文章<a href="http://tonybai.com/2015/03/09/understanding-import-packages/">《理解Golang包导入》</a>。</p>
<p>我们先来看两个功能等价的代码。</p>
<pre><code>//TestDate.java
import java.util.*;
import java.text.DateFormat;

public class TestDate {
        public static void main(String []args){
                Date d = new Date();
                String s = DateFormat.getDateInstance().format(d);
                System.out.println(s);
        }
}

</code></pre>
<p>和</p>
<pre><code>//testdate.go
package main

import (
    "fmt"
    "time"
)

func main() {
    t := time.Now()
    fmt.Println(t.Format("2006-01-02"))
}

</code></pre>
<p>两个程序在Run时，都输出下面内容：</p>
<pre><code>2016-9-13
</code></pre>
<p>我们看到Golang和Java都是用import关键字来进行包导入的：</p>
<pre><code>import java.util.Date;

Date d = new Date();
</code></pre>
<p>vs.</p>
<pre><code>import "time"

t := time.Now()

</code></pre>
<p>咋看起来，Java在package import后似乎使用起来更Easy，使用包内的类和方法时，前面无需再附着Package name，即Date d，而不是java.util.Date d。而Go在导入”time”后，引用包中方法时依然要附着着包名，比如time.Now()。但实质上两种语言在import package的机制上是有很大不同的。</p>
<h4>1、机制</h4>
<p>虽然都使用import，但import关键字后面的字符串所代表的含义有不同。</p>
<p>Java import导入的是类而不是包，import后面的字符串表示的是按需导入Java Package下面的类，比如import java.util.*； 或导入Package下某个类，比如import java.util.Date。而Go import关键字后面的字符串是包名吗？很多初学者会认为这个就是Go包名，实则不然，Go import后面的字符串实际上是一个包导入路径，这也是Java用”xxx.yyy.zzz”形式而Golang使用”xxx/yyy/zzz”形式的原因。我们用个简单的例子就能证明这一点。我们知道Golang会在\$GOROOT/src + \$GOPATH/src下面导入xxx/yyy/zzz路径下的包，我们在import “fmt”时，实际上导入的是\$GOROOT/src/fmt目录下的包，只是恰好这个下面的包的名字是fmt罢了。如果我们将\$GOROOT/src/fmt目录改名为fmt1，结果会是如何呢？</p>
<pre><code>$go build helloworld.go
helloworld.go:3:8: cannot find package "fmt" in any of:
           /Users/tony/.bin/go17/src/fmt (from $GOROOT)
           /Users/tony/Test/GoToolsProjects/src/fmt (from $GOPATH)

helloworld.go是一个helloworld go源码。
</code></pre>
<p>之所以出错是因为在\$GOROOT/src下已经没有fmt这个目录了，所以下面代码中的两个fmt含义是不同的（这也解释了Go in practice中关于包导入的说法的不精确的原因）：</p>
<pre><code>package main

import "fmt"  ---- 这里的fmt指的是$GOROOT/src下的名为"fmt"的目录名

func main() {
    fmt.Println("Hello, World") --- 这里的fmt是真正的包名"fmt"
}
</code></pre>
<p>从上面我们可以看出Go的包名和包的源文件所在的路径的名字并没有必须一致的要求，这也是为什么在Go源码使用包时一定是用packagename.XX形式，而不是packagename.subpackagename.XX的形式了。比如导入”net/http”后，我们在源码中使用的是http.xxx，而不是net.http.xxx，因为net/http只是一个路径，并不是一个嵌套的包名。</p>
<p>之所以看起来导入路径的终段目录名与包名一致，只是因为这是Go官方的建议：Go的导入路径的最后一段目录名(xxx/yyy/zzz中的zzz)与该目录（zzz）下面源文件中的Go Package名字相同。</p>
<p>下面是一个非标准库的包名与导入路径终段名完全不一致的例子：</p>
<pre><code>//github.com/pkgtest/pkg1/foo.go
package foo

import "fmt"

func Foo() {
    fmt.Println("Foo in pkg1")
}

//testfoo.go
package main

import (
    "github.com/pkgtest/pkg1"
)

func main() {
    foo.Foo() //输出：Foo in pkg1
}

</code></pre>
<p>可以看出testfoo.go导入的是”github.com/pkgtest/pkg1&#8243;这个路径，但这个路径下的包名却是foo。</p>
<p>Java语言中的包实际以.jar为单位，.jar内部实际上也是以路径组织.class文件的，比如：foo.jar这个jar包中有一个package名为：com.tonybai.foo，foo包中包含类Foo、Bar，那实际上foo.jar内部的目录格式将是：</p>
<pre><code>foo.jar
    - com/
        - tonybai/
            - foo/
                - Foo.class
                - Bar.class
</code></pre>
<p>但对于Java包的使用者，这些都是透明的。</p>
<h4>2、重名</h4>
<p>Java中关于包导入(实则是类导入)唯一的约束就是不能有两个类导入后的full name相同，如果存在两个导入类的full name完全相同，Javac在resolve时，要以ClassPath路径的先后顺序为准了，选择最先遇到的那个类。但是在Go中，如果导入的两个路径下的包名相同，那么Go compiler显然是不能允许这种情况的存在的，会给出Error信息。</p>
<p>比如我们在GOPATH下的github.com/pkgtest/pkg1和github.com/pkgtest/pkg2下放置了同名包foo，下面代码将会报错：</p>
<pre><code>package main

import (
    "github.com/pkgtest/pkg1"
    "github.com/pkgtest/pkg2"
)

func main() {
    foo.Foo()
}

</code></pre>
<p>错误信息如下：</p>
<pre><code>$go run testfoo.go
# command-line-arguments
./testdate.go:8: foo redeclared as imported package name
           previous declaration at ./testfoo.go:7
</code></pre>
<p>解决这一问题的方法就是采用package alias：</p>
<pre><code>package main

import (
    a "github.com/pkgtest/pkg1"
    b "github.com/pkgtest/pkg2"
)

func main() {
    a.Foo()
    b.Foo()
}
</code></pre>
<p>编译执行上面程序将得到下面结果，而不是Error：</p>
<pre><code>Foo of foo package in pkg1
Foo in foo package in pkg2
</code></pre>
<p style='text-align:left'>&copy; 2016, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2016/09/13/package-import-in-golang-vs-in-java/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Go 1.7中值得关注的几个变化</title>
		<link>https://tonybai.com/2016/06/21/some-changes-in-go-1-7/</link>
		<comments>https://tonybai.com/2016/06/21/some-changes-in-go-1-7/#comments</comments>
		<pubDate>Mon, 20 Jun 2016 22:26:12 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Cgo]]></category>
		<category><![CDATA[Context]]></category>
		<category><![CDATA[GC]]></category>
		<category><![CDATA[gccgo]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Go1.5]]></category>
		<category><![CDATA[Go1.6]]></category>
		<category><![CDATA[Go1.7]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GOPATH]]></category>
		<category><![CDATA[Gopher]]></category>
		<category><![CDATA[GopherCon]]></category>
		<category><![CDATA[GOROOT]]></category>
		<category><![CDATA[goroutine]]></category>
		<category><![CDATA[internal]]></category>
		<category><![CDATA[latency]]></category>
		<category><![CDATA[map]]></category>
		<category><![CDATA[Performance]]></category>
		<category><![CDATA[ReleaseCycle]]></category>
		<category><![CDATA[RobPike]]></category>
		<category><![CDATA[runtime]]></category>
		<category><![CDATA[RussCox]]></category>
		<category><![CDATA[Rust]]></category>
		<category><![CDATA[SSA]]></category>
		<category><![CDATA[subtest]]></category>
		<category><![CDATA[Testing]]></category>
		<category><![CDATA[vendor]]></category>
		<category><![CDATA[垃圾收集器]]></category>
		<category><![CDATA[子测试]]></category>
		<category><![CDATA[性能调优]]></category>
		<category><![CDATA[编译]]></category>
		<category><![CDATA[编译器]]></category>
		<category><![CDATA[运行时]]></category>
		<category><![CDATA[链接器]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2001</guid>
		<description><![CDATA[零、从Release Cycle说起 从Go 1.3版本开始，Golang核心开发Team的版本开发周期逐渐稳定下来。经过Go 1.4、Go1.5和Go 1.6的实践，大神Russ Cox在Go wiki上大致定义了Go Release Cycle的一般流程： 半年一个major release版本。 发布流程启动时间：每年8月1日和次年2月1日（真正发布日期有可能是这个日子，也可能延后几天）。 半年的周期中，前三个月是Active Development，then 功能冻结（大约在11月1日和次年的5月1日）。接下来的三个月为test和polish。 下一个版本的启动计划时间：7月15日和1月15日，版本计划期持续15天，包括讨论这个major版本中要实现的主要功能、要fix的前期遗留的bug。 release前的几个阶段版本：beta版本若干（一般是2-3个）、release candidate版本若干（一般是1-2个）和最后的release版本。 major release版本的维护是通过一系列的minor版本体现的，主要是修正一些导致crash的严重问题或是安全问题，比如major release版本Go 1.6目前就有go 1.6.1和go 1.6.2两个后续minor版本发布。 在制定下一个版本启动计划时，一般会由Russ Cox在golang-dev group发起相关讨论，其他Core developer在讨论帖中谈一下自己在下一个版本中要做的事情，让所有开发者大致了解一下下个版本可能包含的功能和修复的bug概况。但这些东西是否能最终包含在下一个Release版本中，还要看Development阶段feature代码是否能完成、通过review并加入到main trunk中；如果来不及加入，这个功能可能就会放入下一个major release中，比如SSA就错过了Go 1.6（由于Go 1.5改动较大，留给Go 1.6的时间短了些）而放在了Go 1.7中了。 个人感觉Go社区采用的是一种“民主集中制”的文化，即来自Google的Golang core team的少数人具有实际话语权，尤其是几个最早加入Go team的大神，比如Rob Pike老头、Russ Cox以及Ian Lance Taylor等。当然绝大部分合理建议还是被merge到了Go代码中的，但一些与Go哲学有背离的想法，比如加入泛型、增加新类型、改善错误处理等，基本都被Rob Pike老头严词拒绝了，至少Go 1兼容版本中，大家是铁定看不到的了。至于Go 2，就连Go core team的人也不能不能打包票说一定会有这样的新语言规范。不过从Rob Pike前些阶段的一些言论中，大致可以揣摩出Pike老头正在反思Go 1的设计，也许他正在做Go 2的语言规范也说不定呢^_^。这种“文化”并不能被很多开源开发者所欣赏，在GopherChina 2016大会上，大家就对这种“有些独裁”的文化做过深刻了辩论，尤其是对比Rust那种“绝对民主”的文化。见仁见智的问题，这里就不深入了。个人觉得Go core team目前的做法还是可以很好的保持Go语言在版本上的理想的兼容性和发展的一致性的，对于一门面向工程领域的语言而言，这也许是开发者们较为看重的东西；编程语言语法在不同版本间“跳跃式”的演进也许会在短时间内带来新鲜感，但长久看来，对代码阅读和维护而言，都会有一个不小的负担。 下面回归正题。Go [...]]]></description>
			<content:encoded><![CDATA[<h3>零、从Release Cycle说起</h3>
<p>从Go 1.3版本开始，Golang核心开发Team的版本开发周期逐渐稳定下来。经过<a href="http://tonybai.com/2014/11/04/some-changes-in-go-1-4/">Go 1.4</a>、<a href="http://tonybai.com/2015/07/10/some-changes-in-go-1-5/">Go1.5</a>和<a href="http://tonybai.com/2016/02/21/some-changes-in-go-1-6/">Go 1.6</a>的实践，大神<a href="https://github.com/rsc">Russ Cox</a>在<a href="https://github.com/golang/go/wiki">Go wiki</a>上大致定义了<a href="https://github.com/golang/go/wiki/Go-Release-Cycle">Go Release Cycle</a>的一般流程：</p>
<ol>
<li>半年一个major release版本。</li>
<li>发布流程启动时间：每年8月1日和次年2月1日（真正发布日期有可能是这个日子，也可能延后几天）。</li>
<li>半年的周期中，前三个月是Active Development，then 功能冻结（大约在11月1日和次年的5月1日）。接下来的三个月为test和polish。</li>
<li>下一个版本的启动计划时间：7月15日和1月15日，版本计划期持续15天，包括讨论这个major版本中要实现的主要功能、要fix的前期遗留的bug。</li>
<li>release前的几个阶段版本：beta版本若干（一般是2-3个）、release candidate版本若干（一般是1-2个）和最后的release版本。</li>
<li>major release版本的维护是通过一系列的minor版本体现的，主要是修正一些导致crash的严重问题或是安全问题，比如major release版本Go 1.6目前就有go 1.6.1和go 1.6.2两个后续minor版本发布。</li>
</ol>
<p>在制定下一个版本启动计划时，一般会由Russ Cox在<a href="https://groups.google.com/forum/#!forum/golang-dev">golang-dev group</a>发起相关讨论，其他Core developer在讨论帖中谈一下自己在下一个版本中要做的事情，让所有开发者大致了解一下下个版本可能包含的功能和修复的bug概况。但这些东西是否能最终包含在下一个Release版本中，还要看Development阶段feature代码是否能完成、通过review并加入到main trunk中；如果来不及加入，这个功能可能就会放入下一个major release中，比如SSA就错过了Go 1.6（由于Go 1.5改动较大，留给Go 1.6的时间短了些）而放在了Go 1.7中了。</p>
<p>个人感觉Go社区采用的是一种“民主集中制”的文化，即来自Google的Golang core team的少数人具有实际话语权，尤其是几个最早加入Go team的大神，比如<a href="https://github.com/robpike">Rob Pike</a>老头、Russ Cox以及<a href="https://github.com/ianlancetaylor">Ian Lance Taylor</a>等。当然绝大部分合理建议还是被merge到了Go代码中的，但一些与Go哲学有背离的想法，比如加入泛型、增加新类型、改善错误处理等，基本都被Rob Pike老头严词拒绝了，至少Go 1兼容版本中，大家是铁定看不到的了。至于Go 2，就连Go core team的人也不能不能打包票说一定会有这样的新语言规范。不过从Rob Pike前些阶段的一些言论中，大致可以揣摩出Pike老头正在反思Go 1的设计，也许他正在做Go 2的语言规范也说不定呢^_^。这种“文化”并不能被很多开源开发者所欣赏，在<a href="http://tonybai.com/2016/04/18/my-experience-of-gopherchina2016/">GopherChina 2016</a>大会上，大家就对这种“有些独裁”的文化做过深刻了辩论，尤其是对比<a href="https://www.rust-lang.org/">Rust</a>那种“绝对民主”的文化。见仁见智的问题，这里就不深入了。个人觉得Go core team目前的做法还是可以很好的保持Go语言在版本上的理想的兼容性和发展的一致性的，对于一门面向工程领域的语言而言，这也许是开发者们较为看重的东西；编程语言语法在不同版本间“跳跃式”的演进也许会在短时间内带来新鲜感，但长久看来，对代码阅读和维护而言，都会有一个不小的负担。</p>
<p>下面回归正题。Go 1.7究竟带来了哪些值得关注的变化呢？马上揭晓^_^。(以下测试所使用的Go版本为<a href="https://github.com/golang/go/releases/tag/go1.7beta2">go 1.7 beta2</a>)。</p>
<h3>一、语言</h3>
<p>Go 1.7在版本计划阶段设定的目标就是改善和优化(polishing)，因此在Go语言(Specification)规范方面继续保持着与Go 1兼容，因此理论上Go 1.7的发布对以往Go 1兼容的程序而言是透明的，已存在的代码均可以正常通过Go 1.7的编译并正确执行。</p>
<p>不过Go 1.7还是对Go1 Specs中关于“Terminating statements”的说明作了一个extremely tiny的改动：</p>
<pre><code>A statement list ends in a terminating statement if the list is not empty and its final statement is terminating.
=&gt;
A statement list ends in a terminating statement if the list is not empty and its final non-empty statement is terminating.

</code></pre>
<p>Specs是抽象的，例子是生动的，我们用一个例子来说明一下这个改动：</p>
<pre><code>// go17-examples/language/f.go

package f

func f() int {
    return 3
    ;
}

</code></pre>
<p>对于f.go中f函数的body中的语句列表（statement list），所有版本的go compiler或gccgo compiler都会认为其在”return 3&#8243;这个terminating statement处terminate，即便return语句后面还有一个“;”也没关系。但Go 1.7之前的gotype工具却严格按照go 1.7之前的Go 1 specs中的说明进行校验，由于最后的statement是”;” &#8211; 一个empty statement，gotype会提示：”missing return”：</p>
<pre><code>// Go 1.7前版本的gotype

$gotype f.go
f.go:6:1: missing return

</code></pre>
<p>于是就有了gotype与gc、gccgo行为的不一致！为此Go 1.7就做了一些specs上的改动，将statements list的terminate点从”final statement”改为“final non-empty statement”，这样即便后面再有”;”也不打紧了。于是用go 1.7中的gotype执行同样的命令，得到的结果却不一样：</p>
<pre><code>// Go 1.7的gotype
$gotype f.go
没有任何错误输出

</code></pre>
<p>gotype默认以源码形式随着Go发布，我们需要手工将其编译为可用的工具，编译步骤如下：</p>
<pre><code>$cd $GOROOT/src/go/types
$go build gotype.go
在当前目录下就会看到gotype可执行文件，你可以将其mv or cp到$GOBIN下，方便在命令行中使用。
</code></pre>
<h3>二、Go Toolchain（工具链）</h3>
<p>Go的toolchain的强大实用是毋容置疑的，也是让其他编程语言Fans直流口水的那部分。每次Go major version release，Go工具链都会发生或大或小的改进，这次也不例外。</p>
<h4>1、SSA</h4>
<p><a href="https://en.wikipedia.org/wiki/Static_single_assignment_form">SSA</a>（Static Single-Assignment），对于大多数开发者来说都是不熟悉的，也是不需要关心的，只有搞编译器的人才会去认真研究它究竟为何物。对于Go语言的使用者而言，SSA意味着让编译出来的应用更小，运行得更快，未来有更多的优化空间，而这一切的获得却不需要Go开发者修改哪怕是一行代码^_^。</p>
<p>在Go core team最初的计划中，SSA在Go 1.6时就应该加入，但由于Go 1.6开发周期较为短暂，SSA的主要开发者Keith Randall没能按时完成相关开发，尤其是在性能问题上没能达到之前设定的目标，因此merge被推迟到了Go 1.7。即便是Go 1.7，SSA也只是先完成了x86-64系统。<br />
据实而说，SSA后端的引入，风险还是蛮大的，因此Go在编译器中加入了一个开关”-ssa=0|1&#8243;，可以让开发者自行选择是否编译为SSA后端，默认情况下，在x86-64平台下SSA后端是打开的。同时，Go 1.7还修改了包导出的元数据的格式，由以前的文本格式换成了更为短小精炼的二进制格式，这也让Go编译出来的结果文件的Size更为small。</p>
<p>我们可以简单测试一下上述两个优化后对编译后结果的影响，我们以编译github.com/bigwhite/gocmpp/examples/client/例：</p>
<pre><code>-rwxrwxr-x 1 share share 4278888  6月 20 14:20 client-go16*
-rwxrwxr-x 1 share share 3319205  6月 20 14:04 client-go17*
-rwxrwxr-x 1 share share 3319205  6月 20 14:05 client-go17-no-newexport*
-rwxrwxr-x 1 share share 3438317  6月 20 14:04 client-go17-no-ssa*
-rwxrwxr-x 1 share share 3438317  6月 20 14:03 client-go17-no-ssa-no-newexport*

其中：client-go17-no-ssa是通过下面命令行编译的：

$go build -a -gcflags="-ssa=0" github.com/bigwhite/gocmpp/examples/client

client-go17-no-newexport*是通过下面命令行编译的：

$go build -a -gcflags="-newexport=0" github.com/bigwhite/gocmpp/examples/client

client-go17-no-ssa-no-newexport是通过下面命令行编译的：

$go build -a -gcflags="-newexport=0 -ssa=0" github.com/bigwhite/gocmpp/examples/client

</code></pre>
<p>对比client-go16和client-go17，我们可以看到默认情况下Go 17编译出来的可执行程序(client-go17)比Go 1.6编译出来的程序(client-go16)小了约21%，效果十分明显。这也与Go官方宣称的file size缩小20%~30%de 平均效果相符。</p>
<p>不过对比client-go17和client-go17-no-newexport，我们发现，似乎-newexport=0并没有起到什么作用，两个最终可执行文件的size相同。这个在ubuntu 14.04以及darwin平台上测试的结果均是如此，暂无解。</p>
<p>引入SSA后，官方说法是：程序的运行性能平均会提升5%~35%，数据来源于官方的<a href="https://golang.org/test/bench/go1/">benchmark</a>数据，这里就不再重复测试了。</p>
<h4>2、编译器编译性能</h4>
<p>Go 1.5发布以来，Go的编译器性能大幅下降就遭到的Go Fans们的“诟病”，虽然Go Compiler的性能与其他编程语言横向相比依旧是“独领风骚”。最差时，Go 1.5的编译构建时间是Go 1.4.x版本的4倍还多。这个问题也引起了Golang老大Rob Pike的极大关注，在Russ Cox筹划Go 1.7时，Rob Pike就极力要求要对Go compiler&amp;linker的性能进行优化，于是就有了Go 1.7“全民优化”Go编译器和linker的上百次commit，至少从目前来看，效果是明显的。</p>
<p>Go大神<a href="http://dave.cheney.net/">Dave Cheney</a>为了跟踪开发中的Go 1.7的编译器性能情况，建立了三个benchmark：<a href="https://github.com/davecheney/benchjuju">benchjuju</a>、<a href="https://github.com/davecheney/benchkube">benchkube</a>和<a href="https://github.com/davecheney/benchgogs">benchgogs</a>。Dave上个月最新贴出的一幅性能对比图显示：编译同一项目，Go 1.7编译器所需时间仅约是Go 1.6的一半，Go 1.4.3版本的2倍；也就是说经过优化后，Go 1.7的编译性能照比Go 1.6提升了一倍，离Go 1.4.3还有一倍的差距。</p>
<p><img src="http://tonybai.com/wp-content/uploads/go17-compiler-performance-improve.jpg" alt="img{}" /></p>
<h4>3、StackFrame Pointer</h4>
<p>在Go 1.7功能freeze前夕，Russ Cox将<a href="https://github.com/golang/go/issues/15840">StackFrame Pointer</a>加入到Go 1.7中了，目的是使得像Linux Perf或Intel Vtune等工具能更高效的抓取到go程序栈的跟踪信息。但引入STackFrame Pointer会有一些性能上的消耗，大约在2%左右。通过下面环境变量设置可以关闭该功能：</p>
<pre><code>export GOEXPERIMENT=noframepointer
</code></pre>
<h4>4、Cgo增加C.CBytes</h4>
<p><a href="http://tonybai.com/2012/09/26/interoperability-between-go-and-c/">Cgo</a>的helper函数在逐渐丰富，这次Cgo增加C.CBytes helper function就是<a href="https://github.com/golang/go/issues/14838">源于开发者的需求</a>。这里不再赘述Cgo的这些Helper function如何使用了，通过一小段代码感性了解一下即可：</p>
<pre><code>// go17-examples/gotoolchain/cgo/print.go

package main

// #include &lt;stdio.h&gt;
// #include &lt;stdlib.h&gt;
//
// void print(void *array, int len) {
//  char *c = (char*)array;
//
//  for (int i = 0; i &lt; len; i++) {
//      printf("%c", *(c+i));
//  }
//  printf("\n");
// }
import "C"

import "unsafe"

func main() {
    var s = "hello cgo"
    csl := C.CBytes([]byte(s))
    C.print(csl, C.int(len(s)))
    C.free(unsafe.Pointer(csl))
}

执行该程序：

$go run print.go
hello cgo
</code></pre>
<h4>5、其他小改动</h4>
<ul>
<li>经过Go 1.5和Go 1.6实验的<a href="http://tonybai.com/2015/07/31/understand-go15-vendor/">go vendor机制</a>在Go 1.7中将正式去掉GO15VENDOREXPERIMENT环境变量开关，将vendor作为默认机制。</li>
<li>go get支持git.openstack.org导入路径。</li>
<li>go tool dist list命令将打印所有go支持的系统和硬件架构，在我的机器上输出结果如下：</li>
</ul>
<pre><code>$go tool dist list
android/386
android/amd64
android/arm
android/arm64
darwin/386
darwin/amd64
darwin/arm
darwin/arm64
dragonfly/amd64
freebsd/386
freebsd/amd64
freebsd/arm
linux/386
linux/amd64
linux/arm
linux/arm64
linux/mips64
linux/mips64le
linux/ppc64
linux/ppc64le
linux/s390x
nacl/386
nacl/amd64p32
nacl/arm
netbsd/386
netbsd/amd64
netbsd/arm
openbsd/386
openbsd/amd64
openbsd/arm
plan9/386
plan9/amd64
plan9/arm
solaris/amd64
windows/386
windows/amd64
</code></pre>
<h3>三、标准库</h3>
<h4>1、支持subtests和sub-benchmarks</h4>
<p>表驱动测试是golang内置testing框架的一个最佳实践，基于表驱动测试的思路，Go 1.7又进一步完善了testing的组织体系，增加了<a href="https://github.com/golang/proposal/blob/master/design/12166-subtests.md">subtests和sub-benchmarks</a>。目的是为了实现以下几个Features：</p>
<ul>
<li>通过外部command line(go test &#8211;run=xx)可以从一个table中选择某个test或benchmark，用于调试等目的；</li>
<li>简化编写一组相似的benchmarks；</li>
<li>在subtest中使用Fail系列方法(如FailNow，SkipNow等)；</li>
<li>基于外部或动态表创建subtests；</li>
<li>更细粒度的setup和teardown控制，而不仅仅是TestMain提供的；</li>
<li>更多的并行控制；</li>
<li>与顶层函数相比，对于test和benchmark来说，subtests和sub-benchmark代码更clean。</li>
</ul>
<p>下面是一个基于subtests文档中demo改编的例子：</p>
<p>传统的Go 表驱动测试就像下面代码中TestSumInOldWay一样：</p>
<pre><code>// go17-examples/stdlib/subtest/foo_test.go

package foo

import (
    "fmt"
    "testing"
)

var tests = []struct {
    A, B int
    Sum  int
}{
    {1, 2, 3},
    {1, 1, 2},
    {2, 1, 3},
}

func TestSumInOldWay(t *testing.T) {
    for _, tc := range tests {
        if got := tc.A + tc.B; got != tc.Sum {
            t.Errorf("%d + %d = %d; want %d", tc.A, tc.B, got, tc.Sum)
        }
    }
}

</code></pre>
<p>对于这种传统的表驱动测试，我们在控制粒度上仅能在顶层测试方法层面，即TestSumInOldWay这个层面：</p>
<pre><code>$go test --run=TestSumInOldWay
PASS
ok      github.com/bigwhite/experiments/go17-examples/stdlib/subtest    0.008s
</code></pre>
<p>同时为了在case fail时更容易辨别到底是哪组数据导致的问题，Errorf输出时要带上一些测试数据的信息，比如上面代码中的：”%d+%d=%d; want %d”。</p>
<p>若通过subtests来实现，我们可以将控制粒度细化到subtest层面。并且由于subtest自身具有subtest name唯一性，无需在Error中带上那组测试数据的信息：</p>
<pre><code>// go17-examples/stdlib/subtest/foo_test.go

func assertEqual(A, B, expect int, t *testing.T) {
    if got := A + B; got != expect {
        t.Errorf("got %d; want %d", got, expect)
    }
}

func TestSumSubTest(t *testing.T) {
    //setup code ... ...

    for i, tc := range tests {
        t.Run("A=1", func(t *testing.T) {
            if tc.A != 1 {
                t.Skip(i)
            }
            assertEqual(tc.A, tc.B, tc.Sum, t)
        })

        t.Run("A=2", func(t *testing.T) {
            if tc.A != 2 {
                t.Skip(i)
            }
            assertEqual(tc.A, tc.B, tc.Sum, t)
        })
    }

    //teardown code ... ...
}
</code></pre>
<p>我们故意将tests数组中的第三组测试数据的Sum值修改错误，这样便于对比测试结果：</p>
<pre><code>var tests = []struct {
    A, B int
    Sum  int
}{
    {1, 2, 3},
    {1, 1, 2},
    {2, 1, 4},
}
</code></pre>
<p>执行TestSumSubTest：</p>
<pre><code>$go test --run=TestSumSubTest
--- FAIL: TestSumSubTest (0.00s)
    --- FAIL: TestSumSubTest/A=2#02 (0.00s)
        foo_test.go:19: got 3; want 4
FAIL
exit status 1
FAIL    github.com/bigwhite/experiments/go17-examples/stdlib/subtest    0.007s

</code></pre>
<p>分别执行”A=1&#8243;和”A=2&#8243;的两个subtest：</p>
<pre><code>$go test --run=TestSumSubTest/A=1
PASS
ok      github.com/bigwhite/experiments/go17-examples/stdlib/subtest    0.007s

$go test --run=TestSumSubTest/A=2
--- FAIL: TestSumSubTest (0.00s)
    --- FAIL: TestSumSubTest/A=2#02 (0.00s)
        foo_test.go:19: got 3; want 4
FAIL
exit status 1
FAIL    github.com/bigwhite/experiments/go17-examples/stdlib/subtest    0.007s

</code></pre>
<p>测试的结果验证了前面说到的两点：<br />
1、subtest的输出自带唯一标识，比如：“FAIL: TestSumSubTest/A=2#02 (0.00s)”<br />
2、我们可以将控制粒度细化到subtest的层面。</p>
<p>从代码的形态上来看，subtest支持对测试数据进行分组编排，比如上面的测试就将TestSum分为A=1和A=2两组，以便于分别单独控制和结果对比。</p>
<p>另外由于控制粒度支持subtest层，setup和teardown也不再局限尽在TestMain级别了，开发者可以在每个top-level test function中，为其中的subtest加入setup和teardown，大体模式如下：</p>
<pre><code>func TestFoo(t *testing.T) {
    //setup code ... ...

    //subtests... ...

    //teardown code ... ...
}
</code></pre>
<p>Go 1.7中的subtest同样支持并发执行：</p>
<pre><code>func TestSumSubTestInParalell(t *testing.T) {
    t.Run("blockgroup", func(t *testing.T) {
        for _, tc := range tests {
            tc := tc
            t.Run(fmt.Sprint(tc.A, "+", tc.B), func(t *testing.T) {
                t.Parallel()
                assertEqual(tc.A, tc.B, tc.Sum, t)
            })
        }
    })
    //teardown code
}
</code></pre>
<p>这里嵌套了两层Subtest，”blockgroup”子测试里面的三个子测试是相互并行(Paralell)执行，直到这三个子测试执行完毕，blockgroup子测试的Run才会返回。而TestSumSubTestInParalell与foo_test.go中的其他并行测试function（如果有的话）的执行是顺序的。</p>
<p>sub-benchmark在形式和用法上与subtest类似，这里不赘述了。</p>
<h4>2、Context包</h4>
<p>Go 1.7将原来的golang.org/x/net/context包挪入了标准库中，放在$GOROOT/src/context下面，这显然是由于context模式用途广泛，Go core team响应了社区的声音，同时这也是Go core team自身的需要。Std lib中net、net/http、os/exec都用到了context。关于Context的详细说明，没有哪个比Go team的一篇”<a href="https://blog.golang.org/context">Go Concurrent Patterns：Context</a>“更好了。</p>
<h3>四、其他改动</h3>
<p>Runtime这块普通开发者很少使用，一般都是Go core team才会用到。值得注意的是Go 1.7增加了一个runtime.Error（接口），所有runtime引起的panic，其panic value既实现了标准error接口，也实现了runtime.Error接口。</p>
<p>Golang的GC在1.7版本中继续由<a href="https://github.com/aclements">Austin Clements</a>和Rick Hudson进行打磨和优化。</p>
<p>Go 1.7编译的程序的执行效率由于SSA的引入和GC的优化，整体上会平均提升5%-35%（在x86-64平台上）。一些标准库的包得到了显著的优化，比如：crypto/sha1, crypto/sha256, encoding/binary, fmt, hash/adler32, hash/crc32, hash/crc64, image/color, math/big, strconv, strings, unicode, 和unicode/utf16，性能提升在10%以上。</p>
<p>Go 1.7还增加了对<a href="http://tonybai.com/2015/03/09/understanding-import-packages/">使用二进制包（非源码）构建程序</a>的实验性支持（出于一些对商业软件发布形态的考虑），但Go core team显然是不情愿在这方面走太远，不承诺对此进行完整的工具链支持。</p>
<p>标准库中其他的一些细微改动，大家尽可以参考Go 1.7 release notes。</p>
<p>本文涉及到的example代码在<a href="https://github.com/bigwhite/experiments/tree/master/go17-examples">这里</a>可以下载到。</p>
<p style='text-align:left'>&copy; 2016, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2016/06/21/some-changes-in-go-1-7/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
	</channel>
</rss>
