标签 汇编 下的文章

通过实例理解Go内联优化

本文永久链接 – https://tonybai.com/2022/10/17/understand-go-inlining-optimisations-by-example


移动互联网时代,直面C端用户的业务系统规模一般都很庞大,系统消耗的机器资源也很可观,系统使用的CPU核数、内存都是在消耗公司的真金白银。在服务水平不下降的前提下尽量降低单服务实例的资源消耗,即我们俗称的“少吃草多产奶”,一直是各个公司经营人员的目标,有些公司每降低1%的CPU核数使用,每年都能节省几十万的开销。

在编程语言选择不变的情况下,要想持续降低服务资源消耗,一方面要靠开发人员对代码性能持续地打磨,另一方面依靠编程语言编译器在编译优化方面提升带来的效果则更为自然和直接。不过,这两方面也是相辅相成的,开发人员如果能对编译器的优化场景和手段理解更为透彻的话,就能写出对编译优化更为友好的代码,从而获得更好的性能优化效果。

Go核心团队在Go编译器优化方面一直在持续投入并取得了不俗的效果,虽然和老牌的GCCllvm的代码优化功力相比还有不小的空间。近期看到的一篇文章“字节大规模微服务语言发展之路”中也有提到:字节内部通过修改Go编译器的内联优化(收益最大的改动),从而让字节内部服务的Go代码获得了更多的优化机会,实现了线上服务10-20%的性能提升以及内存资源使用的下降,节约了大概了十几万个核。

看到这么明显的效果,想必各位读者都很想了解一下Go编译器的内联优化了。别急,在这一篇文章中,我就和大家一起来学习和理解一下Go编译器的内联优化。希望通过本文的学习,能让大家掌握如下内容:

  • 什么是内联优化以及它的好处是什么
  • 内联优化在Go编译过程中所处的环节和实现原理
  • 哪些代码能被内联优化,哪些还不能被内联优化
  • 如何控制Go编译器的内联优化
  • 内联优化的弊端有哪些

下面我们就先来了解一下什么是内联优化。


1. 什么是编译器的内联优化

内联(inlining)是编程语言编译器常用的优化手段,其优化的对象为函数,也称为函数内联。如果某函数F支持内联,则意味着编译器可以用F的函数体/函数定义替换掉对函数F进行调用的代码,以消除函数调用带来的额外开销,这个过程如下图所示:

我们知道Go从1.17版本才改为基于寄存器的调用规约,之前的版本一直是基于栈传递参数与返回值,函数调用的开销更大,在这样的情况下,内联优化的效果也就更为显著。

除此之外,内联优化之后,编译器的优化决策可以不局限在每个单独的函数(比如上图中的函数g)上下文中做出,而是可以在函数调用链上做出了(内联替换后,代码变得更平(flat)了)。比如上图中对g后续执行的优化将不局限在g上下文,由于f的内联,让编译器可以在g->f这个调用链的上下文上决策后续要执行的优化手段,即内联让编译器可以看得更广更远了

我们来看一个简单的例子:

// github.com/bigwhite/experiments/tree/master/inlining-optimisations/add/add.go

//go:noinline
func add(a, b int) int {
    return a + b
}

func main() {
    var a, b = 5, 6
    c := add(a, b)
    println(c)
}

这个例子中,我们的关注点是add函数,在add函数定义上方,我们用//go:noinline告知编译器对add函数关闭inline,我们构建该程序,得到可执行文件:add-without-inline;然后去掉//go:noinline这一行,再进行一次程序构建,得到可执行文件add,我们用lensm工具以图形化的方式查看一下这两个可执行文件的汇编代码,并做以下对比:

我们看到:非内联优化的版本add-without-inline如我们预期那样,在main函数中通过CALL指令调用了add函数;但在内联优化版本中,add函数的函数体并没有替换掉main函数中调用add函数位置上的代码,main函数调用add函数的位置上对应的是一个NOPL的汇编指令,这是一条不执行任何操作的空指令。那么add函数实现的汇编代码哪去了呢?

// add函数实现的汇编代码
ADDQ BX, AX
RET

结论是:被优化掉了!这就是前面说的内联为后续的优化提供更多的机会。add函数调用被替换为add函数的实现后,Go编译器直接可以确定调用结果为11,于是连加法运算都省略了,直接将add函数的结果换成了一个常数11(0xb),然后直接将常量11传给了println内置函数(MOVL 0xb, AX)。

通过一个简单的benchmark,也可以看出内联与非内联add的性能差异:

// 开启内联优化
$go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/experiments/inlining-optimisations/add
BenchmarkAdd-8      1000000000           0.2720 ns/op
PASS
ok      github.com/bigwhite/experiments/inlining-optimisations/add  0.307s

// 关闭内联优化
$go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/experiments/inlining-optimisations/add
BenchmarkAdd-8      818820634            1.357 ns/op
PASS
ok      github.com/bigwhite/experiments/inlining-optimisations/add  1.268s

我们看到:内联版本是非内联版本性能的5倍左右

到这里,很多朋友可能会问:既然内联优化的效果这么好,为什么不将Go程序内部的所有函数都内联了,这样整个Go程序就变成了一个大函数,中间再没有任何函数调用了,这样性能是不是可以变得更高呢?虽然理论上可能是这种情况,但内联优化不是没有开销的,并且针对不同复杂性的函数,内联的效果也是不同的。下面我就和大家一起先来看看内联优化的开销!

2. 内联优化的“开销”

在真正理解内联优化的开销之前,我们先来看看内联优化在Go编译过程中的位置,即处于哪个环节。

Go编译过程

和所有静态语言编译器一样,Go编译过程大致分为如下几个阶段:

  • 编译前端

Go团队并没有刻意将Go编译过程分为我们常识中的前后端,如果非要这么分,源码分析(包括词法和语法分析)、类型检查和中间表示(Intermediate Representation)构建可以归为逻辑上的编译前端,后面的其余环节都划归为后端。

源码分析形成抽象语法树,然后是基于抽象语法树的类型检查,待类型检查通过后,Go编译器将AST转换为一个与目标平台无关的中间代码表示。

目前Go有两种IR实现方式,一种是irgen(又名”-G=3″或是”noder2″),irgen是从Go 1.18版本开始使用的实现(这也是一种类似AST的结构);另外一种是unified IR,在Go 1.19版本中,我们可以使用GOEXPERIMENT=unified启用它,根据最新消息,unified IR将在Go 1.20版本落地。

