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


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

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

Go 开发指南大图


Engineering

我以为 Go 在工程上良好的支持,是 Go 可以在服务器领域有一席之地的重要缘由。这里说的工程友好包括:python

  • gofmt 保证代码的基本一致,增长可读性,避免在争论不清楚的地方争论;
  • 原生支持的 profiling,为性能调优和死锁问题提供了强大的工具支持;
  • utest 和 coverage,持续集成,为项目的质量提供了良好的支撑;
  • example 和注释,让接口定义更友好合理,让库的质量更高。

GOFMT 规范编码

以前有段时间,朋友圈霸屏的新闻是码农由于代码不规范问题枪击同事,虽然实际上枪击案可能不是由于代码规范,但能够看出你们对于代码规范问题能引起枪击是绝不怀疑的。这些年在不一样的公司码代码,和不一样的人一块儿码代码,每一个地方总有人喜欢纠结于 if () 中是否应该有空格,甚至还大开怼戒。nginx

Go 语言历来不会有这种争论,由于有 gofmt,语言的工具链支持了格式化代码,避免你们在代码风格上白费口舌。git

好比,下面的代码看着真是揪心,任何语言均可以写出相似的一坨代码:程序员

package main
import (
    "fmt"
    "strings"
)
func foo()[]string {
    return []string{"gofmt","pprof","cover"}}

func main() {
    if v:=foo();len(v)>0{fmt.Println("Hello",strings.Join(v,", "))}
}
复制代码

若是有几万行代码都是这样,是否是有扣动扳机的冲动?若是咱们执行下 gofmt -w t.go 以后,就变成下面的样子:github

package main

import (
	"fmt"
	"strings"
)

func foo() []string {
	return []string{"gofmt", "pprof", "cover"}
}

func main() {
	if v := foo(); len(v) > 0 {
		fmt.Println("Hello", strings.Join(v, ", "))
	}
}
复制代码

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

  • 有些 IDE 会在保存时自动 gofmt,若是没有手动运行下命令 gofmt -w .,能够将当前目录和子目录下的全部文件都格式化一遍,也很容易的是否是;
  • gofmt 不识别空行,由于空行是有意义的,由于空行有意义因此 gofmt 不知道如何处理,而这正是不少同窗常常犯的问题;
  • gofmt 有时候会由于对齐问题,致使额外的没必要要的修改,这不会有什么问题,可是会干扰 CR 从而影响 CR 的质量。

先看空行问题,不能随便使用空行,由于空行有意义。不能在不应空行的地方用空行,不能在该有空行的地方不用空行,好比下面的例子:web

package main

import (
	"fmt"
	"io"
	"os"
)

func main() {
	f, err := os.Open(os.Args[1])

	if err != nil {

		fmt.Println("show file err %v", err)
		os.Exit(-1)
	}
	defer f.Close()
	io.Copy(os.Stdout, f)
}
复制代码

上面的例子看起来就至关的奇葩,if 和 os.Open 之间没有任何缘由须要个空行,结果来了个空行;而 defer 和 io.Copy 之间应该有个空行却没有个空行。空行是很是好的体现了逻辑关联的方式,因此空行不能随意,很是严重地影响可读性,要么就是一坨东西看得很费劲,要么就是忽然看到两个紧密的逻辑身首异处,真的让人很诧异。算法

上面的代码能够改为这样,是否是看起来很舒服了:

package main

import (
	"fmt"
	"io"
	"os"
)

func main() {
	f, err := os.Open(os.Args[1])
	if err != nil {
		fmt.Println("show file err %v", err)
		os.Exit(-1)
	}
	defer f.Close()
	
	io.Copy(os.Stdout, f)
}
复制代码

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

package main

type NetworkStat struct {
	IncomingBytes int `json:"ib"`
	OutgoingBytes int `json:"ob"`
}

func main() {
}
复制代码

若是新增字段比较长,会致使以前的字段也会增长空白对齐,看起来整个结构体都改变了:

package main

type NetworkStat struct {
	IncomingBytes          int `json:"ib"`
	OutgoingBytes          int `json:"ob"`
	IncomingPacketsPerHour int `json:"ipp"`
	DropKiloRateLastMinute int `json:"dkrlm"`
}

func main() {
}
复制代码

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

Profile 性能调优

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

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

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

package main

import (
	"net/http"
	_ "net/http/pprof"
	"time"
)

func main() {
	go http.ListenAndServe("127.0.0.1:6060", nil)

	for {
		b := make([]byte, 4096)
		for i := 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) top
Showing nodes accounting for 42.41s, 99.14% of 42.78s total
Dropped 27 nodes (cum <= 0.21s)
Showing top 10 nodes out of 22
      flat  flat%   sum%        cum   cum%
    27.20s 63.58% 63.58%     27.20s 63.58%  runtime.pthread_cond_signal
    13.07s 30.55% 94.13%     13.08s 30.58%  runtime.pthread_cond_wait
     1.93s  4.51% 98.64%      1.93s  4.51%  runtime.usleep
     0.15s  0.35% 98.99%      0.22s  0.51%  main.main
复制代码

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

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) {
		if o := r.Header.Get("Origin"); len(o) > 0 {
			w.Header().Set("Access-Control-Allow-Origin", "*")
		}

		if proxyUrls == nil {
			......
			fs.ServeHTTP(w, r)
			return
		}

		for _, proxyUrl := range proxyUrls {
			srcPath, proxyPath := r.URL.Path, proxyUrl.Path
			......
			if proxy, ok := proxies[proxyUrl.Path]; ok {
				p.ServeHTTP(w, r)
				return
			}
		}

		fs.ServeHTTP(w, r)
	})
复制代码

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

func shouldProxyURL(srcPath, proxyPath string) 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 += "/"
	}

	return strings.HasPrefix(srcPath, proxyPath)
}

func run(ctx context.Context) error {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		......
		for _, proxyUrl := range proxyUrls {
			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 的使用例子。

注释和 Example

注释和 Example 是很是容易被忽视的,我以为应该注意的地方包括:

  • 项目的 README.md 和 Wiki,这实际上就是新人指南,由于新人若是能懂那么就很容易了解这个项目的大概状况,不少项目都没有这个。若是没有 README,那么就须要看文件,该看哪一个文件?这就让人很抓狂了;
  • 关键代码没有注释,好比库的 API,关键的函数,很差懂的代码段落。若是看标准库,绝大部分能够调用的 API 都有很好的注释,没有注释怎么调用呢?只能看代码实现了,若是每次调用都要看一遍实现,真的很难受了;
  • 库没有 Example,库是一种要求很高的包,就是给别人使用的包,好比标准库。绝大部分的标准库的包,都有 Example,由于没有 Example 很难设计出合理的 API。

先看关键代码的注释,有些注释彻底是代码的重复,没有任何存在的意义,惟一的存在就是提升代码的“注释率”,这又有什么用呢,好比下面代码:

wsconn *Conn //ws connection

// The RPC call.
type rpcCall struct {

// Setup logger.
if err := SetupLogger(......); err != nil {

// Wait for os signal
server.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 string's 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.
func ParseInt(s string, base int, bitSize int) (i int64, err error) {
复制代码

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

我们再看 Example,一种特殊的 test,可能不会执行,它的主要做用是为了推演接口是否合理,固然也就提供了如何使用库的例子,这就要求 Example 必须覆盖到库的主要使用场景。举个例子,有个库须要方式 SSRF 攻击,也就是检查 HTTP Redirect 时的 URL 规则,最初咱们是这样提供这个库的:

func NewHttpClientNoRedirect() *http.Client {
复制代码

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

func ExampleNoRedirectClient() {
	url := "http://xxx/yyy"

	client := ssrf.NewHttpClientNoRedirect()
	Req, err := http.NewRequest("GET", url, nil)
	if err != 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 会是这样:

func ExampleNoRedirectClient() {
	client := http.Client{}

	//Must specify checkRedirect attribute to NewFuncNoRedirect
	client.CheckRedirect = ssrf.NewFuncNoRedirect()

	Req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		fmt.Println("failed to create request")
		return
	}

	resp, err := client.Do(Req)
}
复制代码

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

其余工程化

最近得知 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 项目虽然愈来愈庞大,可是总体研发效率却很高。

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/signalreflectruntimesynctestingtimeunsafe 等等。我可能乐观的估计 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 版本,若出现不兼容性的语言和标准库,对用户也不是个好结果,甚至仍是有害的。

Others

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

GC

GC 通常是 C/C 程序员对于 Go 最多见、也是最早想到的一个质疑,GC 这玩意儿能行吗?咱们之前 C/C 程序都是本身实现内存池的,咱们内存分配算法很是牛逼的。

Go 的 GC 优化之路,能够详细读 Getting to Go: The Journey of Go's Garbage Collector

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

上图是 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 来获取数据,实例代码以下:

package main

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

func main() {
	http.ListenAndServe("localhost:6060", nil)
}
复制代码

启动程序后,执行命令就能够拿到结果(因为上面的例子中没有 GC,下面的数据取的是另外程序的部分数据):

$ curl 'http://localhost:6060/debug/pprof/allocs?debug=1' 2>/dev/null |grep PauseNs
# PauseNs = [205683 79032 202102 82216 104853 142320 90058 113638 152504 
145965 72047 49690 158458 60499 99610 112754 122262 52252 49234 68420 159857 
97940 226085 103644 135428 245291 141997 92470 79974 132817 74634 65653 73582 
47399 51653 86107 48619 62583 68906 131868 111903 85482 44531 74585 50162 
31445 107397 10903081771 92603 58585 96620 40416 29763 102248 32804 49394 
83715 77099 108983 66133 47832 35379 143949 69235 27820 35677 99430 104303 
132657 63542 39434 126418 63845 167969 116438 68904 77899 136506 119708 47501]
复制代码

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

Declaration Syntax

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

int (*signal(int, void (*fp)(int)))(int);
复制代码

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

func signal(a int, b func(int)) func(int)int
复制代码

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

固然实际上 C 语言若是借助 typedef 也是能得到比较好的可读性的:

typedef void (*PFP)(int);
typedef int (*PRET)(int);
PRET signal(int a, PFP b);
复制代码

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

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

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

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-oryxv0.1.13 Supports 10k(2CPUs) for RTMP players。因为仅仅是语言的差别而重写一个项目,没有找到更好的方式或理由,以为很不值得,因此仍是放弃了 Go 语言版本,只维护 C++ 版本的 SRS。Go 目前通常在 API 服务器用得比较多,可否在流媒体服务器中应用?答案是确定的,我已经实现过了。

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

目前 Go 能够做为 SRS 前面的代理,实现多核的优点,参考 go-oryx

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

做者简介
杨成立(花名:忘篱),阿里巴巴高级技术专家。他发起并维护了基于 MIT 协议的开源流媒体服务器项目 - SRS(Simple Rtmp Server)。感兴趣的同窗能够扫描下方二维码进入钉钉群,直面和大神进行交流!

云原生技术公开课




本课程是由 CNCF 官方与阿里巴巴强强联合,共同推出的以“云原生技术体系”为核心、以“技术解读”和“实践落地”并重的系列 技术公开课

阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,作最懂云原生开发者的技术圈。”

相关文章
相关标签/搜索