做者 | 楊成立(忘籬) 阿里巴巴高級技術專家html
Go 開發關鍵技術指南文章目錄:
node
- 爲何你要選擇 Go?
- Go 面向失敗編程
- 帶着服務器編程金剛經走進 2020 年
- 敢問路在何方?
我以爲 Go 在工程上良好的支持,是 Go 可以在服務器領域有一席之地的重要緣由。這裏說的工程友好包括:python
以前有段時間,朋友圈霸屏的新聞是碼農由於代碼不規範問題槍擊同事,雖然實際上槍擊案可能不是由於代碼規範,但能夠看出你們對於代碼規範問題能引起槍擊是絕不懷疑的。這些年在不一樣的公司碼代碼,和不一樣的人一塊兒碼代碼,每一個地方總有人喜歡糾結於 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
gofmt -w .
,能夠將當前目錄和子目錄下的全部文件都格式化一遍,也很容易的是否是;先看空行問題,不能隨便使用空行,由於空行有意義。不能在不應空行的地方用空行,不能在該有空行的地方不用空行,好比下面的例子: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() {
}
複製代碼
比較好的解決辦法就是用註釋,添加註釋後就不會強制對齊了。
性能調優是一個工程問題,關鍵是測量後優化,而不是盲目優化。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 和覆蓋率,關於覆蓋 Go 也提供了原生支持 The cover story
,通常會有專門的 CISE 集成測試環境。集成測試之因此重要,是由於隨着代碼規模的增加,有效的覆蓋能顯著的下降引入問題的可能性。
什麼是有效的覆蓋?通常多少覆蓋率比較合適?80% 覆蓋夠好了嗎?90% 覆蓋必定比 30% 覆蓋好嗎?我以爲可不必定,參考 Testivus On Test Coverage。對於 UTest 和覆蓋,我以爲重點在於:
分離核心代碼是關鍵。
能夠將核心代碼分離到單獨的 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 的測試還有幾點值得說明:
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 函數的問題;testing.T
而是 testing.B
,執行時會動態調整一些參數,好比 testing.B.N,還有並行執行的 testing.PB. RunParallel
,參考 Benchamrk;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 是很是容易被忽視的,我以爲應該注意的地方包括:
先看關鍵代碼的註釋,有些註釋徹底是代碼的重複,沒有任何存在的意義,惟一的存在就是提升代碼的「註釋率」,這又有什麼用呢,好比下面代碼:
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 的設計草案在 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 上能夠學到的東西是:向後兼容是生死攸關的;和其餘語言互操做的接口兼容是很是重要的;可否升級到新的語言是由調用的庫支持的。
特別說明的是,很是高興的是 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,還有哪些重要的技術值得了解呢?下面將進行詳細的分享。
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 微秒。
關於 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 不是拍腦殼和大腿想出來的規則嘛,這種認真和嚴謹是值得佩服和學習的。
新的語言文檔支持都很好,不用買本書看,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? Go? Cgo!
Organizing Go code
, Package names
, Effective Go
, versioning
, Russ Cox: vgo
Go GC: Prioritizing low latency and simplicity
, Getting to Go: The Journey of Go Garbage Collector
, Proposal: Eliminate STW stack re-scanning
其中,文章中有引用其餘很好的文章,我也列出來哈:
Go Declaration Syntax
,引用了一篇神做,介紹 C 的螺旋語法,寫 C 的多,讀過這個的很少,The "Clockwise/Spiral Rule"
Strings, bytes, runes and characters in Go
,引用了很好的一篇文章,號稱每一個人都要懂的,關於字符集和 Unicode 的文章,Every Software Developer Must Know (No Excuses!)
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 **便可獲取清晰知識大圖及最全腦圖連接!
做者簡介
楊成立(花名:忘籬),阿里巴巴高級技術專家。他發起並維護了基於 MIT 協議的開源流媒體服務器項目 - SRS(Simple Rtmp Server)。感興趣的同窗能夠掃描下方二維碼進入釘釘羣,直面和大神進行交流!
「阿里巴巴雲原生關注微服務、Serverless、容器、Service Mesh 等技術領域、聚焦雲原生流行技術趨勢、雲原生大規模的落地實踐,作最懂雲原生開發者的技術圈。」