Go 开发关键技术指南 | 敢问路在何方?(内含超全知识大图)
作者 | 楊成立(忘籬) 阿里巴巴高級(jí)技術(shù)專家
Go 開(kāi)發(fā)關(guān)鍵技術(shù)指南文章目錄:
- 為什么你要選擇 Go?
- Go 面向失敗編程
- 帶著服務(wù)器編程金剛經(jīng)走進(jìn) 2020 年
- 敢問(wèn)路在何方?
Go 開(kāi)發(fā)指南大圖
Engineering
我覺(jué)得 Go 在工程上良好的支持,是 Go 能夠在服務(wù)器領(lǐng)域有一席之地的重要原因。這里說(shuō)的工程友好包括:
- gofmt 保證代碼的基本一致,增加可讀性,避免在爭(zhēng)論不清楚的地方爭(zhēng)論;
- 原生支持的 profiling,為性能調(diào)優(yōu)和死鎖問(wèn)題提供了強(qiáng)大的工具支持;
- utest 和 coverage,持續(xù)集成,為項(xiàng)目的質(zhì)量提供了良好的支撐;
- example 和注釋,讓接口定義更友好合理,讓庫(kù)的質(zhì)量更高。
GOFMT 規(guī)范編碼
之前有段時(shí)間,朋友圈霸屏的新聞是碼農(nóng)因?yàn)榇a不規(guī)范問(wèn)題槍擊同事,雖然實(shí)際上槍擊案可能不是因?yàn)榇a規(guī)范,但可以看出大家對(duì)于代碼規(guī)范問(wèn)題能引發(fā)槍擊是毫不懷疑的。這些年在不同的公司碼代碼,和不同的人一起碼代碼,每個(gè)地方總有人喜歡糾結(jié)于 if () 中是否應(yīng)該有空格,甚至還大開(kāi)懟戒。
Go 語(yǔ)言從來(lái)不會(huì)有這種爭(zhēng)論,因?yàn)橛?gofmt,語(yǔ)言的工具鏈支持了格式化代碼,避免大家在代碼風(fēng)格上白費(fèi)口舌。
比如,下面的代碼看著真是揪心,任何語(yǔ)言都可以寫(xiě)出類似的一坨代碼:
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,", "))} }如果有幾萬(wàn)行代碼都是這樣,是不是有扣動(dòng)扳機(jī)的沖動(dòng)?如果我們執(zhí)行下 gofmt -w t.go 之后,就變成下面的樣子:
package mainimport ("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 只能解決基本的代碼風(fēng)格問(wèn)題,雖然這個(gè)已經(jīng)節(jié)約了不少口舌和唾沫,我想特別強(qiáng)調(diào)幾點(diǎn):
- 有些 IDE 會(huì)在保存時(shí)自動(dòng) gofmt,如果沒(méi)有手動(dòng)運(yùn)行下命令 gofmt -w .,可以將當(dāng)前目錄和子目錄下的所有文件都格式化一遍,也很容易的是不是;
- gofmt 不識(shí)別空行,因?yàn)榭招惺怯幸饬x的,因?yàn)榭招杏幸饬x所以 gofmt 不知道如何處理,而這正是很多同學(xué)經(jīng)常犯的問(wèn)題;
- gofmt 有時(shí)候會(huì)因?yàn)閷?duì)齊問(wèn)題,導(dǎo)致額外的不必要的修改,這不會(huì)有什么問(wèn)題,但是會(huì)干擾 CR 從而影響 CR 的質(zhì)量。
先看空行問(wèn)題,不能隨便使用空行,因?yàn)榭招杏幸饬x。不能在不該空行的地方用空行,不能在該有空行的地方不用空行,比如下面的例子:
package mainimport ("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) }上面的例子看起來(lái)就相當(dāng)?shù)钠孑?#xff0c;if 和 os.Open 之間沒(méi)有任何原因需要個(gè)空行,結(jié)果來(lái)了個(gè)空行;而 defer 和 io.Copy 之間應(yīng)該有個(gè)空行卻沒(méi)有個(gè)空行。空行是非常好的體現(xiàn)了邏輯關(guān)聯(lián)的方式,所以空行不能隨意,非常嚴(yán)重地影響可讀性,要么就是一坨東西看得很費(fèi)勁,要么就是突然看到兩個(gè)緊密的邏輯身首異處,真的讓人很詫異。
上面的代碼可以改成這樣,是不是看起來(lái)很舒服了:
package mainimport ("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 的對(duì)齊問(wèn)題,一般出現(xiàn)在一些結(jié)構(gòu)體有長(zhǎng)短不一的字段,比如統(tǒng)計(jì)信息,比如下面的代碼:
package maintype NetworkStat struct {IncomingBytes int `json:"ib"`OutgoingBytes int `json:"ob"` }func main() { }如果新增字段比較長(zhǎng),會(huì)導(dǎo)致之前的字段也會(huì)增加空白對(duì)齊,看起來(lái)整個(gè)結(jié)構(gòu)體都改變了:
package maintype NetworkStat struct {IncomingBytes int `json:"ib"`OutgoingBytes int `json:"ob"`IncomingPacketsPerHour int `json:"ipp"`DropKiloRateLastMinute int `json:"dkrlm"` }func main() { }比較好的解決辦法就是用注釋,添加注釋后就不會(huì)強(qiáng)制對(duì)齊了。
Profile 性能調(diào)優(yōu)
性能調(diào)優(yōu)是一個(gè)工程問(wèn)題,關(guān)鍵是測(cè)量后優(yōu)化,而不是盲目?jī)?yōu)化。Go 提供了大量的測(cè)量程序的工具和機(jī)制,包括 Profiling Go Programs, Introducing HTTP Tracing,我們也在性能優(yōu)化時(shí)使用過(guò) Go 的 Profiling,原生支持是非常便捷的。
對(duì)于多線程同步可能出現(xiàn)的死鎖和競(jìng)爭(zhēng)問(wèn)題,Go 提供了一系列工具鏈,比如 Introducing the Go Race Detector, Data Race Detector,不過(guò)打開(kāi) race 后有明顯的性能損耗,不應(yīng)該在負(fù)載較高的線上服務(wù)器打開(kāi),會(huì)造成明顯的性能瓶頸。
推薦服務(wù)器開(kāi)啟 http profiling,偵聽(tīng)在本機(jī)可以避免安全問(wèn)題,需要 profiling 時(shí)去機(jī)器上把 profile 數(shù)據(jù)拿到后,拿到線下分析原因。實(shí)例代碼如下:
package mainimport ("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)} }編譯成二進(jìn)制后啟動(dòng) go mod init private.me && go build . && ./private.me,在瀏覽器訪問(wèn)頁(yè)面可以看到各種性能數(shù)據(jù)的導(dǎo)航:http://localhost:6060/debug/pprof/
例如分析 CPU 的性能瓶頸,可以執(zhí)行 go tool pprof private.me http://localhost:6060/debug/pprof/profile,默認(rèn)是分析 30 秒內(nèi)的性能數(shù)據(jù),進(jìn)入 pprof 后執(zhí)行 top 可以看到 CPU 使用最高的函數(shù):
(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 22flat flat% sum% cum cum%27.20s 63.58% 63.58% 27.20s 63.58% runtime.pthread_cond_signal13.07s 30.55% 94.13% 13.08s 30.58% runtime.pthread_cond_wait1.93s 4.51% 98.64% 1.93s 4.51% runtime.usleep0.15s 0.35% 98.99% 0.22s 0.51% main.main除了 top,還可以輸入 web 命令看調(diào)用圖,還可以用 go-torch 看火焰圖等。
UTest 和 Coverage
當(dāng)然工程化少不了 UTest 和覆蓋率,關(guān)于覆蓋 Go 也提供了原生支持 The cover story,一般會(huì)有專門(mén)的 CISE 集成測(cè)試環(huán)境。集成測(cè)試之所以重要,是因?yàn)殡S著代碼規(guī)模的增長(zhǎng),有效的覆蓋能顯著的降低引入問(wèn)題的可能性。
什么是有效的覆蓋?一般多少覆蓋率比較合適?80% 覆蓋夠好了嗎?90% 覆蓋一定比 30% 覆蓋好嗎?我覺(jué)得可不一定,參考 Testivus On Test Coverage。對(duì)于 UTest 和覆蓋,我覺(jué)得重點(diǎn)在于:
- UTest 和覆蓋率一定要有,哪怕是 0.1% 也必須要有,為什么呢?因?yàn)槌霈F(xiàn)故障時(shí)讓老板心里好受點(diǎn)啊,能用數(shù)據(jù)衡量出來(lái)裸奔的代碼有多少;
- 核心代碼和業(yè)務(wù)代碼一定要分離,強(qiáng)調(diào)核心代碼的覆蓋率才有意義,比如整體覆蓋了 80%,核心代碼占 5%,核心代碼覆蓋率為 10%,那么這個(gè)覆蓋就不怎么有效了;
- 除了關(guān)鍵正常邏輯,更應(yīng)該重視異常邏輯,異常邏輯一般不會(huì)執(zhí)行到,而一旦藏有 bug 可能就會(huì)造成問(wèn)題。有可能有些罕見(jiàn)的代碼無(wú)法覆蓋到,那么這部分邏輯代碼,CR 時(shí)需要特別人工 Review。
分離核心代碼是關(guān)鍵。
可以將核心代碼分離到單獨(dú)的 package,對(duì)這個(gè) package 要求更高的覆蓋率,比如我們要求 98% 的覆蓋(實(shí)際上做到了 99.14% 的覆蓋)。對(duì)于應(yīng)用的代碼,具備可測(cè)性是非常關(guān)鍵的,舉個(gè)我自己的例子,go-oryx 這部分代碼是判斷哪些 url 是代理,就不具備可測(cè)性,下面是主要的邏輯:
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)})可以看得出來(lái),關(guān)鍵需要測(cè)試的核心代碼,在于后面如何判斷URL符合定義的規(guī)范,這部分應(yīng)該被定義成函數(shù),這樣就可以單獨(dú)測(cè)試了:
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,覆蓋率請(qǐng)看 gocover: For go-oryx coverage,這樣的代碼可測(cè)性就會(huì)比較好,也能在有限的精力下盡量讓覆蓋率有效。
Note: 可見(jiàn),單元測(cè)試和覆蓋率,并不是測(cè)試的事情,而是代碼本身應(yīng)該提高的代碼“可測(cè)試性”。
另外,對(duì)于 Go 的測(cè)試還有幾點(diǎn)值得說(shuō)明:
- helper:測(cè)試時(shí)如果調(diào)用某個(gè)函數(shù),出錯(cuò)時(shí)總是打印那個(gè)共用的函數(shù)的行數(shù),而不是測(cè)試的函數(shù)。比如 test_helper.go,如果 compare 不調(diào)用 t.Helper(),那么錯(cuò)誤顯示是 hello_test.go:26: Returned: [Hello, world!], Expected: [BROKEN!],調(diào)用 t.Helper() 之后是 hello_test.go:18: Returned: [Hello, world!], Expected: [BROKEN!]`,實(shí)際上應(yīng)該是 18 行的 case 有問(wèn)題,而不是 26 行這個(gè) compare 函數(shù)的問(wèn)題;
- benchmark:測(cè)試時(shí)還可以帶 Benchmark 的,參數(shù)不是 testing.T 而是 testing.B,執(zhí)行時(shí)會(huì)動(dòng)態(tài)調(diào)整一些參數(shù),比如 testing.B.N,還有并行執(zhí)行的 testing.PB. RunParallel,參考 Benchamrk;
- main: 測(cè)試也是有個(gè) main 函數(shù)的,參考 TestMain,可以做一些全局的初始化和處理。
- doc.go: 整個(gè)包的文檔描述,一般是在 package http 前面加說(shuō)明,比如 http doc 的使用例子。
對(duì)于 Helper 還有一種思路,就是用帶堆棧的 error,參考前面關(guān)于 errors 的說(shuō)明,不僅能將所有堆棧的行數(shù)給出來(lái),而且可以帶上每一層的信息。
注意如果 package 只暴露了 interface,比如 go-oryx-lib: aac 通過(guò) NewADTS() (ADTS, error) 返回的是接口 ADTS,無(wú)法給 ADTS 的函數(shù)加 Example;因此我們專門(mén)暴露了一個(gè) ADTSImpl 的結(jié)構(gòu)體,而 New 函數(shù)返回的還是接口,這種做法不是最好的,讓用戶有點(diǎn)無(wú)所適從,不知道該用 ADTS 還是 ADTSImpl。所以一種可選的辦法,就是在包里面有個(gè) doc.go 放說(shuō)明,例如 net/http/doc.go 文件,就是在 package http 前面加說(shuō)明,比如 http doc 的使用例子。
注釋和 Example
注釋和 Example 是非常容易被忽視的,我覺(jué)得應(yīng)該注意的地方包括:
- 項(xiàng)目的 README.md 和 Wiki,這實(shí)際上就是新人指南,因?yàn)樾氯巳绻芏敲淳秃苋菀琢私膺@個(gè)項(xiàng)目的大概情況,很多項(xiàng)目都沒(méi)有這個(gè)。如果沒(méi)有 README,那么就需要看文件,該看哪個(gè)文件?這就讓人很抓狂了;
- 關(guān)鍵代碼沒(méi)有注釋,比如庫(kù)的 API,關(guān)鍵的函數(shù),不好懂的代碼段落。如果看標(biāo)準(zhǔn)庫(kù),絕大部分可以調(diào)用的 API 都有很好的注釋,沒(méi)有注釋怎么調(diào)用呢?只能看代碼實(shí)現(xiàn)了,如果每次調(diào)用都要看一遍實(shí)現(xiàn),真的很難受了;
- 庫(kù)沒(méi)有 Example,庫(kù)是一種要求很高的包,就是給別人使用的包,比如標(biāo)準(zhǔn)庫(kù)。絕大部分的標(biāo)準(zhǔn)庫(kù)的包,都有 Example,因?yàn)闆](méi)有 Example 很難設(shè)計(jì)出合理的 API。
先看關(guān)鍵代碼的注釋,有些注釋完全是代碼的重復(fù),沒(méi)有任何存在的意義,唯一的存在就是提高代碼的“注釋率”,這又有什么用呢,比如下面代碼:
wsconn *Conn //ws connection// The RPC call. type rpcCall struct {// Setup logger. if err := SetupLogger(......); err != nil {// Wait for os signal server.WaitForSignals(如果注釋能通過(guò)函數(shù)名看出來(lái)(比較好的函數(shù)名要能看出來(lái)它的職責(zé)),那么就不需要寫(xiě)重復(fù)的注釋,注釋要說(shuō)明一些從代碼中看不出來(lái)的東西,比如標(biāo)準(zhǔn)庫(kù)的函數(shù)的注釋:
// 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) {標(biāo)準(zhǔn)庫(kù)做得很好的是,會(huì)把參數(shù)名稱寫(xiě)到注釋中(而不是用 @param 這種方式),而且會(huì)說(shuō)明大量的背景信息,這些信息是從函數(shù)名和參數(shù)看不到的重要信息。
咱們?cè)倏?Example,一種特殊的 test,可能不會(huì)執(zhí)行,它的主要作用是為了推演接口是否合理,當(dāng)然也就提供了如何使用庫(kù)的例子,這就要求 Example 必須覆蓋到庫(kù)的主要使用場(chǎng)景。舉個(gè)例子,有個(gè)庫(kù)需要方式 SSRF 攻擊,也就是檢查 HTTP Redirect 時(shí)的 URL 規(guī)則,最初我們是這樣提供這個(gè)庫(kù)的:
func NewHttpClientNoRedirect() *http.Client {看起來(lái)也沒(méi)有問(wèn)題,提供一種特殊的 http.Client,如果發(fā)現(xiàn)有 Redirect 就返回錯(cuò)誤,那么它的 Example 就會(huì)是這樣:
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) }這時(shí)候就會(huì)出現(xiàn)問(wèn)題,我們總是返回了一個(gè)新的 http.Client,如果用戶自己有了自己定義的 http.Client 怎么辦?實(shí)際上我們只是設(shè)置了 http.Client.CheckRedirect 這個(gè)回調(diào)函數(shù)。如果我們先寫(xiě) Example,更好的 Example 會(huì)是這樣:
func ExampleNoRedirectClient() {client := http.Client{}//Must specify checkRedirect attribute to NewFuncNoRedirectclient.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) }那么我們自然知道應(yīng)該如何提供接口了。
其他工程化
最近得知 WebRTC 有 4GB 的代碼,包括它自己的以及依賴的代碼,就算去掉一般的測(cè)試文件和文檔,也有 2GB 的代碼!!!編譯起來(lái)真的是非常耗時(shí)間,而 Go 對(duì)于編譯速度的優(yōu)化,據(jù)說(shuō)是在 Google 有過(guò)驗(yàn)證的,具體我們還沒(méi)有到這個(gè)規(guī)模。具體可以參考 Why so fast?,主要是編譯器本身比 GCC 快 (5X),以及 Go 的依賴管理做的比較好。
Go 的內(nèi)存和異常處理也做得很好,比如不會(huì)出現(xiàn)野指針,雖然有空指針問(wèn)題可以用 recover 來(lái)隔離異常的影響。而 C 或 C 服務(wù)器,目前還沒(méi)有見(jiàn)過(guò)沒(méi)有內(nèi)存問(wèn)題的,上線后就是各種的野指針滿天飛,總有因?yàn)橐爸羔樃闼赖臅r(shí)候,只是或多或少罷了。
按照 Go 的版本發(fā)布節(jié)奏,6 個(gè)月就發(fā)一個(gè)版本,基本上這么多版本都很穩(wěn)定,Go1.11 的代碼一共有 166 萬(wàn)行 Go 代碼,還有 12 萬(wàn)行匯編代碼,其中單元測(cè)試代碼有 32 萬(wàn)行(占 17.9%),使用實(shí)例 Example 有 1.3 萬(wàn)行。Go 對(duì)于核心 API 是全部覆蓋的,提交有沒(méi)有導(dǎo)致 API 不符合要求都有單元測(cè)試保證,Go 有多個(gè)集成測(cè)試環(huán)境,每個(gè)平臺(tái)是否測(cè)試通過(guò)也能看到,這一整套機(jī)制讓 Go 項(xiàng)目雖然越來(lái)越龐大,但是整體研發(fā)效率卻很高。
Go2 Transition
Go2 的設(shè)計(jì)草案在 Go 2 Draft Designs ,而 Go1 如何遷移到 Go2 也是我個(gè)人特別關(guān)心的問(wèn)題,Python2 和 Python3 的那種不兼容的遷移方式簡(jiǎn)直就是噩夢(mèng)一樣的記憶。Go 的提案中,有一個(gè)專門(mén)說(shuō)了遷移的問(wèn)題,參考 Go2 Transition。
Go2 Transition 還不是最終方案,不過(guò)它也對(duì)比了各種語(yǔ)言的遷移,還是很有意思的一個(gè)總結(jié)。這個(gè)提案描述了在非兼容性變更時(shí),如何給開(kāi)發(fā)者挖的坑最小。
目前 Go1 的標(biāo)準(zhǔn)庫(kù)是遵守兼容性原則的,參考 Go 1 compatibility guarantee,這個(gè)規(guī)范保證了 Go1 沒(méi)有兼容性問(wèn)題,幾乎可以沒(méi)有影響的升級(jí)比如從 Go1.2 升級(jí)到 Go1.11。幾乎的意思,是很大概率是沒(méi)有問(wèn)題,當(dāng)然如果用了一些非常冷門(mén)的特性,可能會(huì)有坑,我們遇到過(guò) json 解析時(shí),內(nèi)嵌結(jié)構(gòu)體的數(shù)據(jù)成員也得是 exposed 的才行,而這個(gè)在老版本中是可以非 exposed;還遇到過(guò) cgo 對(duì)于鏈接參數(shù)的變更導(dǎo)致編譯失敗,這些問(wèn)題幾乎很難遇到,都可以算是兼容的吧,有時(shí)候只是把模糊不清的定義清楚了而已。
Go2 在語(yǔ)言和標(biāo)準(zhǔn)庫(kù)上,會(huì)打破 Go1 的兼容性規(guī)范,也就是和 Go1 不再兼容。不過(guò) Go 是分布式開(kāi)源社區(qū)在維護(hù),不能依賴于 flag day,還是要容許不同 Go 版本寫(xiě)的 package 的互操作性。
先了解下各個(gè)語(yǔ)言如何考慮兼容性:
-
C 是嚴(yán)格向后兼容的,很早寫(xiě)的程序總是能在新的編譯器中編譯。另外新的編譯器也支持指定之前的標(biāo)準(zhǔn),比如 -std=c90 使用 ISO C90 標(biāo)準(zhǔn)編譯程序。關(guān)鍵的特性是編譯成目標(biāo)文件后,不同版本的 C 的目標(biāo)文件,能完美的鏈接成執(zhí)行程序;C90 實(shí)際上是對(duì)之前 K&R C 版本不兼容的,主要引入了 volatile 關(guān)鍵字、整數(shù)精度問(wèn)題,還引入了 trigraphs,最糟糕的是引入了 undefined 行為比如數(shù)組越界和整數(shù)溢出的行為未定義。從 C 上可以學(xué)到的是:后向兼容非常重要;非常小的打破兼容性也問(wèn)題不大特別是可以通過(guò)編譯器選項(xiàng)來(lái)處理;能將不同版本的目標(biāo)文件鏈接到一起是非常關(guān)鍵的;undefined 行為嚴(yán)重困擾開(kāi)發(fā)者容易造成問(wèn)題;
-
C 也是 ISO 組織驅(qū)動(dòng)的語(yǔ)言,和 C 一樣也是向后兼容的。C 和C一樣坑爹的地方坑到吐血,比如 undefined行為等。盡管一直保持向后兼容,但是新的C 代碼比如C 11 看起來(lái)完全不同,這是因?yàn)橛行碌母淖兊奶匦?#xff0c;比如很少會(huì)用裸指針、比如 range 代替了傳統(tǒng)的 for 循環(huán),這導(dǎo)致熟悉老C 語(yǔ)法的程序員看新的代碼非常難受甚至看不懂。C 毋庸置疑是非常流行的,但是新的語(yǔ)言標(biāo)準(zhǔn)在這方面沒(méi)有貢獻(xiàn)。從C 上可以學(xué)到的新東西是:盡管保持向后兼容,語(yǔ)言的新版本可能也會(huì)帶來(lái)巨大的不同的感受(保持向后兼容并不能保證能持續(xù)看懂)。
-
Java 也是向后兼容的,是在字節(jié)碼層面和語(yǔ)言層面都向后兼容,盡管語(yǔ)言上不斷新增了關(guān)鍵字。Java 的標(biāo)準(zhǔn)庫(kù)非常龐大,也不斷在更新,過(guò)時(shí)的特性會(huì)被標(biāo)記為 deprecated 并且編譯時(shí)會(huì)有警告,理論上一定版本后 deprecated 的特性會(huì)不可用。Java 的兼容性問(wèn)題主要在 JVM 解決,如果用新的版本編譯的字節(jié)碼,得用新的 JVM 才能執(zhí)行。Java 還做了一些前向兼容,這個(gè)影響了字節(jié)碼啥的(我本身不懂 Java,作者也不說(shuō)自己不是專家,我就沒(méi)仔細(xì)看了)。Java 上可以學(xué)到的新東西是:要警惕因?yàn)楸3旨嫒菪远拗普Z(yǔ)言未來(lái)的改變。
-
Python2.7 是 2010 年發(fā)布的,目前主要是用這個(gè)版本。Python3 是 2006 年開(kāi)始開(kāi)發(fā),2008 年發(fā)布,十年后的今天還沒(méi)有遷移完成,甚至主要是用的 Python2 而不是 Python3,**這當(dāng)然不是 Go2 要走的路。**看起來(lái)是因?yàn)槿狈ο蚝蠹嫒輰?dǎo)致的問(wèn)題,Python3 刻意的和之前版本不兼容,比如 print 從語(yǔ)句變成了一個(gè)函數(shù),string 也變成了 Unicode(這導(dǎo)致和 C 調(diào)用時(shí)會(huì)有很多問(wèn)題)。沒(méi)有向后兼容,同時(shí)還是解釋型語(yǔ)言,這導(dǎo)致 Python2 和 3 的代碼混著用是不可能的,這意味著程序依賴的所有庫(kù)必須支持兩個(gè)版本。Python 支持 from __future__ import FEATURE,這樣可以在 Python2 中用 Python3 的特性。Python 上可以學(xué)到的東西是:向后兼容是生死攸關(guān)的;和其他語(yǔ)言互操作的接口兼容是非常重要的;能否升級(jí)到新的語(yǔ)言是由調(diào)用的庫(kù)支持的。
-
Perl6 是 2000 年開(kāi)始開(kāi)發(fā)的,15 年后才正式發(fā)布,這也不是 Go2 應(yīng)該走的路。這么漫長(zhǎng)的主要原因包括:刻意沒(méi)有向后兼容,只有語(yǔ)言的規(guī)范沒(méi)有實(shí)現(xiàn)而這些規(guī)范不斷的修改。Perl 上可以學(xué)到的東西是:不要學(xué) Perl;設(shè)置期限按期交付;別一下子全部改了。
特別說(shuō)明的是,非常高興的是 Go2 不會(huì)重新走 Python3 的老路子,當(dāng)初被 Python 的版本兼容問(wèn)題坑得不要不要的。
雖然上面只是列舉了各種語(yǔ)言的演進(jìn),確實(shí)也了解得更多了,有時(shí)候描述問(wèn)題本身,反而更能明白解決方案。C 和 C 的向后兼容確實(shí)非常關(guān)鍵,但也不是它們能有今天地位的原因,C11 的新特性到底增加了多少 DAU 呢,確實(shí)是值得思考的。另外 C11 加了那么多新的語(yǔ)言特性,比如 WebRTC 代碼就是這樣,很多老 C 程序員看到后一臉懵逼,和一門(mén)新的語(yǔ)言一樣了,是否保持完全的兼容不能做一點(diǎn)點(diǎn)變更,其實(shí)也不是的。
應(yīng)該將 Go 的語(yǔ)言版本和標(biāo)準(zhǔn)庫(kù)的版本分開(kāi)考慮,這兩個(gè)也是分別演進(jìn)的,例如 alias 是 1.9 引入的向后兼容的特性,1.9 之前的版本不支持,1.9 之后的都支持。語(yǔ)言方面包括:
-
Language additions 新增的特性。比如 1.9 新增的 type alias,這些向后兼容的新特性,并不要求代碼中指定特殊的版本號(hào),比如用了 alias 的代碼不用指定要 1.9 才能編譯,用之前的版本會(huì)報(bào)錯(cuò)。向后兼容的語(yǔ)言新增的特性,是依靠程序員而不是工具鏈來(lái)維護(hù)的,要用這個(gè)特性或庫(kù)升級(jí)到要求的版本就可以。
-
Language removals 刪除的特性。比如有個(gè)提案 #3939 去掉 string(int),字符串構(gòu)造函數(shù)不支持整數(shù),假設(shè)這個(gè)在 Go1.20 版本去掉,那么 Go1.20 之后這種 string(1000) 代碼就要編譯失敗了。這種情況沒(méi)有特別好的辦法能解決,我們可以提供工具,將代碼自動(dòng)替換成新的方式,這樣就算庫(kù)維護(hù)者不更新,使用者自己也能更新。這種場(chǎng)景引出了指定最大版本,類似 C 的 -std=C90,可以指定最大編譯的版本比如 -lang=go1.19,當(dāng)然必須能和 Go1.20 的代碼鏈接。指定最大版本可以在 go.mod 中指定,這需要工具鏈兼容歷史的版本,由于這種特性的刪除不會(huì)很頻繁,維護(hù)負(fù)擔(dān)還是可以接受的。
-
Minimum language version 最小要求版本。為了可以更明確的錯(cuò)誤信息,可以允許模塊在 go.mod 中指定最小要求的版本,這不是強(qiáng)制性的,只是說(shuō)明了這個(gè)信息后編譯工具能明確給出錯(cuò)誤,比如給出應(yīng)該用具體哪個(gè)版本。
-
Language redefinitions 語(yǔ)言重定義。比如 Go1.1 時(shí),int 在 64 位系統(tǒng)中長(zhǎng)度從 4 字節(jié)變成了 8 字節(jié),這會(huì)導(dǎo)致很多潛在的問(wèn)題。比如 #20733 修改了變量在 for 中的作用域,看起來(lái)是解決潛在的問(wèn)題,但也可能會(huì)引入問(wèn)題。引入關(guān)鍵字一般不會(huì)有問(wèn)題,不過(guò)如果和函數(shù)沖突就會(huì)有問(wèn)題,比如 error: check。為了讓 Go 的生態(tài)能遷移到 Go2,語(yǔ)言重定義的事情應(yīng)該盡量少做,因?yàn)槲覀儾辉倌芤蕾嚲幾g器檢查錯(cuò)誤。雖然指定版本能解決這種問(wèn)題,但是這始終會(huì)導(dǎo)致未知的結(jié)果,很有可能一升級(jí) Go 版本就掛了。**我覺(jué)得對(duì)于語(yǔ)言重定義,應(yīng)該完全禁止。**比如 #20733 可以改成禁止這種做法,這樣就會(huì)變成編譯錯(cuò)誤,可能會(huì)幫助找到代碼中潛在的 BUG。
-
Build tags 編譯 tags。在指定文件中指定編譯選項(xiàng),是現(xiàn)有的機(jī)制,不過(guò)是指定的 release 版本號(hào),它更多是指定了最小要求的版本,而沒(méi)有解決最大依賴版本問(wèn)題。
-
Import go2 導(dǎo)入新特性。和 Python 的特性一樣,可以在 Go1 中導(dǎo)入 Go2 的新特性,比如可以顯式地導(dǎo)入 import "go2/type-aliases",而不是在 go.mod 中隱式的指定。這會(huì)導(dǎo)致語(yǔ)言比較復(fù)雜,將語(yǔ)言打亂成了各種特性的組合。而且這種方式一旦使用,將無(wú)法去掉。這種方式看起來(lái)不太適合 Go。
如果有更多的資源來(lái)維護(hù)和測(cè)試,標(biāo)準(zhǔn)庫(kù)后續(xù)會(huì)更快發(fā)布,雖然還是 6 個(gè)月的周期。標(biāo)準(zhǔn)庫(kù)方面的變更包括:
-
Core standard library 核心標(biāo)準(zhǔn)庫(kù)。有些和編譯工具鏈相關(guān)的庫(kù),還有其他的一些關(guān)鍵的庫(kù),應(yīng)該遵守 6 個(gè)月的發(fā)布周期,而且這些核心標(biāo)準(zhǔn)庫(kù)應(yīng)該保持 Go1 的兼容性,比如 os/signal、reflect、runtime、sync、testing、time、unsafe 等等。我可能樂(lè)觀的估計(jì) net, os, 和 syscall 不在這個(gè)范疇。
-
Penumbra standard library 邊緣標(biāo)準(zhǔn)庫(kù)。它們被獨(dú)立維護(hù),但是在一個(gè) release 中一起發(fā)布,當(dāng)前核心庫(kù)大部分都屬于這種。這使得可以用 go get 等工具來(lái)更新這些庫(kù),比 6 個(gè)月的周期會(huì)更快。標(biāo)準(zhǔn)庫(kù)會(huì)保持和前面版本的編譯兼容,至少和前面一個(gè)版本兼容。
-
Removing packages from the standard library 去掉一些不太常用的標(biāo)準(zhǔn)庫(kù),比如 net/http/cgi 等。
如果上述的工作做得很好的話,開(kāi)發(fā)者會(huì)感覺(jué)不到有個(gè)大版本叫做 Go2,或者這種緩慢而自然的變化逐漸全部更新成了 Go2。甚至我們都不用宣傳有個(gè) Go2,既然沒(méi)有 C2.0 為何要 Go2.0 呢?主流的語(yǔ)言比如 C、C 和 Java 從來(lái)沒(méi)有 2.0,一直都是 1.N 的版本,我們也可以模仿它們。事實(shí)上,一般所認(rèn)為的全新的 2.0 版本,若出現(xiàn)不兼容性的語(yǔ)言和標(biāo)準(zhǔn)庫(kù),對(duì)用戶也不是個(gè)好結(jié)果,甚至還是有害的。
Others
關(guān)于 Go,還有哪些重要的技術(shù)值得了解呢?下面將進(jìn)行詳細(xì)的分享。
GC
GC 一般是 C/C 程序員對(duì)于 Go 最常見(jiàn)、也是最先想到的一個(gè)質(zhì)疑,GC 這玩意兒能行嗎?我們以前 C/C 程序都是自己實(shí)現(xiàn)內(nèi)存池的,我們內(nèi)存分配算法非常牛逼的。
Go 的 GC 優(yōu)化之路,可以詳細(xì)讀 Getting to Go: The Journey of Go's Garbage Collector。
2014 年 Go1.4,GC 還是很弱的,是決定 Go 生死的大短板。
上圖是 Twitter 的線上服務(wù)監(jiān)控。Go1.4 的 STW(Stop the World) Pause time 是 300 毫秒,而 Go1.5 優(yōu)化到了 30 毫秒。
而 Go1.6 的 GC 暫停時(shí)間降低到了 3 毫秒左右。
Go1.8 則降低到了 0.5 毫秒左右,也就是 500 微秒。從 Go1.4 到 Go1.8,優(yōu)化了 600 倍性能。
如何看 GC 的 STW 時(shí)間呢?可以引入 net/http/pprof 這個(gè)庫(kù),然后通過(guò) curl 來(lái)獲取數(shù)據(jù),實(shí)例代碼如下:
package mainimport ("net/http"_ "net/http/pprof" )func main() {http.ListenAndServe("localhost:6060", nil) }啟動(dòng)程序后,執(zhí)行命令就可以拿到結(jié)果(由于上面的例子中沒(méi)有 GC,下面的數(shù)據(jù)取的是另外程序的部分?jǐn)?shù)據(jù)):
$ 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 計(jì)算最大值是 322 微秒,最小是 26 微秒,平均值是 81 微秒。
Declaration Syntax
關(guān)于 Go 的聲明語(yǔ)法 Go Declaration Syntax,和 C 語(yǔ)言有對(duì)比,在 The "Clockwise/Spiral Rule" 這個(gè)文章中也詳細(xì)描述了 C 的順時(shí)針語(yǔ)法規(guī)則。其中有個(gè)例子:
int (*signal(int, void (*fp)(int)))(int);這是個(gè)什么呢?翻譯成 Go 語(yǔ)言就能看得很清楚:
func signal(a int, b func(int)) func(int)intsignal 是個(gè)函數(shù),有兩個(gè)參數(shù),返回了一個(gè)函數(shù)指針。signal 的第一個(gè)參數(shù)是 int,第二個(gè)參數(shù)是一個(gè)函數(shù)指針。
當(dāng)然實(shí)際上 C 語(yǔ)言如果借助 typedef 也是能獲得比較好的可讀性的:
typedef void (*PFP)(int); typedef int (*PRET)(int); PRET signal(int a, PFP b);只是從語(yǔ)言的語(yǔ)法設(shè)計(jì)上來(lái)說(shuō),還是 Go 的可讀性確實(shí)會(huì)好一些。這些點(diǎn)點(diǎn)滴滴的小傲嬌,是否可以支撐我們夠浪程序員浪起來(lái)的資本呢?至少 Rob Pike 不是拍腦袋和大腿想出來(lái)的規(guī)則嘛,這種認(rèn)真和嚴(yán)謹(jǐn)是值得佩服和學(xué)習(xí)的。
Documents
新的語(yǔ)言文檔支持都很好,不用買(mǎi)本書(shū)看,Go 也是一樣,Go 官網(wǎng)歷年比較重要的文章包括:
- 語(yǔ)法特性及思考:Go Declaration Syntax, The Laws of Reflection, Constants, Generics Discussion, Another Go at Language Design, Composition not inheritance, Interfaces and other types
- 并發(fā)相關(guān)特性: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
- 錯(cuò)誤處理相關(guān):Defer, Panic, and Recover, Error handling and Go, Errors are values, Stack traces and the errors package, Error Handling In Go, The Error Model
- 性能和優(yōu)化:Profiling Go Programs, Introducing the Go Race Detector, The cover story, Introducing HTTP Tracing, Data Race Detector
- 標(biāo)準(zhǔn)庫(kù)說(shuō)明: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的結(jié)合:C? Go? Cgo!
- 項(xiàng)目相關(guān):Organizing Go code, Package names, Effective Go, versioning, Russ Cox: vgo
- 關(guān)于GC:Go GC: Prioritizing low latency and simplicity, Getting to Go: The Journey of Go Garbage Collector, Proposal: Eliminate STW stack re-scanning
其中,文章中有引用其他很好的文章,我也列出來(lái)哈:
- Go Declaration Syntax,引用了一篇神作,介紹 C 的螺旋語(yǔ)法,寫(xiě) C 的多,讀過(guò)這個(gè)的不多,The "Clockwise/Spiral Rule"
- Strings, bytes, runes and characters in Go,引用了很好的一篇文章,號(hào)稱每個(gè)人都要懂的,關(guān)于字符集和 Unicode 的文章,Every Software Developer Must Know (No Excuses!)
- 為何錯(cuò)誤碼模型,比異常模型更有優(yōu)勢(shì),參考 Cleaner, more elegant, and wrong 以及 Cleaner, more elegant, and harder to recognize。
- Go 中的面向?qū)ο笤O(shè)計(jì)原則 SOLID。
- Go 的版本語(yǔ)義 Semantic Versioning,如何在大型項(xiàng)目中規(guī)范版本,避免導(dǎo)致依賴地獄 (Dependency Hell) 問(wèn)題。
SRS
SRS 是使用 ST,單進(jìn)程單線程,性能是 EDSM 模型的 nginx-rtmp 的 3 到 5 倍,參考 SRS: Performance,當(dāng)然不是 ST 本身性能是 EDSM 的三倍,而是說(shuō) ST 并不會(huì)比 EDSM 性能低,主要還是要根據(jù)業(yè)務(wù)上的特征做優(yōu)化。
關(guān)于 ST 和 EDSM,參考本文前面關(guān)于 Concurrency 對(duì)于協(xié)程的描述,ST 它是 C 的一個(gè)協(xié)程庫(kù),EDSM 是異步事件驅(qū)動(dòng)模型。
SRS 是單進(jìn)程單線程,可以擴(kuò)展為多進(jìn)程,可以在 SRS 中改代碼 Fork 子進(jìn)程,或者使用一個(gè) TCP 代理,比如 TCP 代理 go-oryx: rtmplb。
在 2016 年和 2017 年我用 Go 重寫(xiě)過(guò) SRS,驗(yàn)證過(guò) Go 使用 2CPU 可以跑到 C10K,參考 go-oryx,v0.1.13 Supports 10k(2CPUs) for RTMP players。由于僅僅是語(yǔ)言的差異而重寫(xiě)一個(gè)項(xiàng)目,沒(méi)有找到更好的方式或理由,覺(jué)得很不值得,所以還是放棄了 Go 語(yǔ)言版本,只維護(hù) C 版本的 SRS。Go 目前一般在 API 服務(wù)器用得比較多,能否在流媒體服務(wù)器中應(yīng)用?答案是肯定的,我已經(jīng)實(shí)現(xiàn)過(guò)了。
后來(lái)在 2017 年,終于找到相對(duì)比較合理的方式來(lái)用 Go 寫(xiě)流媒體,就是只提供庫(kù)而不是二進(jìn)制的服務(wù)器,參考 go-oryx-lib。
目前 Go 可以作為 SRS 前面的代理,實(shí)現(xiàn)多核的優(yōu)勢(shì),參考 go-oryx。
**關(guān)注“阿里巴巴云原生”公眾號(hào),回復(fù) ****Go **即可獲取清晰知識(shí)大圖及最全腦圖鏈接!
作者簡(jiǎn)介
楊成立(花名:忘籬),阿里巴巴高級(jí)技術(shù)專家。他發(fā)起并維護(hù)了基于 MIT 協(xié)議的開(kāi)源流媒體服務(wù)器項(xiàng)目 - SRS(Simple Rtmp Server)。感興趣的同學(xué)可以掃描下方二維碼進(jìn)入釘釘群,直面和大神進(jìn)行交流!
云原生技術(shù)公開(kāi)課
本課程是由 CNCF 官方與阿里巴巴強(qiáng)強(qiáng)聯(lián)合,共同推出的以“云原生技術(shù)體系”為核心、以“技術(shù)解讀”和“實(shí)踐落地”并重的系列技術(shù)公開(kāi)課。
“阿里巴巴云原生關(guān)注微服務(wù)、Serverless、容器、Service Mesh 等技術(shù)領(lǐng)域、聚焦云原生流行技術(shù)趨勢(shì)、云原生大規(guī)模的落地實(shí)踐,做最懂云原生開(kāi)發(fā)者的技術(shù)圈。”
《新程序員》:云原生和全面數(shù)字化實(shí)踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的Go 开发关键技术指南 | 敢问路在何方?(内含超全知识大图)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 函数计算支持应用中心功能
- 下一篇: VMware 完成 27 亿美元的 Pi