注:现代编程语言编译过程多数会多次生成中间代码(IR),比如下面要提到的静态单赋值形式(SSA)也是一种IR形式。针对每种IR,编译器都会有一些优化动作:


图:编译优化过程(图来自https://www.slideserve.com/heidi-farmer/ssa-static-single-assignment-form)

  • 编译后端

编译后端的第一步是一个被Go团队称为中端(middle end)的环节,在这个环节中,Go编译器将基于上面的中间代码进行多轮(pass)的优化,包括死代码消除、内联优化、方法调用实体化(devirtualization)和逃逸分析等。

注:devirtualization是指将通过接口变量调用的方法转换为接口的动态类型变量直接调用该方法,消除通过接口进行方法表查找的过程。

接下来是中间代码遍历(walk),这个环节是基于上述IR表示的最后一轮优化,它主要是将复杂的语句分解成单独的、更简单的语句,引入临时变量并重新评估执行顺序,同时在这个环节,它还会将一些高层次的Go结构转换为更底层、更基础的操作结构,比如将switch语句转换为二分查找或跳表,将对map和channel的操作替换为运行时的调用(如mapaccess)等。

接下来是编译后端的最后两个环节,首先是将IR转换为SSA(静态单一赋值)形式,并再次基于SSA做多轮优化,最后针对目标架构,基于SSA的最终形式生成机器相关的汇编指令,然后交给汇编器生成可重定位的目标机器码。

注: 编译器(go compiler)产生的可重定位的目标机器码最终提供给链接器(linker)生成可执行文件。

我们看到Go内联发生在中端环节,是基于IR中间代码的一种优化手段,在IR层面上实现函数是否可内联的决策,以及对可内联函数在其调用处的函数体替换

一旦了解了Go内联所处环节,我们就能大致判断出Go内联优化带来的开销了。

Go内联优化的开销

我们用一个实例来看一下Go内联优化的开销。reviewdog是一个纯Go实现的支持github、gitlab等主流代码托管平台的代码评审工具,它的规模大约有12k行(使用loccount统计):

// reviewdog代码行数统计结果:

$loccount .
all          SLOC=14903   (100.00%) LLOC=4613    in 141 files
Go           SLOC=12456   (83.58%)  LLOC=4584    in 106 files
... ...

我们在开启内联优化和关闭内联优化的情况下分别对reviewdog进行构建,采集其构建时间与构建出的二进制文件的size,结果如下:

// 开启内联优化(默认)
$time go build -o reviewdog-inline -a github.com/reviewdog/reviewdog/cmd/reviewdog
go build -o reviewdog-inline -a github.com/reviewdog/reviewdog/cmd/reviewdog  53.87s user 9.55s system 567% cpu 11.181 total

// 关闭内联优化
$time go build -o reviewdog-noinline -gcflags=all="-l" -a github.com/reviewdog/reviewdog/cmd/reviewdog
go build -o reviewdog-noinline -gcflags=all="-l" -a   43.25s user 8.09s system 566% cpu 9.069 total

$ ls -l
-rwxrwxr-x  1 tonybai tonybai 23080429 Oct 13 12:05 reviewdog-inline*
-rwxrwxr-x  1 tonybai tonybai 20745006 Oct 13 12:04 reviewdog-noinline*
... ...

我们看到开启内联优化的版本,其编译消耗时间比关闭内联优化版本的编译时间多出24%左右,并且生成的二进制文件size要大出11%左右 – 这就是内联优化带来的开销!即会拖慢编译器并导致生成的二进制文件size变大。

注:hello world级别的程序是否开启内联优化大多数情况是看不出来太多差异的,无论是编译时间,还是二进制文件的size。

由于我们知道了内联优化所处的环节,因此这种开销就可以很好地给予解释:根据内联优化的定义,一旦某个函数被决策为可内联,那么程序中所有调用该函数的位置的代码就会被替换为该函数的实现,从而消除掉函数调用带来的运行时开销,同时这也导致了在IR(中间代码)层面出现一定的代码“膨胀”。前面也说过,代码膨胀后的“副作用”是编译器可以以更广更远的视角看待代码,从而可能实施的优化手段会更多。可实施的优化轮次越多,编译器执行的就越慢,这进一步增加了编译器的耗时;同时膨胀的代码让编译器需要在后面环节处理和生成更多代码,不仅增加耗时,还增加了最终二进制文件的size。

Go向来对编译速度和binary size较为敏感,所以Go采用了相对保守的内联优化策略。那么到底Go编译器是如何决策一个函数是否可以内联呢?下面我们就来简单看看Go编译器是如何决策哪些函数可以实施内联优化的。

3. 函数内联的决策原理

前面说过,内联优化是编译中端多轮(pass)优化中的一轮,因此它的逻辑相对独立,它基于IR代码进行,改变的也是IR代码。我们可以在Go源码的$GOROOT/src/cmd/compile/internal/inline/inl.go中找到Go编译器进行内联优化的主要代码。

注:Go编译器内联优化部分的代码的位置和逻辑在以前的版本以及在未来的版本中可能有变化,目前本文提到的是代码是Go 1.19.1中的源码。

内联优化IR优化环节会做两件事:第一遍历IR中所有函数,通过CanInline判断某个函数是否可以内联,对于可内联的函数,保存相应信息,比如函数body等,供后续做内联函数替换使用;第二呢,则是对函数中调用的所有内联函数进行替换。 我们重点关注CanInline,即Go编译器究竟是如何决策一个函数是否可以内联的

内联优化过程的“驱动逻辑”在$GOROOT/src/cmd/compile/internal/gc/main.go的Main函数中:

// $GOROOT/src/cmd/compile/internal/gc/main.go
func Main(archInit func(*ssagen.ArchInfo)) {
    base.Timer.Start("fe", "init")

    defer handlePanic()

    archInit(&ssagen.Arch)
    ... ...

    // Enable inlining (after RecordFlags, to avoid recording the rewritten -l).  For now:
    //  default: inlining on.  (Flag.LowerL == 1)
    //  -l: inlining off  (Flag.LowerL == 0)
    //  -l=2, -l=3: inlining on again, with extra debugging (Flag.LowerL > 1)
    if base.Flag.LowerL <= 1 {
        base.Flag.LowerL = 1 - base.Flag.LowerL
    }
    ... ...

    // Inlining
    base.Timer.Start("fe", "inlining")
    if base.Flag.LowerL != 0 {
        inline.InlinePackage()
    }
    noder.MakeWrappers(typecheck.Target) // must happen after inlining
    ... ...
}

从代码中我们看到:如果没有全局关闭内联优化(base.Flag.LowerL != 0),那么Main就会调用inline包的InlinePackage函数执行内联优化。InlinePackage的代码如下:

// $GOROOT/src/cmd/compile/internal/inline/inl.go
func InlinePackage() {
    ir.VisitFuncsBottomUp(typecheck.Target.Decls, func(list []*ir.Func, recursive bool) {
        numfns := numNonClosures(list)
        for _, n := range list {
            if !recursive || numfns > 1 {
                // We allow inlining if there is no
                // recursion, or the recursion cycle is
                // across more than one function.
                CanInline(n)
            } else {
                if base.Flag.LowerM > 1 {
                    fmt.Printf("%v: cannot inline %v: recursive\n", ir.Line(n), n.Nname)
                }
            }
            InlineCalls(n)
        }
    })
}

InlinePackage遍历每个顶层声明的函数,对于非递归函数或递归前跨越一个以上函数的递归函数,通过调用CanInline函数判断其是否可以内联,无论是否可以内联,接下来都会调用InlineCalls函数对其函数定义中调用的内联函数进行替换。

VisitFuncsBottomUp是根据函数调用图从底向上遍历的,这样可以保证每次在调用analyze时,列表中的每个函数都只调用列表中的其他函数,或者是在之前的调用中已经analyze过(在这里就是被内联函数体替换过)的函数。

什么是递归前跨越一个以上函数的递归函数,看下面这个例子就懂了:

// github.com/bigwhite/experiments/tree/master/inlining-optimisations/recursion/recursion1.go
func main() {
    f(100)
}

func f(x int) {
    if x < 0 {
        return
    }
    g(x - 1)
}
func g(x int) {
    h(x - 1)
}
func h(x int) {
    f(x - 1)
}

f是一个递归函数,但并非自己调用自己,而是通过g -> h这个函数链最终又调回自己,而这个函数链长度>1,所以f是可以内联的:

$go build -gcflags '-m=2'  recursion1.go
./recursion1.go:7:6: can inline f with cost 67 as: func(int) { if x < 0 { return  }; g(x - 1) }

我们继续看CanInline函数。CanInline函数有100多行代码,其主要逻辑分为三个部分。

首先是对一些//go:xxx指示符(directive)的判定,当该函数包含下面指示符时,则该函数不能内联:

  • //go:noinline
  • //go:norace或构建命令行中包含-race选项
  • //go:nocheckptr
  • //go:cgo_unsafe_args
  • //go:uintptrkeepalive
  • //go:uintptrescapes
  • … …

其次会对该函数的状态做判定,比如如果函数体为空,则不能内联;如果未做类型检查(typecheck),则不能内联等。

最后调用visitor.tooHairy对函数的复杂性做判定。判定方法就是先为此次遍历(visitor)设置一个初始最大预算(budget),这个初始最大预算值为一个常量(inlineMaxBudget),目前其值为80:

// $GOROOT/src/cmd/compile/internal/inline/inl.go
const (
    inlineMaxBudget       = 80
)

然后在visitor.tooHairy函数中遍历该函数实现中的各个语法元素:

// $GOROOT/src/cmd/compile/internal/inline/inl.go
func CanInline(fn *ir.Func) {
    ... ...
    visitor := hairyVisitor{
        budget:        inlineMaxBudget,
        extraCallCost: cc,
    }
    if visitor.tooHairy(fn) {
        reason = visitor.reason
        return
    }
    ... ...
}

不同元素对预算的消耗都有不同,比如调用一次append,visitor预算值就要减去inlineExtraAppendCost,再比如如果该函数是中间函数(而非叶子函数),那么visitor预算值也要减去v.extraCallCost,即57。就这样一路下来,如果预算被用光,即v.budget < 0,则说明这个函数过于复杂,不能被内联;相反,如果一路下来,预算依然有,那么说明这个函数相对简单,可以被内联优化。

注:为什么inlineExtraCallCost的值是57?这是一个经验值,是通过一个benchmark得出来的

一旦确定可以被内联,那么Go编译器就会将一些信息保存下来,保存到IR中该函数节点的Inl字段中:

// $GOROOT/src/cmd/compile/internal/inline/inl.go
func CanInline(fn *ir.Func) {
    ... ...
    n.Func.Inl = &ir.Inline{
        Cost: inlineMaxBudget - visitor.budget,
        Dcl:  pruneUnusedAutos(n.Defn.(*ir.Func).Dcl, &visitor),
        Body: inlcopylist(fn.Body),

        CanDelayResults: canDelayResults(fn),
    }
    ... ...
}

Go编译器设置budget值为80,显然是不想让过于复杂的函数被内联优化,这是为什么呢?主要是权衡内联优化带来的收益与其开销。让更复杂的函数内联,开销会增大,但收益却可能不会有明显增加,即所谓的“投入产出比”不足。

从上面的原理描述可知,对那些size不大(复杂性较低)、被反复调用的函数施以内联的效果可能更好。而对于那些过于复杂的函数,函数调用的开销占其执行开销的比重已经十分小了,甚至可忽略不计,这样内联效果就会较差。

很多人会说:内联后不是还有更多编译器优化机会么?问题在于究竟是否有优化机会以及会实施哪些更多的优化,这是无法预测的事情。

4. 对Go编译器的内联优化进行干预

最后我们再来看看如何对Go编译器的内联优化进行干预。Go编译器默认是开启全局内联优化的,并按照上面inl.go中CanInline的决策流程来确定一个函数是否可以内联。

不过Go也给了我们控制内联的一些手段,比如我们可以在某个函数上显式告知编译器不要对该函数进行内联,我们以上面示例中的add.go为例:

//go:noinline
func add(a, b int) int {
    return a + b
}

通过//go:noinline指示符,我们可以禁止对add的内联:

$go build -gcflags '-m=2' add.go
./add.go:4:6: cannot inline add: marked go:noinline

注:禁止某个函数内联不会影响InlineCalls函数对该函数内部调用的内联函数的函数体替换。

我们也可以在更大范围关闭内联优化,借助-gcflags ‘-l’选项,我们可以在全局范围关闭优化,即Flag.LowerL == 0,Go编译器的InlinePackage将不会执行。

我们以前面提到过的reviewdog来验证一下:

// 默认开启内联
$go build -o reviewdog-inline github.com/reviewdog/reviewdog/cmd/reviewdog

// 关闭内联
$go build -o reviewdog-noinline -gcflags '-l' github.com/reviewdog/reviewdog/cmd/reviewdog

之后我们查看一下生成的binary文件size:

$ls -l |grep reviewdog
-rwxrwxr-x  1 tonybai tonybai 23080346 Oct 13 20:28 reviewdog-inline*
-rwxrwxr-x  1 tonybai tonybai 23087867 Oct 13 20:28 reviewdog-noinline*

我们发现noinline版本居然比inline版本的size还要略大!这是为什么呢?这与-gcflags参数的传递方式有关,如果只是像上面命令行那样传入-gcflags ‘-l’,关闭内联仅适用于当前package,即cmd/reviewdog,而该package的依赖等都不会受到影响。-gcflags支持pattern匹配:

-gcflags '[pattern=]arg list'
    arguments to pass on each go tool compile invocation.

我们可以通过设置不同pattern来匹配更多包,比如all这个模式就可以包括当前包的所有依赖,我们再来试试:

$go build -o reviewdog-noinline-all -gcflags='all=-l' github.com/reviewdog/reviewdog/cmd/reviewdog
$ls -l |grep reviewdog
-rw-rw-r--  1 tonybai tonybai     3154 Sep  2 10:56 reviewdog.go
-rwxrwxr-x  1 tonybai tonybai 23080346 Oct 13 20:28 reviewdog-inline*
-rwxrwxr-x  1 tonybai tonybai 23087867 Oct 13 20:28 reviewdog-noinline*
-rwxrwxr-x  1 tonybai tonybai 20745006 Oct 13 20:30 reviewdog-noinline-all*

这回我们看到reviewdog-noinline-all要比reviewdog-inline的size小了不少,这是因为all将reviewdog依赖的各个包的内联也都关闭了。

5. 小结

在这篇文章中,我带大家一起了解了Go内联相关的知识,包括内联的概念、内联的作用、内联优化的“开销”以及Go编译器进行函数内联决策的原理,最后我还给出控制Go编译器内联优化的手段。

内联优化是一种重要的优化手段,使用得当将会给你的系统带来不小的性能改善。Go编译器组也在对Go内联优化做持续改善,从之前仅支持叶子函数的内联,到现在支持非叶子节点函数的内联,相信Go开发者在未来还会继续得到这方面带来的性能红利。

本文涉及的源码可以在这里下载。

6. 参考资料

  • Introduction to the Go compiler – https://go.dev/src/cmd/compile/README
  • Proposal: Mid-stack inlining in the Go compiler – https://github.com/golang/proposal/blob/master/design/19348-midstack-inlining.md
  • Mid-stack inlining in the Go compiler – https://golang.org/s/go19inliningtalk
  • Inlining optimisations in Go – https://dave.cheney.net/2020/04/25/inlining-optimisations-in-go
  • Mid-stack inlining in Go – https://dave.cheney.net/2020/05/02/mid-stack-inlining-in-go
  • cmd/compile: relax recursive restriction while inlining – https://github.com/golang/go/issues/29737

“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2022年,Gopher部落全面改版,将持续分享Go语言与Go应用领域的知识、技巧与实践,并增加诸多互动形式。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

GoCN社区Go读书会第二期:《Go语言精进之路》

本文永久链接 – https://tonybai.com/2022/07/07/gocn-community-go-book-club-issue2-go-programming-from-beginner-to-master

本文是2022年6月26日我在GoCN社区的Go读书会第二期《Go语言精进之路》直播的文字稿。本文对直播的内容做了重新整理与修订,供喜欢阅读文字的朋友们在收看直播后的揣摩和参考。视频控的童鞋可以关注GoCN公众号和视频号看剪辑后的视频,也可以在B站GopherChina专区下收看视频回放(https://www.bilibili.com/video/BV1p94y1R7jg)。


大家晚上好,我叫白明,是《Go语言精进之路》一书的作者,也是tonybai.com的博主,很荣幸今天参加GoCN社区Go读书会第二期,分享一下我个人在写书和读书方面的经验和体会。

今天的分享包括三方面内容:

  • 写书的历程。一些Gopher可能比较好奇,这么厚的一套书是怎么写出来的,今天就和大家聊一聊。
  • 《Go语言精进之路》导读。主要是把这本书的整体构思与大家聊聊,希望通过这个导读帮助读者更好地阅读和理解这套书。
  • 我个人的读书方法与经验的简要分享。

首先和大家分享一下写书的历程。

一. 写书的历程

1. 程序员的“小目标”与写书三要素

今天收看直播的童鞋都是有追求的技术人员,可能心底都有写一本属于自己的书的小目标。这样可以把自己学习到的知识、技能和经验以比较系统的方式输出给其他人,可以帮助其他人快速学习和掌握本领域的知识、技能和经验。

当然写书还有其他好处,比如:提升名气、更容易混技术圈子、可能给你带来更好的职业发展机会,当然也会给你带来一些额外的副业收入,至于多少,还要看书籍的口碑与销量。

那怎么才能写书呢?作为“过来人”,我总结了三个要素,也是三个条件。

第一个要素是能力

这个很容易理解。以Go为例,如果你没有在Go语言方面的知识、技能的沉淀,没有对Go语言方方面面的较为深入的理解,你很难写出一本口碑很好的书籍。尤其是那种有原创性、独到见解的著书。而不是对前人资料做系统整理摘抄的编书。编书更常见于教材、字典等。显然著书对作者水平的要求更高。

第二个要素是意愿

写过书的同学都有体会,写书是一件辛苦活。需要你在正式工作之余付出大量业余时间伏案创作。并且对于小众技术类书籍来说,写书能带来的金钱上的收益和你付出的时长和精力不成正比。就这个问题,我曾与机械工业出版社的营销编辑老师聊过,得到的信息是:Go技术书籍的市场与Java、Python还没法比,即便是像Go语言圣经《Go程序设计语言》的销量也没法与Java、Python的头部书籍销量相比。

第三个要素是机会

记得小时候十分羡慕那些能出书的人,觉得都是大神级的人物。不过那个时候出书的确很难,机会应该很少,你要不是在学术圈里混很难出书。如今就容易地多了,渠道也多了。每年出版社都有自己的出版计划,各个出版社的编辑老师也在根据计划在各种自媒体上、技术圈子中寻觅匹配的技术作者。

如果你有自己的思路,也可以整理出大纲,并通过某种方式联系到出版社老师,如果匹配就可以出。

另外国外流行电子自助出版,这也给很多技术作者很好的出版机会。比如国内作者老貘写的Go 101系列就是在亚马逊leanpub上做的自助出版,效果还不错。

以上就是我总结的出书的三个要素,一旦集齐这三个要素呢,出书实际就是自然而然的一件事了。以我为例。

从能力方面来说呢,我大约从2011年开始接触和学习Go语言,算是国内较早的一批Go语言接纳者。Go语言2012年才发布1.0版本,因此那时我接触的Go时还是r60版本,还不是正式的1.0版本。从那时起就一直在跟踪Go演化,日常写一些Go项目的小程序。

Go 1.5实现自举并大幅降低GC延迟,我于是开始在一些生产环境使用Go,并逐渐将知识和经验做了沉淀,在自己的博客上不断做着Go相关内容的输出,反响也不错。

随着输出Go内容的增多,我发现以博客的形式输出,内容组织零散,于是我第一次有了将自己的Go知识系统整理并输出的意愿和想法。

我在实践Go的过程中收到很多Go初学者的提问:Go入门容易,但精进难,怎么才能像Go开发团队那样写出符合Go思维和语言惯例的高质量代码呢?这个问题引发了我的思考。在2017年GopherChina大会我以《go coding in go way》为主题,以演讲的形式尝试回答这个问题,但鉴于演讲的时长有限,很多内容没能展开,效果不甚理想。这进一步增强了我通过书籍的形式系统解答这个问题的意愿。

而当时我家大宝已经长大了,我也希望通过写书这个行动身体力行地给孩子树立一个正面的榜样。中国古语有云:言传身教,我也想践行一下。

机会就这样自然而然的来了!2018年初,机械工业出版社副总编杨福川老师在微信联系到我,和我探讨一下是否可以写一本类似于“Effective Go”的书,当时机械工业出版社华章出版社策划了Effective XXX(编写高质量XXX)系列图书,当时已经出版了C、Python等语言版本的书籍,还差Go语言的。我的出书意愿与出版社的需求甚是匹配,于是我答应的杨老师的要求,成为了这套丛书的Go版本的作者。

2. 写书的过程

我是2018下旬开始真正动笔的。

真正开始码字的时候,我才意识到,写书真不容易,要写出高质量书稿,的确需付出大量时间和汗水。每天晚上、早上都在构思、码字、写代码示例、画插图,睡眠时间很少。记得当时每周末都在奋笔疾书,陪伴家人尤其是孩子的时间很少。

另外我这个人还习惯于把一个知识点讲细讲透,这样每一节的篇幅都不小。因此,写作进展是很缓慢的,就这样,进度一再延期。好在编辑老师比较nice,考虑到书稿质量,没有狠狠催进度。

2020年11月末,我正式向出版社交了初稿,记得初稿有66条,近40w字。

又经过一年的排期、编辑、修订、排版,2021年12月下旬正式出版。

2022年1月《Go语言精进之路》正式上架到各个渠道货架。

到今天为止,出版了近六个月,这本书收获了还不错的口碑,在各个平台上的口碑都在8分以上(注:口碑分数还在动态变化,下图仅为当时的快照,不代表如今的分数)。


能获得大家的认可,让我很是欣慰,觉得写书过程付出的辛苦没有白费。

以上就是我的写书历程。总的来说一句话:写书不易,写高质量的书更难

接下来我来进行一下《Go语言精进之路》一书的导读。

二. 《Go语言精进之路》导读

也许是“用力过猛”,《Go语言精进之路》一书写的太厚了,无法装订为一册。编辑老师建议装订为两册,即1、2册。很多同学好奇为什么不是上下册而是1、2册,这里是编辑老师的“高瞻远瞩”,目的是为后续可能的“续写”(比如第3册)留足空间,毕竟Go语言还在快速演进,目前的版本还不包含像泛型这样的新语法。不过,目前第3册还尚未列入计划。

本套书共分为10个部分,66个主题。第一册包含了前7个部分,后3部分在第二册中。

1. 整体写作思路

整套书围绕着两个前后关联的思路循序展开。

第一个思路我叫它:精进之路,思维先行

第二个思路称为:践行哲学,遵循惯例,认清本质,理解原理

我们先来看看第一个思路。

2. 精进之路,思维先行

收看直播的童鞋都不止学过一门编程语言。大家可能都有过这样的经历:你已经精通A语言,然后在学习B语言的时候用A语言的思维去写B代码,你会觉得写出的B代码很别扭,写出的代码总是感觉不是很地道,总觉得不是那种高质量的B语言代码。

其实,不仅学习编程语言是这样,学自然语言也是一样。最典型的一个例子,大家都学过十几年的英语,但毕业后能用地道的英语表达自己观点的人却不多,为什么呢?那就是我们总用中文的思维方式去组织英语的句子,去说英语,这样再怎么努力也很难上一个层次。

其实,很多语言大师早就意识到了这一点。下面是我收集的这些大师的关于语言与思维的论点,这里和大家分享一下:

“语言决定思维方式” – 萨丕尔假说

“我的语言之局限,即我的世界之局限” – 路德维希·维特根斯坦,语言哲学的奠基人

“不能改变你思维方式的语言,不值得学习” – Alan Perlis(首届ACM图灵奖得主)

我们看到:无论是自然语言界的大师,还是IT界的大佬,他们的观点异曲同工。总之一句话:语言要精进,思维要先行

3. Part1:进入Go语言编程思维导引

正是因为意识到语言与思维的紧密关系,我在书的第一部分就安排了Go语言编程思维导引,希望大家意识到Go编程思维在语言精进之路上的重要性。

一门编程语言的思维也不是与生俱来的,而是在演进中逐步形成的。所以在这一部分,我安排了Go诞生与演进、Go设计哲学:简单、组合、并发、面向工程。这样做的目的是让大家一起了解Go语言设计者在设计Go语言时的所思所想,让读者站在语言设计者的高度理解Go语言与众不同的设计,认同Go语言的设计理念。因为这些是Go编程语言思维形成的“土壤”

这一部分最后一节是Go编程思维举例导引,书中给出了C, Haskell和Go程序员在面对同一个问题时,首先考虑到的思维方式以及不同思维下代码设计方式的差异。

知道Go编程思维的重要性后,我们应该怎么做呢?

4. 怎么学习Go编程思维?

学习的本质是一种模仿。要学习Go思维,就要去模仿Go团队、Go社区的优秀项目和代码,看看他们怎么做的。这套书后面的部分讲的就是这个。而“践行哲学,遵循惯例,认清本质,理解原理”就是对后面内容的写作思路的概要性总结。

  • 践行哲学

把Go设计哲学用于自己的项目的设计实践中,而不是仅停留在口头知道上。

  • 遵循惯例

遵循Go团队的一些语言惯例,比如“comma,ok”、使用复合字面值初始化等,使用这些惯例你可以让你的代码显得很地道,别人一看就懂。

  • 认清本质

为了更高效地利用语言机制,我们要认清一些语言机制背后的本质,比如切片、字符串在运行时的表示,这样一来既能帮助开发人员正确使用这些语法元素,同时也能避免入坑。

  • 理解原理

Go带有运行时。运行时全程参与Go应用生命周期,因此,只有对Goroutine调度、GC等原理做适当了解,才能更好的发挥Go的威力。

这套书的part2-part10 就是基于对Go团队、Go社区优秀实践与惯例的梳理,用系统化的思路构建出来并循序渐进呈现给大家的。

5. Part2 – 项目基础:布局、代码风格与命名

这部门的内容是每个gopher在开启一个Go项目时都要考虑的事情。

  • 项目布局

我见过很多Gopher问项目布局的事情,因为Go官方没有给出标准布局。本书讲解了Go项目的结构布局的演进历程以及Go社区的事实标准,希望能给大家提供足够的参考信息。

  • 代码风格

针对Go代码风格,由于代码风格在Go中已经弱化,所以这里主要还是带大家理解gofmt存在的意义和使用方法。

  • 命名惯例

关于命名,我不知道大家是否觉得命名难,但对我来说是挺难的,我总是绞尽脑汁在想用啥名(手动允悲)。所以我的原则是“代码未动,命名先行”。 对于Go中变量、标识符等的命名惯例这样的“关键的问题”,我使用了“笨方法”:我统计了Go标准库、Docker库、k8s库的命名情况,并分门别类给出不同语法元素的命名惯例,具体内容大家可以看书了解 。

6. Part3 – 语法基础:声明、类型、语句与控制结构

第三部分讲的很基础,但内容还是要高于基础的。

  • 一致的变量声明

我们知道Go提供多种变量声明方式,但是在不同位置该用哪种声明方式可读性好又不容易造坑呢(尤其要注意短变量声明)?书中给出了系统阐述。

  • 无类型常量与iota

大家都用过常量,但很多人对于无类型常量与有类型常量区别不了解,书中帮你做了总结。还有,很多人用过iota,但却不理解iota的真正含义以及它能帮你做啥。书中对iota的语义做了说明,对常见用途做了梳理。

  • 零值可用

Go提倡零值可用,也内置了有很多零值可用类型,用起来很爽,比如:切片(不全是,仅在append时是零值可用,当用下标访问时,不具备零值可用)、sync包中的Mutex、RDMutex等

其实类比于线程(thread),goroutine也是一种零值可用的“类型”,只是Go没有goroutine这个类型罢了。

如果我们是包的设计者,如果提供零值可用的类型,可以提升包的使用者的体验。

  • 复合字面值来初始化

使用复合字面值对相应的变量进行初始化是一个Go语言的惯例, Go虽然提供了new和make,但日常很少用,尤其是new。

  • 切片、字符串、map的原理、惯用法与坑

Go是带有runtime的语言,语法层面展示的很多语法元素和runtime层真实的表示并不一致。要想高效利用这些类型,如果不了解runtime层表示还真不行。有时候还有很严重的“坑”。懂了,自然就能绕过坑。

  • 包导入

Go源文件的import语句后面跟着的是包名还是包路径?Go编译是不是必须要有依赖项的源码才可以,只有.a是否可以?这些问题书中都有系统说明

  • 代码块与作用域

代码块与作用域是Go语言的基础概念,虽然基础,如果理解不好,也是有“坑”的,比如最常见的变量遮蔽等。一旦理解透了,还可以帮你解决意想不到的语法问题和执行语义错误问题。

  • 控制语句

Go倡导“一个问题只有一种解决方法”。Go针对每种控制语句仅提供一种语法形式。虽然仅有一种形式,用不好,一样容器掉坑。本套书总结了Go控制语句的惯用法与使用注意事项。

7. Part4 – 语法基础:函数与方法

我们日常编写的Go代码逻辑都在函数或方法中,函数/方法是Go程序逻辑的基本承载单元。

  • init函数

init函数是包初始化过程中执行的函数,它有很多特殊用途。并且其初始化顺序对程序执行语义也有影响,这方面要搞清楚。书中对init函数的常见用途做了梳理,比如database/sql包的驱动自注册模式等。

  • 成为“一等公民”

在Go中,函数成为了“一等公民”。函数成为一等公民后可以像变量一样,被作为参数传递到函数中、作为返回值从函数中返回、作为右值赋值给其他变量等,书中系统讲解了这个特性都有哪些性质和特殊应用,比如函数式编程等。

  • defer语句的惯用法与坑

defer就是帮你简化代码逻辑的,书中总结了defer语句的应用模式。以及使用defer的注意事项,比如函数求值时机、使用开销等。

  • 变长参数函数

Go支持变长参数函数。大家可以没有意识到:变长参数函数是我们日常用的最多的一类函数,比如append函数、fmt.Printf系列、log包中提供的按日志严重级别输出日志的函数等。

但变长参数函数可能也是我们自己设计与实现较少的一类函数形式。 变长参数函数能帮我们做什么呢?书中讲解了变长参数函数的常见用途,比如实现功能选项模式等。

  • 方法的本质、receiver参数类型选择、方法集合

方法的本质其实是函数,弄清楚方法的本质可以帮助我们解决很多难题,书中以实例方式帮助大家理解这一点。

方法receiver参数类型的选择也是Go初学者的常见困惑,这里书中给出三个原则,参照这三个原则,receiver类型选择就不是问题了。

怎么确定一个类型是否实现接口?我们需要看类型的方法集合。那么确定一个类型方法集合就十分重要,尤其是那些包括类型嵌入的类型的方法集合,书中对这块内容做了系统的讲解。

8. Part5 – 语法核心:接口

  • 接口的内部表示

接口是Go语言中的重要语法。Russ Cox曾说过:“如果要从Go语言中挑选出一个特性放入其他语言,我会选择接口”。可见接口的重要性。不过,用好接口类型的前提是理解接口在runtime层的表示,这一节会详细说明空接口与非空接口的内部表示。

  • 接口的设计惯例

我们应该设计什么样的接口呢? 大接口有何弊端?小接口有何优势?多小的接口算是合理的呢?这些在本节都有说明。

  • 接口与组合

组合是Go的设计哲学,Go是关于组合的语言。接口在面向组合编程时将发挥重要作用。这里我将提到Go的两种组合方式:垂直组合和水平组合。其中接口类型在水平组合中起到的关键性的作用。书中还讲解了通过接口进行水平组合的几种模式:包裹模式、适配器函数、中间件等。

很多初学者告诉我,他们做了一段时间Go编码了,但还没有自己设计过接口,我建议这样的同学好好读读这一部分。

9. Part6 – 语法核心:并发编程

  • 并发设计vs并行设计

学习并发编程首先要搞懂并发与并行的概念,书中用了一个很形象的机场安检的例子,来告诉大家并发与并行的区别。并发关乎结构,并行关注执行

  • 并发原语的原理与应用模式

Go实现了csp模型,提供了goroutine、channel、select并发原语。

理解go并发编程。首先要深入理解基于goroutine的并发模型与调度方式。书中对这方面做了深入浅出的讲解,不涉及太多代码,相信大家都能看懂。

书中还对比了go并发模型,一种是csp,一种是传统的基于共享内存方式,并列举了Go并发的常见模式,比如创建、取消、超时、管道模式等。

另外,channel作为goroutine间通信的标准原语,有很多玩法,这里列举了常见的模式和使用注意事项。

  • 低级同步原语(sync和atomic)

虽然有了CSP模型的并发原语,极大简化并发编程,但是sync包和原子操作也不能忘记,很多性能敏感的临界区还需要sync包/atomic这样的低级同步原语来同步。

10. Part7 – 错误处理

单独将错误处理拎出来,是因为很多人尤其是来自java的童鞋,习惯了try-catch-finally的结构化错误处理,看到go的错误处理就让其头疼。

Go语言十分重视错误处理,但它也的确有着相对保守的设计和显式处理错误的惯例。

本部分涵盖常见Go错误处理的策略、避免if err != nil写太多的方案,更为重要的是panic与错误处理的差别。我见过太多将panic用作正常处理的同学了。尤其是来自java阵营的童鞋。

11. Part8 – 编程实践:测试、调试与性能剖析

本部分聚焦编码之外的Go工具链工程实践。

  • Go测试惯例与组织形式

这部分首先和大家聊聊go test包的组织形式,包括是选择包内测试还是包外测试?何时采用符合go惯例的表驱动的测试用例组织形式?如何管理测试依赖的外部数据文件等。

  • 模糊测试(fuzzing test)。

这里的模糊测试并非基于go 1.18的原生fuzzing test进行,写书的时候go 1.18版本尚未发布,而是基于德米特里-维尤科夫的go-fuzz工具

  • 性能基准测试、度量数据与pprof性能剖析

Go原生提供性能基准测试。这一节讲解了如何做性能基准测试、如何编写串行与并行的测试、性能基准测试结果比较工具以及如何排除额外干扰,让结果更准确等方面内容。在讲解pprof性能剖析工具时,我使用一个实例进行剖析讲解,这样理解起来更为直观。

  • Go调试

说到Go调试,我们日常使用最多的估计还是print大法。但在print大法之外,其实有一个事实标准的Go调试工具,它就是delve。在这一节中,我讲解了delve的工作原理以及使用delve如何实现并发调试、coredump调试以及在线挂接(attach)进程的调试。

12. Part9 – 标准库、反射与cgo

go是自带电池,开箱即用的语言,拥有高质量的标准库。在国外有些Gopher甚至倡导仅依赖标准库实现go应用。

  • 高频使用的标准库包(net、http、strings、time、crypto等)

在这一节,我对高频使用的标准库包的原理和使用进行拆解分析,net、http、标准库io模型、strings、time、crypto等以帮助大家更高效的运用标准库。

  • reflect包使用的三大法则

reflect包为go提供了反射能力,书中对反射的实现原理做了讲解,重点是reflect使用的三大法则。

  • cgo使用

cgo不是go,但是cgo机制是使用go与c交互的唯一手段。书中对cgo的用法与约束做了详细讲解,尤其是在cgo开启的情况下如何做静态编译值得大家细读。

  • unsafe包的安全使用法则

事实证明unsafe包很有用,但要做到安全使用unsafe包,尤其是unsafe.Pointer,需要遵循一定的安全使用法则。书中对此做了举例详细说明。

反射、cgo、unsafe算是高级话题,要透彻理解,需要多阅读几遍书中内容并结合实践。

13. Part10 – 工程实践

  • go module

go module在go 1.11版本中引入go,在go 1.16版本中成为go官方默认构建模式。go程序员入门go,精进go都跨不过go module这道坎儿。书中对go module构建模式做了超级系统的讲解:从go构建模式演进历史、go module的概念、原理、惯例、升降级major版本的操作,到使用注意事项等。不过这里还有有一些瑕疵,那就是go module这一节放置的位置太靠后了,应该往往前面提提。如果后面有修订版,可以考虑这么做。

  • 自定义go包导入路径

书中还给出了一个自定义go包导入路径的一种实现方案,十分适合组织内部的私有仓库,有兴趣的同学可以重点看看。

  • go命令的使用模式详解

这一节将go命令分门别类地进行详细说明。包括:

- 获取与安装的go get/go install
- go包检视的go list
- go包构建的go build
- 运行与诊断的GODEBUG、GOGC等环境变量的功用
- 代码静态检查与重构
- 文档查看
- go代码生成go generate
  • Go常见的“坑”

这一节将Go常见的“坑”进行了一次检阅。我这里将坑分为“语法类”和“标准库类”,并借鉴了央视五套天下足球top10节目,对每个坑的“遇坑指数”与“坑害指数”做了点评。

14. 具备完整的示例代码与勘误表

这套书拥有具备完整的示例代码与勘误表,它们都被持续维护,让大家没有读书的后顾之忧。

三. 读书的实践与体会

下面我再分享一下我个人是怎么读书的,包括go技术书籍的读书历程,以及关于读书的一些实践体会。

读书是千人千面的事,没有固定标准的。我的读书方法也不见得适合诸位。大家听听即可,觉得还不错,能借鉴上就最好了。

今天收看直播估计以gopher为主,所以首先说说Go语言书籍的阅读历程

1. Go语言书籍阅读历程:先外后内

对于IT技术类图书,初期还是要看原版的。这个没办法,因为it编程技术绝大多数来自国外。

我读的第一本Go技术书就是《the way to go》,至今这本书也没有引入国内。这是一本Go语言百科全书,大多数内容如今仍适用。唯一不足是该书成书于Go 1.0发布之前,使用的好像是r60版本,有少部分内容已经不适用。

后来Go 1.0发布后,我还陆续读过Addison-Wesley出版的《programming in go》和《The Go Programming Language Phrasebook》,两本书都还不错。

2015年末的布莱恩.克尼根和go核心团队的多诺万联合编写的《The Go Programming Language》,国内称之为Go圣经的书出版了,这让外文go技术书籍达到了巅峰,后来虽然也有go书籍书籍陆续出版,但都无法触及go圣经的地位。

说完外文图书,我再来说说中文Go图书的阅读历程。

我读过的第一本中文Go书籍是2012年许式伟老师的《Go语言编程》,很佩服许老师的眼光和魄力,七牛云很早就在生产用go。

第二本中文Go书籍是雨痕老师的《go学习笔记》,这也是国内第一本深入到go底层原理的书籍(后半部分),遗憾的是书籍停留在go 1.5(还是go 1.6)的实现上,没有随Go版本演进而持续更新。

柴大和曹大合著的《go高级编程》也是一本不错的go技术书籍,如果你要深入学习cgo和go汇编,建议阅读此书。

后面的《Go语言底层原理剖析》和《Go语言设计与实现》也都是以深入了解Go运行机制为目标的书籍,口碑都很好,对这方面内容感兴趣的gopher,可以任意挑一本学习。

2. 自己的读书方法

我的读书方法其实不复杂,主要分为精读和泛读。

  • 阅读方式:好书精读,闲书泛读

好书,集中一大段时间内进行阅读。 闲书(不烧脑),通常是 碎片化阅读。

  • 精读方法:摘录+脑图+行动清单

摘录就是将书中的观点和细节摘录出来,放到读书笔记,最好能用自己的语言重新描述出来,这样印象深刻,理解更为透彻。

脑图,概括书的思维脉络,防止读完就忘记。 通过脑图,我至少看着脉络能想起来。

行动清单:如果没有能输出行动清单,那这本书对你来说意义就不大。 什么是好书,好书就是那种看完后很迫切的想基于书中的观点做点什么。行动清单将有助于我在后续的行动中反复理解书中内容,提高知识的消化率和理解深度。

  • 泛读方法:碎片化+听书

泛读主要是碎片化快读或听书,主要是坐地铁,坐公交,散步时。开车时在保证安全的前提下,可以用听书的方式。

四. 小结

本次分享了三块内容,这里小结一下:

  • 写书历程和写书三要素:能力 + 意愿 + 机会;
  • Go精进之路导读:思维先行,践行哲学,遵循惯例,认清本质,理解原理;
  • 读书方法:选高质量图书精读(脑图+细节摘录+行动清单)。

五. Q&A

  • 在实际开发中有没有什么优雅的处理error的方法?

建议看《Go语言精进之路》第一册第七部分中关于error处理的内容。

  • 是否在工作中使用过六边形架构以及依赖注入的处理经验?

暂没有使用过六边形架构,生产中没有使用过Go第三方依赖注入的方案。

  • 后面会有泛型和模糊测试的补充么?

从书籍内容覆盖全面性的角度而言,我个人有补充上述内容的想法,但还要看现在这套书的销售情况以及出版社的计划。目前还没列入个人工作计划。

  • 作者总结一系列go方法论、惯例等很实用,这种有逻辑的思考和见解是怎么形成的?

没有特意考虑过是怎么形成的。个人平时喜欢多问自己几个为什么,形成让自己信服的工作和学习逻辑。(文字稿补充:同理心、多总结、多复盘、多输出)。

学习Go惯例、方法论,可以多多看Go语言开源项目自身的代码评审,看看Go contributor写代码的思路和如何评审其他贡献者的代码的。(文字稿补充:在这一过程中,潜移默化的感受Go编程思维)。

  • 如何阅读大型go项目的源码?

我个人的方法就是自上而下。先拆分结构,然后找入口。如果是一个可执行的go程序,还是从入口层层的向后看。然后通过一些工具,比如我个人之前开发的函数调用跟踪工具,查看程序执行过程中的函数调用次序。

更细节的内容,还是要深入到代码中去查看。

  • 对Go项目中的一些设计模式的看法?如何使用设计模式,使用时注意哪些事项?

设计模式在go语言中并不是一个经常拿出来提的东西。我之前的一个观点:在其他语言中,需要大家通过一些额外细心的设计构建出来的设计模式,在Go语言中是自然而然就有的东西。

我在自己的日常编码过程中,不会太多从如何应用设计模式的角度思考,而是按照go设计哲学,去考虑并发设计、组合的设计,而不是非要套用那23个经典设计模式。


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2022年,Gopher部落全面改版,将持续分享Go语言与Go应用领域的知识、技巧与实践,并增加诸多互动形式。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats