Go开发关键技术指南敢问路在何方?(内含超全知识大图)

Go开发关键技术指南敢问路在何方?(内含超全知识大图)插图1

作者 | 杨成立(忘篱) 阿里巴巴高级技术专家

Go 开发关键技术指南文章目录:

Go开发关键技术指南敢问路在何方?(内含超全知识大图)插图3

Go 开发指南大图

Go开发关键技术指南敢问路在何方?(内含超全知识大图)插图5

关注阿里巴巴云原生公众号,回复Go即可获取清晰知识大图及最全脑图链接!

Go开发关键技术指南敢问路在何方?(内含超全知识大图)插图3

Engineering

我觉得 Go 在工程上良好的支持,是 Go 能够在服务器领域有一席之地的重要原因。这里说的工程友好包括:

  • gofmt 保证代码的基本一致,增加可读性,避免在争论不清楚的地方争论;

  • 原生支持的 profiling,为性能调优和死锁问题提供了强大的工具支持;

  • utest 和 coverage,持续集成,为项目的质量提供了良好的支撑;

  • example 和注释,让接口定义更友好合理,让库的质量更高。

1. GOFMT 规范编码

之前有段时间,朋友圈霸屏的新闻是码农因为代码不规范问题枪击同事,虽然实际上枪击案可能不是因为代码规范,但可以看出大家对于代码规范问题能引发枪击是毫不怀疑的。

这些年在不同的公司码代码,和不同的人一起码代码,每个地方总有人喜欢纠结于 if () 中是否应该有空格,甚至还大开怼戒。

Go 语言从来不会有这种争论,因为有 gofmt,语言的工具链支持了格式化代码,避免大家在代码风格上白费口舌。

比如,下面的代码看着真是揪心,任何语言都可以写出类似的一坨代码:

packagemainimport("fmt""strings")funcfoo()[]string{return[]string{"gofmt","pprof","cover"}}funcmain(){ifv:=foo();len(v)>0{fmt.Println("Hello",strings.Join(v,", "))}}

如果有几万行代码都是这样,是不是有扣动扳机的冲动?如果我们执行下 gofmt -w t.go 之后,就变成下面的样子:

packagemainimport("fmt""strings")funcfoo()[]string{return[]string{"gofmt","pprof","cover"}}funcmain(){ifv := foo();len(v) >0{fmt.Println("Hello", strings.Join(v,", "))}}

是不是心情舒服多了?gofmt 只能解决基本的代码风格问题,虽然这个已经节约了不少口舌和唾沫,我想特别强调几点:

  • 有些 IDE 会在保存时自动 gofmt,如果没有手动运行下命令 gofmt -w .,可以将当前目录和子目录下的所有文件都格式化一遍,也很容易的是不是;

  • gofmt 不识别空行,因为空行是有意义的,因为空行有意义所以 gofmt 不知道如何处理,而这正是很多同学经常犯的问题;

  • gofmt 有时候会因为对齐问题,导致额外的不必要的修改,这不会有什么问题,但是会干扰 CR 从而影响 CR 的质量。

先看空行问题,不能随便使用空行,因为空行有意义。不能在不该空行的地方用空行,不能在该有空行的地方不用空行,比如下面的例子:

packagemainimport("fmt""io""os")funcmain(){f, err := os.Open(os.Args[1])iferr !=nil{fmt.Println("show file err %v", err)os.Exit(-1)}deferf.Close()io.Copy(os.Stdout, f)}

上面的例子看起来就相当的奇葩,if 和 os.Open 之间没有任何原因需要个空行,结果来了个空行;而 defer 和 io.Copy 之间应该有个空行却没有个空行。

空行是非常好的体现了逻辑关联的方式,所以空行不能随意,非常严重地影响可读性,要么就是一坨东西看得很费劲,要么就是突然看到两个紧密的逻辑身首异处,真的让人很诧异。

上面的代码可以改成这样,是不是看起来很舒服了:

packagemainimport("fmt""io""os")funcmain(){f, err := os.Open(os.Args[1])iferr !=nil{fmt.Println("show file err %v", err)os.Exit(-1)}deferf.Close()io.Copy(os.Stdout, f)}

再看 gofmt 的对齐问题,一般出现在一些结构体有长短不一的字段,比如统计信息,比如下面的代码:

packagemaintypeNetworkStatstruct{IncomingBytesint`json:"ib"`OutgoingBytesint`json:"ob"`}funcmain(){}

如果新增字段比较长,会导致之前的字段也会增加空白对齐,看起来整个结构体都改变了:

packagemaintypeNetworkStatstruct{IncomingBytesint`json:"ib"`OutgoingBytesint`json:"ob"`IncomingPacketsPerHourint`json:"ipp"`DropKiloRateLastMinuteint`json:"dkrlm"`}funcmain(){}

比较好的解决办法就是用注释,添加注释后就不会强制对齐了。

2. Profile 性能调优

性能调优是一个工程问题,关键是测量后优化,而不是盲目优化。Go 提供了大量的测量程序的工具和机制,包括Profiling Go Programs,Introducing HTTP Tracing,我们也在性能优化时使用过 Go 的 Profiling,原生支持是非常便捷的。

对于多线程同步可能出现的死锁和竞争问题,Go 提供了一系列工具链,比如 Introducing the Go Race Detector, Data Race Detector,不过打开 race 后有明显的性能损耗,不应该在负载较高的线上服务器打开,会造成明显的性能瓶颈。

推荐服务器开启 http profiling,侦听在本机可以避免安全问题,需要 profiling 时去机器上把 profile 数据拿到后,拿到线下分析原因。实例代码如下:

packagemainimport("net/http"_"net/http/pprof""time")funcmain(){gohttp.ListenAndServe("127.0.0.1:6060",nil)for{b :=make([]byte,4096)fori :=0; i <len(b); i++ {b[i] = b[i] +0xf}time.Sleep(time.Nanosecond)}}

编译成二进制后启动 go mod init private.me && go build . && ./private.me,在浏览器访问页面可以看到各种性能数据的导航:http://localhost:6060/debug/pprof/。

例如分析 CPU 的性能瓶颈,可以执行 go tool pprof private.me http://localhost:6060/debug/pprof/profile,默认是分析 30 秒内的性能数据,进入 pprof 后执行 top 可以看到 CPU 使用最高的函数:

(pprof) topShowing nodes accountingfor42.41s,99.14% of42.78stotalDropped27nodes (cum <=0.21s)Showing top10nodes out of22flat  flat%   sum%        cum   cum%27.20s63.58%63.58%27.20s63.58%  runtime.pthread_cond_signal13.07s30.55%94.13%13.08s30.58%  runtime.pthread_cond_wait1.93s4.51%98.64%1.93s4.51%  runtime.usleep0.15s0.35%98.99%0.22s0.51%  main.main

除了 top,还可以输入 web 命令看调用图,还可以用 go-torch 看火焰图等。

3. UTest 和 Coverage

当然工程化少不了 UTest 和覆盖率,关于覆盖 Go 也提供了原生支持The cover story,一般会有专门的 CISE 集成测试环境。集成测试之所以重要,是因为随着代码规模的增长,有效的覆盖能显著的降低引入问题的可能性。

什么是有效的覆盖?一般多少覆盖率比较合适?80% 覆盖够好了吗?90% 覆盖一定比 30% 覆盖好吗?我觉得可不一定,参考Testivus On Test Coverage。对于 UTest 和覆盖,我觉得重点在于:

  • UTest 和覆盖率一定要有,哪怕是 0.1% 也必须要有,为什么呢?因为出现故障时让老板心里好受点啊,能用数据衡量出来裸奔的代码有多少;

  • 核心代码和业务代码一定要分离,强调核心代码的覆盖率才有意义,比如整体覆盖了 80%,核心代码占 5%,核心代码覆盖率为 10%,那么这个覆盖就不怎么有效了;

  • 除了关键正常逻辑,更应该重视异常逻辑,异常逻辑一般不会执行到,而一旦藏有 bug 可能就会造成问题。有可能有些罕见的代码无法覆盖到,那么这部分逻辑代码,CR 时需要特别人工 Review。

分离核心代码是关键。

可以将核心代码分离到单独的 package,对这个 package 要求更高的覆盖率,比如我们要求 98% 的覆盖(实际上做到了 99.14% 的覆盖)

对于应用的代码,具备可测性是非常关键的,举个我自己的例子,go-oryx 这部分代码是判断哪些 url 是代理,就不具备可测性,下面是主要的逻辑:

http.HandleFunc("/",func(w http.ResponseWriter, r *http.Request){ifo := r.Header.Get("Origin");len(o) >0{w.Header().Set("Access-Control-Allow-Origin","*")}ifproxyUrls ==nil{......fs.ServeHTTP(w, r)return}for_, proxyUrl :=rangeproxyUrls {srcPath, proxyPath := r.URL.Path, proxyUrl.Path......ifproxy, ok := proxies[proxyUrl.Path]; ok {p.ServeHTTP(w, r)return}}fs.ServeHTTP(w, r)})

可以看得出来,关键需要测试的核心代码,在于后面如何判断URL符合定义的规范,这部分应该被定义成函数,这样就可以单独测试了:

funcshouldProxyURL(srcPath, proxyPathstring)bool{if!strings.HasSuffix(srcPath,"/") {// /api to /api/// /api.js to /api.js/// /api/100 to /api/100/srcPath +="/"}if!strings.HasSuffix(proxyPath,"/") {// /api/ to /api/// to match /api/ or /api/100// and not match /api.js/proxyPath +="/"}returnstrings.HasPrefix(srcPath, proxyPath)}funcrun(ctx context.Context)error{http.HandleFunc("/",func(w http.ResponseWriter, r *http.Request){......for_, proxyUrl :=rangeproxyUrls {if!shouldProxyURL(r.URL.Path, proxyUrl.Path) {continue}

代码参考go-oryx: Extract and test URL proxy,覆盖率请看gocover: For go-oryx coverage,这样的代码可测性就会比较好,也能在有限的精力下尽量让覆盖率有效。

Note: 可见,单元测试和覆盖率,并不是测试的事情,而是代码本身应该提高的代码可测试性。

另外,对于 Go 的测试还有几点值得说明:

  • helper:测试时如果调用某个函数,出错时总是打印那个共用的函数的行数,而不是测试的函数。比如test_helper.go,如果 compare 不调用 t.Helper(),那么错误显示是 hello_test.go:26: Returned: [Hello, world!], Expected: [BROKEN!],调用 t.Helper() 之后是 hello_test.go:18: Returned: [Hello, world!], Expected: [BROKEN!]`,实际上应该是 18 行的 case 有问题,而不是 26 行这个 compare 函数的问题;

  • benchmark:测试时还可以带 Benchmark 的,参数不是 testing.T 而是 testing.B,执行时会动态调整一些参数,比如 testing.B.N,还有并行执行的 testing.PB. RunParallel,参考Benchamrk

  • main: 测试也是有个 main 函数的,参考TestMain,可以做一些全局的初始化和处理。

  • doc.go: 整个包的文档描述,一般是在 package http 前面加说明,比如http doc的使用例子。

对于 Helper 还有一种思路,就是用带堆栈的 error,参考前面关于 errors 的说明,不仅能将所有堆栈的行数给出来,而且可以带上每一层的信息。

注意如果 package 只暴露了 interface,比如 go-oryx-lib: aac 通过 NewADTS() (ADTS, error) 返回的是接口 ADTS,无法给 ADTS 的函数加 Example;因此我们专门暴露了一个 ADTSImpl 的结构体,而 New 函数返回的还是接口,这种做法不是最好的,让用户有点无所适从,不知道该用 ADTS 还是 ADTSImpl。

所以一种可选的办法,就是在包里面有个 doc.go 放说明,例如 net/http/doc.go 文件,就是在 package http 前面加说明,比如 http doc 的使用例子。

4. 注释和 Example

注释和 Example 是非常容易被忽视的,我觉得应该注意的地方包括:

  • 项目的 README.md 和 Wiki,这实际上就是新人指南,因为新人如果能懂那么就很容易了解这个项目的大概情况,很多项目都没有这个。如果没有 README,那么就需要看文件,该看哪个文件?这就让人很抓狂了;

  • 关键代码没有注释,比如库的 API,关键的函数,不好懂的代码段落。如果看标准库,绝大部分可以调用的 API 都有很好的注释,没有注释怎么调用呢?只能看代码实现了,如果每次调用都要看一遍实现,真的很难受了;

  • 库没有 Example,库是一种要求很高的包,就是给别人使用的包,比如标准库。绝大部分的标准库的包,都有 Example,因为没有 Example 很难设计出合理的 API。

先看关键代码的注释,有些注释完全是代码的重复,没有任何存在的意义,唯一的存在就是提高代码的注释率,这又有什么用呢,比如下面代码:

wsconn *Conn//ws connection// The RPC call.typerpcCallstruct{// Setup logger.iferr := SetupLogger(......); err !=nil{// Wait for os signalserver.WaitForSignals(

如果注释能通过函数名看出来(比较好的函数名要能看出来它的职责),那么就不需要写重复的注释,注释要说明一些从代码中看不出来的东西,比如标准库的函数的注释:

// Serve accepts incoming connections on the Listener l, creating a// new service goroutine for each. The service goroutines read requests and// then call srv.Handler to reply to them.//// HTTP/2 support is only enabled if the Listener returns *tls.Conn// connections and they were configured with "h2" in the TLS// Config.NextProtos.//// Serve always returns a non-nil error and closes l.// After Shutdown or Close, the returned error is ErrServerClosed.func(srv *Server)Serve(l net.Listener)error{// ParseInt interprets a string s in the given base (0, 2 to 36) and// bit size (0 to 64) and returns the corresponding value i.//// If base == 0, the base is implied by the strings prefix:// base 2 for "0b", base 8 for "0" or "0o", base 16 for "0x",// and base 10 otherwise. Also, for base == 0 only, underscore// characters are permitted per the Go integer literal syntax.// If base is below 0, is 1, or is above 36, an error is returned.//// The bitSize argument specifies the integer type// that the result must fit into. Bit sizes 0, 8, 16, 32, and 64// correspond to int, int8, int16, int32, and int64.// If bitSize is below 0 or above 64, an error is returned.//// The errors that ParseInt returns have concrete type *NumError// and include err.Num = s. If s is empty or contains invalid// digits, err.Err = ErrSyntax and the returned value is 0;// if the value corresponding to s cannot be represented by a// signed integer of the given size, err.Err = ErrRange and the// returned value is the maximum magnitude integer of the// appropriate bitSize and sign.funcParseInt(sstring, baseint, bitSizeint)(iint64, err error){

标准库做得很好的是,会把参数名称写到注释中(而不是用 @param 这种方式),而且会说明大量的背景信息,这些信息是从函数名和参数看不到的重要信息。

咱们再看 Example,一种特殊的 test,可能不会执行,它的主要作用是为了推演接口是否合理,当然也就提供了如何使用库的例子,这就要求 Example 必须覆盖到库的主要使用场景。

举个例子,有个库需要方式 SSRF 攻击,也就是检查 HTTP Redirect 时的 URL 规则,最初我们是这样提供这个库的:

funcNewHttpClientNoRedirect()*http.Client{

看起来也没有问题,提供一种特殊的 http.Client,如果发现有 Redirect 就返回错误,那么它的 Example 就会是这样:

funcExampleNoRedirectClient(){url :="http://xxx/yyy"client := ssrf.NewHttpClientNoRedirect()Req, err := http.NewRequest("GET", url,nil)iferr !=nil{fmt.Println("failed to create request")return}resp, err := client.Do(Req)fmt.Printf("status :%v", resp.Status)}

这时候就会出现问题,我们总是返回了一个新的 http.Client,如果用户自己有了自己定义的 http.Client 怎么办?实际上我们只是设置了 http.Client.CheckRedirect 这个回调函数。如果我们先写 Example,更好的 Example 会是这样:

funcExampleNoRedirectClient(){client := http.Client{}//Must specify checkRedirect attribute to NewFuncNoRedirectclient.CheckRedirect = ssrf.NewFuncNoRedirect()Req, err := http.NewRequest("GET", url,nil)iferr !=nil{fmt.Println("failed to create request")return}resp, err := client.Do(Req)}

那么我们自然知道应该如何提供接口了。

5. 其他工程化

最近得知 WebRTC 有 4GB 的代码,包括它自己的以及依赖的代码,就算去掉一般的测试文件和文档,也有 2GB 的代码!!!编译起来真的是非常耗时间,而 Go 对于编译速度的优化,据说是在 Google 有过验证的,具体我们还没有到这个规模。具体可以参考Why so fast?,主要是编译器本身比 GCC 快 (5X),以及 Go 的依赖管理做的比较好。

Go 的内存和异常处理也做得很好,比如不会出现野指针,虽然有空指针问题可以用 recover 来隔离异常的影响。而 C 或 C++ 服务器,目前还没有见过没有内存问题的,上线后就是各种的野指针满天飞,总有因为野指针搞死的时候,只是或多或少罢了。

按照 Go 的版本发布节奏,6 个月就发一个版本,基本上这么多版本都很稳定,Go1.11 的代码一共有 166 万行 Go 代码,还有 12 万行汇编代码,其中单元测试代码有 32 万行(占 17.9%),使用实例 Example 有 1.3 万行。

Go 对于核心 API 是全部覆盖的,提交有没有导致 API 不符合要求都有单元测试保证,Go 有多个集成测试环境,每个平台是否测试通过也能看到,这一整套机制让 Go 项目虽然越来越庞大,但是整体研发效率却很高。

Go开发关键技术指南敢问路在何方?(内含超全知识大图)插图3

Go2 Transition

Go2 的设计草案在Go 2 Draft Designs,而 Go1 如何迁移到 Go2 也是我个人特别关心的问题,Python2 和 Python3 的那种不兼容的迁移方式简直就是噩梦一样的记忆。Go 的提案中,有一个专门说了迁移的问题,参考Go2 Transition

Go2 Transition 还不是最终方案,不过它也对比了各种语言的迁移,还是很有意思的一个总结。这个提案描述了在非兼容性变更时,如何给开发者挖的坑最小。

目前 Go1 的标准库是遵守兼容性原则的,参考 Go 1 compatibility guarantee,这个规范保证了 Go1 没有兼容性问题,几乎可以没有影响的升级比如从 Go1.2 升级到 Go1.11。

几乎的意思,是很大概率是没有问题,当然如果用了一些非常冷门的特性,可能会有坑,我们遇到过 json 解析时,内嵌结构体的数据成员也得是 exposed 的才行,而这个在老版本中是可以非 exposed;还遇到过 cgo 对于链接参数的变更导致编译失败,这些问题几乎很难遇到,都可以算是兼容的吧,有时候只是把模糊不清的定义清楚了而已。

Go2 在语言和标准库上,会打破 Go1 的兼容性规范,也就是和 Go1 不再兼容。不过 Go 是分布式开源社区在维护,不能依赖于flag day,还是要容许不同 Go 版本写的 package 的互操作性。

先了解下各个语言如何考虑兼容性:

  • C 是严格向后兼容的,很早写的程序总是能在新的编译器中编译。另外新的编译器也支持指定之前的标准,比如 -std=c90 使用 ISO C90 标准编译程序。关键的特性是编译成目标文件后,不同版本的 C 的目标文件,能完美的链接成执行程序;C90 实际上是对之前 K&R C 版本不兼容的,主要引入了 volatile 关键字、整数精度问题,还引入了trigraphs,最糟糕的是引入了undefined行为,比如数组越界和整数溢出的行为未定义。从 C 上可以学到的是:后向兼容非常重要;非常小的打破兼容性也问题不大特别是可以通过编译器选项来处理;能将不同版本的目标文件链接到一起是非常关键的;undefined 行为严重困扰开发者容易造成问题;

  • C++ 也是 ISO 组织驱动的语言,和 C 一样也是向后兼容的。C++和C一样坑爹的地方坑到吐血,比如 undefined行为等。尽管一直保持向后兼容,但是新的C++代码比如C++11 看起来完全不同,这是因为有新的改变的特性,比如很少会用裸指针、比如 range 代替了传统的 for 循环,这导致熟悉老C++语法的程序员看新的代码非常难受甚至看不懂。C++毋庸置疑是非常流行的,但是新的语言标准在这方面没有贡献。从C++上可以学到的新东西是:尽管保持向后兼容,语言的新版本可能也会带来巨大的不同的感受(保持向后兼容并不能保证能持续看懂)

  • Java 也是向后兼容的,是在字节码层面和语言层面都向后兼容,尽管语言上不断新增了关键字。Java 的标准库非常庞大,也不断在更新,过时的特性会被标记为 deprecated 并且编译时会有警告,理论上一定版本后 deprecated 的特性会不可用。Java 的兼容性问题主要在 JVM 解决,如果用新的版本编译的字节码,得用新的 JVM 才能执行。Java 还做了一些前向兼容,这个影响了字节码啥的(我本身不懂 Java,作者也不说自己不是专家,我就没仔细看了)。Java 上可以学到的新东西是:要警惕因为保持兼容性而限制语言未来的改变;

  • Python2.7 是 2010 年发布的,目前主要是用这个版本。Python3 是 2006 年开始开发,2008 年发布,十年后的今天还没有迁移完成,甚至主要是用的 Python2 而不是 Python3,这当然不是 Go2 要走的路。看起来是因为缺乏向后兼容导致的问题,Python3 刻意的和之前版本不兼容,比如 print 从语句变成了一个函数,string 也变成了 Unicode(这导致和 C 调用时会有很多问题)。没有向后兼容,同时还是解释型语言,这导致 Python2 和 3 的代码混着用是不可能的,这意味着程序依赖的所有库必须支持两个版本。Python 支持 from __future__ import FEATURE,这样可以在 Python2 中用 Python3 的特性。Python 上可以学到的东西是:向后兼容是生死攸关的;和其他语言互操作的接口兼容是非常重要的;能否升级到新的语言是由调用的库支持的;

  • Perl6 是 2000 年开始开发的,15 年后才正式发布,这也不是 Go2 应该走的路。这么漫长的主要原因包括:刻意没有向后兼容,只有语言的规范没有实现而这些规范不断的修改。Perl 上可以学到的东西是:不要学 Perl;设置期限按期交付;别一下子全部改了。

特别说明的是,非常高兴的是 Go2 不会重新走 Python3 的老路子,当初被 Python 的版本兼容问题坑得不要不要的。

虽然上面只是列举了各种语言的演进,确实也了解得更多了,有时候描述问题本身,反而更能明白解决方案。C 和 C 的向后兼容确实非常关键,但也不是它们能有今天地位的原因,C11 的新特性到底增加了多少 DAU 呢,确实是值得思考的。

另外 C11 加了那么多新的语言特性,比如 WebRTC 代码就是这样,很多老 C 程序员看到后一脸懵逼,和一门新的语言一样了,是否保持完全的兼容不能做一点点变更,其实也不是的。

应该将 Go 的语言版本和标准库的版本分开考虑,这两个也是分别演进的,例如 alias 是 1.9 引入的向后兼容的特性,1.9 之前的版本不支持,1.9 之后的都支持。语言方面包括:

  • Language additions 新增的特性。比如 1.9 新增的 type alias,这些向后兼容的新特性,并不要求代码中指定特殊的版本号,比如用了 alias 的代码不用指定要 1.9 才能编译,用之前的版本会报错。向后兼容的语言新增的特性,是依靠程序员而不是工具链来维护的,要用这个特性或库升级到要求的版本就可以;

  • Language removals 删除的特性。比如有个提案3939去掉 string(int),字符串构造函数不支持整数,假设这个在 Go1.20 版本去掉,那么 Go1.20 之后这种 string(1000) 代码就要编译失败了。这种情况没有特别好的办法能解决,我们可以提供工具,将代码自动替换成新的方式,这样就算库维护者不更新,使用者自己也能更新。这种场景引出了指定最大版本,类似 C 的 -std=C90,可以指定最大编译的版本比如 -lang=go1.19,当然必须能和 Go1.20 的代码链接。指定最大版本可以在 go.mod 中指定,这需要工具链兼容历史的版本,由于这种特性的删除不会很频繁,维护负担还是可以接受的;

  • Minimum language version 最小要求版本。为了可以更明确的错误信息,可以允许模块在 go.mod 中指定最小要求的版本,这不是强制性的,只是说明了这个信息后编译工具能明确给出错误,比如给出应该用具体哪个版本;

  • Language redefinitions 语言重定义。比如 Go1.1 时,int 在 64 位系统中长度从 4 字节变成了 8 字节,这会导致很多潜在的问题。比如20733修改了变量在 for 中的作用域,看起来是解决潜在的问题,但也可能会引入问题。引入关键字一般不会有问题,不过如果和函数冲突就会有问题,比如 error: check。为了让 Go 的生态能迁移到 Go2,语言重定义的事情应该尽量少做,因为我们不再能依赖编译器检查错误。虽然指定版本能解决这种问题,但是这始终会导致未知的结果,很有可能一升级 Go 版本就挂了。我觉得对于语言重定义,应该完全禁止。比如 20733 可以改成禁止这种做法,这样就会变成编译错误,可能会帮助找到代码中潜在的 BUG;

  • Build tags 编译 tags。在指定文件中指定编译选项,是现有的机制,不过是指定的 release 版本号,它更多是指定了最小要求的版本,而没有解决最大依赖版本问题;

  • Import go2 导入新特性。和 Python 的特性一样,可以在 Go1 中导入 Go2 的新特性,比如可以显式地导入 import “go2/type-aliases”,而不是在 go.mod 中隐式的指定。这会导致语言比较复杂,将语言打乱成了各种特性的组合。而且这种方式一旦使用,将无法去掉。这种方式看起来不太适合 Go。

如果有更多的资源来维护和测试,标准库后续会更快发布,虽然还是 6 个月的周期。标准库方面的变更包括:

  • Core standard library 核心标准库。有些和编译工具链相关的库,还有其他的一些关键的库,应该遵守 6 个月的发布周期,而且这些核心标准库应该保持 Go1 的兼容性,比如 os/signal、reflect、runtime、sync、testing、time、unsafe 等等。我可能乐观的估计 net, os, 和 syscall 不在这个范畴;

  • Penumbra standard library 边缘标准库。它们被独立维护,但是在一个 release 中一起发布,当前核心库大部分都属于这种。这使得可以用 go get 等工具来更新这些库,比 6 个月的周期会更快。标准库会保持和前面版本的编译兼容,至少和前面一个版本兼容

  • Removing packages from the standard library 去掉一些不太常用的标准库,比如 net/http/cgi 等。

如果上述的工作做得很好的话,开发者会感觉不到有个大版本叫做 Go2,或者这种缓慢而自然的变化逐渐全部更新成了 Go2。甚至我们都不用宣传有个 Go2,既然没有 C2.0 为何要 Go2.0 呢?主流的语言比如 C、C++ 和 Java 从来没有 2.0,一直都是 1.N 的版本,我们也可以模仿它们。

事实上,一般所认为的全新的 2.0 版本,若出现不兼容性的语言和标准库,对用户也不是个好结果,甚至还是有害的。

Go开发关键技术指南敢问路在何方?(内含超全知识大图)插图3

Others

关于 Go,还有哪些重要的技术值得了解呢?下面将进行详细的分享。

1. GC

GC 一般是 C/C 程序员对于 Go 最常见、也是最先想到的一个质疑,GC 这玩意儿能行吗?我们以前 C/C 程序都是自己实现内存池的,我们内存分配算法非常牛逼的。

Go 的 GC 优化之路,可以详细读Getting to Go: The Journey of Gos Garbage Collector

2014 年 Go1.4,GC 还是很弱的,是决定 Go 生死的大短板。

Go开发关键技术指南敢问路在何方?(内含超全知识大图)插图7

上图是 Twitter 的线上服务监控。Go1.4 的 STW(Stop the World) Pause time 是 300 毫秒,而 Go1.5 优化到了 30 毫秒。

而 Go1.6 的 GC 暂停时间降低到了 3 毫秒左右。

Go1.8 则降低到了 0.5 毫秒左右,也就是 500 微秒。从 Go1.4 到 Go1.8,优化了 600 倍性能。

如何看 GC 的 STW 时间呢?可以引入 net/http/pprof 这个库,然后通过 curl 来获取数据,实例代码如下:

packagemainimport("net/http"_"net/http/pprof")funcmain(){http.ListenAndServe("localhost:6060",nil)}

启动程序后,执行命令就可以拿到结果(由于上面的例子中没有 GC,下面的数据取的是另外程序的部分数据)

$ curlhttp://localhost:6060/debug/pprof/allocs?debug=12>/dev/null|grep PauseNsPauseNs = [205683 79032 202102 82216 104853 142320 90058 113638 1525041459657204749690158458604999961011275412226252252492346842015985797940226085103644135428245291141997924707997413281774634656537358247399516538610748619625836890613186811190385482445317458550162314451073971090308177192603585859662040416297631022483280449394837157709910898366133478323537914394969235278203567799430104303132657635423943412641863845167969116438689047789913650611970847501]

可以用 python 计算最大值是 322 微秒,最小是 26 微秒,平均值是 81 微秒。

2. Declaration Syntax

关于 Go 的声明语法Go Declaration Syntax,和 C 语言有对比,在The “Clockwise/Spiral Rule”这个文章中也详细描述了 C 的顺时针语法规则。其中有个例子:

int(*signal(int,void(*fp)(int)))(int);

这是个什么呢?翻译成 Go 语言就能看得很清楚:

funcsignal(aint, bfunc(int))func(int)int

signal 是个函数,有两个参数,返回了一个函数指针。signal 的第一个参数是 int,第二个参数是一个函数指针。

当然实际上 C 语言如果借助 typedef 也是能获得比较好的可读性的:

typedefvoid(*PFP)(int);typedefint(*PRET)(int);PRETsignal(inta, PFP b);

只是从语言的语法设计上来说,还是 Go 的可读性确实会好一些。这些点点滴滴的小傲娇,是否可以支撑我们够浪程序员浪起来的资本呢?至少 Rob Pike 不是拍脑袋和大腿想出来的规则嘛,这种认真和严谨是值得佩服和学习的。

3. Documents

新的语言文档支持都很好,不用买本书看,Go 也是一样,Go 官网历年比较重要的文章包括:

  • 语法特性及思考:Go Declaration Syntax, The Laws of Reflection, Constants, Generics Discussion, Another Go at Language Design, Composition not inheritance, Interfaces and other types;

  • 并发相关特性:Share Memory By Communicating, Go Concurrency Patterns: Timing out, moving on, Concurrency is not parallelism, Advanced Go Concurrency Patterns, Go Concurrency Patterns: Pipelines and cancellation, Go Concurrency Patterns: Context, Mutex or Channel;

  • 错误处理相关:Defer, Panic, and Recover, Error handling and Go, Errors are values, Stack traces and the errors package, Error Handling In Go, The Error Model;

  • 性能和优化:Profiling Go Programs, Introducing the Go Race Detector, The cover story, Introducing HTTP Tracing, Data Race Detector;

  • 标准库说明:Go maps in action, Go Slices: usage and internals, Arrays, slices (and strings): The mechanics of append, Strings, bytes, runes and characters in Go;

  • 和 C 的结合:C? Go? Cgo!

  • 项目相关:Organizing Go code, Package names, Effective Go, versioning, Russ Cox: vgo;

  • 关于 GC:Go GC: Prioritizing low latency and simplicity, Getting to Go: The Journey of Go Garbage Collector, Proposal: Eliminate STW stack re-scanning.

其中,文章中有引用其他很好的文章,我也列出来哈:

  • Go 中的面向对象设计原则SOLID

  • Go 的版本语义Semantic Versioning,如何在大型项目中规范版本,避免导致依赖地狱 (Dependency Hell) 问题。

4. SRS

SRS是使用 ST,单进程单线程,性能是 EDSM 模型的nginx-rtmp的 3 到 5 倍,参考SRS: Performance,当然不是 ST 本身性能是 EDSM 的三倍,而是说 ST 并不会比 EDSM 性能低,主要还是要根据业务上的特征做优化。

关于 ST 和 EDSM,参考本文前面关于Concurrency对于协程的描述,ST 它是 C 的一个协程库,EDSM 是异步事件驱动模型。

SRS 是单进程单线程,可以扩展为多进程,可以在 SRS 中改代码 Fork 子进程,或者使用一个 TCP 代理,比如 TCP 代理go-oryx: rtmplb

在 2016 年和 2017 年我用 Go 重写过 SRS,验证过 Go 使用 2CPU 可以跑到 C10K,参考go-oryx,v0.1.13 Supports 10k(2CPUs) for RTMP players。

由于仅仅是语言的差异而重写一个项目,没有找到更好的方式或理由,觉得很不值得,所以还是放弃了 Go 语言版本,只维护 C++ 版本的 SRS。Go 目前一般在 API 服务器用得比较多,能否在流媒体服务器中应用?答案是肯定的,我已经实现过了。

后来在 2017 年,终于找到相对比较合理的方式来用 Go 写流媒体,就是只提供库而不是二进制的服务器,参考go-oryx-lib

目前 Go 可以作为 SRS 前面的代理,实现多核的优势,参考go-oryx

原创文章 Go开发关键技术指南敢问路在何方?(内含超全知识大图),版权所有
如若转载,请注明出处:https://www.itxiaozhan.cn/20229392.html

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注