Go 逃逸分析的缺陷

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。## 序先閱讀這個由四部分組成的系列文章,對理解逃逸分析和資料語義會有協助。下面詳細介紹了閱讀逃逸分析報告和 pprof 輸出的方法。(GCTT 已經在翻譯中)<https://www.ardanlabs.com/blog/2017/05/language-mechanics-on-stacks-and-pointers.html>## 介紹即使使用了 Go 4 年,我仍然會被這門語言震驚到。多虧了編譯器執行的靜態程式碼分析,編譯器可以對其產生的程式碼進行一些有趣的最佳化。編碼器執行的其中一種分析稱為逃逸分析。這會對記憶體管理進行最佳化和簡化。在過去的兩年中,(Go)語言團隊一直致力於最佳化編譯器產生的程式碼,以獲得更好的效能,並且做了極其出色的工作。我相信,如果逃逸分析中的一些現有的缺陷得以解決,那麼,Go 程式會得到更大的改進。早在 2015 年 2 月,Dmitry Vyukov 就撰寫了這篇文章,概述了編譯器已知的逃逸分析缺陷。https://docs.google.com/document/d/1CxgUBPlx9iJzkz9JWkb6tIpTe5q32QDmz8l0BouG0Cw/edit我很好奇,自這篇文章以來,當中有多少提及的缺陷被修複了,然後,我發現,迄今為止,只有少數一些缺陷得到瞭解決。也就是說,有五個特定的缺陷尚未被修複,而我很樂意看到在 Go 不久的將來發布的版本中能有所改善。我將這些缺陷標記為: * 間接賦值(Indirect Assignment) * 間接調用(Indirect Call) * 切片和 Map 賦值(Slice and Map Assignments) * 介面(Interfaces) * 未知缺陷(Unknown)我認為,探索這些缺陷是很有趣的,所以,你可以看到它們被修複後對現有 Go 程式的積極影響。下面你所看到的所有東西都基於 1.9 編譯器。## 間接賦值“間接賦值(Indirect Assignment)”缺陷與通過間接分配值時發生的分配有關。這是一個程式碼範例:**代碼清單 1** <https://github.com/ardanlabs/gotraining/blob/master/topics/go/language/pointers/flaws/example1/example1_test.go>```gopackage flawsimport "testing"func BenchmarkAssignmentIndirect(b *testing.B) {type X struct {p *int}for i := 0; i < b.N; i++ {var i1 intx1 := &X{p: &i1, // GOOD: i1 does not escape}_ = x1var i2 intx2 := &X{}x2.p = &i2 // BAD: Cause of i2 escape}}```在代碼清單 1 中,類型 `X` 擁有單個欄位,這個欄位的名字是 `p`,它是一個指向整型的指標。然後在第 11 行到第 13 行中,構造了一個類型為 `X` 的值,使用緊湊形式,用 `i1` 變數的地址來初始化 `p` 欄位。`x1` 變數是作為一個指標建立的,因此,這個變數與在第 17 行建立的變數是一樣的。在第 16 行中,聲明了名為 `i2` 的變數,然後在第 17 行中,構造了一個使用指標語義的類型為 `X` 的值,然後將其賦值給指標變數 `x2`。接著在第 18 行中,`i2` 變數的地址被賦給變數 `x2` 執行的值中的 `p` 欄位。在這個語句中,存在通過使用指標變數的賦值,這是一種間接賦值。以下是運行基準測試的結果,以及一份逃逸分析報告。還包括了 pprof list 命令的輸出。**基準測試輸出**```console$ go test -gcflags "-m -m" -run none -bench . -benchmem -memprofile mem.outBenchmarkAssignmentIndirect-8 100000000 14.2 ns/op 8 B/op 1 allocs/op```**逃逸分析報告**```./example2_test.go:18:10: &i2 escapes to heap./example2_test.go:18:10: from x2.p (star-dot-equals) at ./example2_test.go:18:8./example2_test.go:16:7: moved to heap: i2./example2_test.go:12:7: BenchmarkAssignmentIndirect &i1 does not escape```**Pprof 輸出**```$ go tool pprof -alloc_space mem.outROUTINE ========================759.51MB 759.51MB (flat, cum) 100% of Total. . 11: x1 := &X{. . 12: p: &i1, // GOOD: i1 does not escape. . 13: }. . 14: _ = x1. . 15:759.51MB 759.51MB 16: var i2 int. . 17: x2 := &X{}. . 18: x2.p = &i2 // BAD: Cause of i2 escape. . 19: }. . 20:}```在逃逸分析報告中,`i2` 逃逸給出的理由是,`(star-dot-equals)`。我想這是指編譯器需要執行諸如以下的操作來完成此賦值。**Star-Dot-Equals**```go(*x2).p = &i2```pprof 輸出清晰地顯示,`i2` 是在堆上分配的,而 `i1` 不是。我在 Go 語言小萌新寫的 Go 代碼中,大量看到 16 行到 18 行這樣的代碼。這個缺陷可以協助更萌新的開發人員從堆中移除一些垃圾。## 間接調用“間接調用(Indirect Call)”缺陷與和通過間接調用的函數共用一個值時發生的分配有關。下面是一個程式碼範例:**代碼清單 2.1** <https://github.com/ardanlabs/gotraining/blob/master/topics/go/language/pointers/flaws/example2/example2_test.go>```gopackage flawsimport "testing"func BenchmarkLiteralFunctions(b *testing.B) {for i := 0; i < b.N; i++ {var y1 intfoo(&y1, 42) // GOOD: y1 does not escapevar y2 intfunc(p *int, x int) {*p = x}(&y2, 42) // BAD: Cause of y2 escapevar y3 intp := foop(&y3, 42) // BAD: Cause of y3 escape}}func foo(p *int, x int) {*p = x}```在代碼清單 2.1 中,在第 21 行聲明了一個名為 `foo` 的命名函數。這個函數接受一個整型的地址和一個整型值作為參數。然後,這個函數將傳遞的整型值賦值給 `p` 指標指向的位置。在第 07 行,聲明了一個類型為 `int`,名字為 `y1` 的變數,這個變數在第 08 行對 `foo` 的函數調用過程中發生了共用。從第 10 行到第 13 行,存在類似的情況。聲明了一個類型為 `int` 的變數 `y2`,然後這個變數作為第一個參數共用給一個在第 13 行聲明和執行的字面函數。這個字面函數與 `foo` 函數相同。最後,在第 15 行到第 17 行之間,`foo`函數被賦給一個名為 `p` 的變數。通過變數 `p`,`foo` 函數被執行,其中,變數 `y3` 被共用。第 17 行的這個函數調用是通過 `p` 變數間接完成的。這與第 13 行的字面函數沒有顯式函數變數所執行的函數調用方式情況相同。以下是運行基準測試的結果,以及一份逃逸分析報告。還包括了 pprof list 命令的輸出。**基準測試輸出**```$ go test -gcflags "-m -m" -run none -bench BenchmarkLiteralFunctions -benchmem -memprofile mem.outBenchmarkLiteralFunctions-8 50000000 30.7 ns/op 16 B/op 2 allocs/op```**逃逸分析報告**```./example2_test.go:13:5: &y2 escapes to heap./example2_test.go:13:5: from (func literal)(&y2, 42) (parameter to indirect call) at ./example2_test.go:13:4./example2_test.go:10:7: moved to heap: y2./example2_test.go:17:5: &y3 escapes to heap./example2_test.go:17:5: from p(&y3, 42) (parameter to indirect call) at ./example2_test.go:17:4./example2_test.go:15:7: moved to heap: y3```**Pprof 輸出**```$ go tool pprof -alloc_space mem.outROUTINE ======================== 768.01MB 768.01MB (flat, cum) 100% of Total. . 5:func BenchmarkLiteralFunctions(b *testing.B) {. . 6: for i := 0; i < b.N; i++ {. . 7: var y1 int. . 8: foo(&y1, 42) // GOOD: y1 does not escape. . 9: 380.51MB 380.51MB 10: var y2 int. . 11: func(p *int, x int) {. . 12: *p = x. . 13: }(&y2, 42) // BAD: Cause of y2 escape. . 14: 387.51MB 387.51MB 15: var y3 int. . 16: p := foo. . 17: p(&y3, 42) // BAD: Cause of y3 escape. . 18: }. . 19:}```在逃逸分析報告中,為變數 `y2` 和 `y3` 變數的分配給出的原因是 `(parameter to indirect call)`。pprof 輸出很清楚的顯示出,`y2` 和 `y3` 被分配在堆上,而 `y1` 不是。雖然,我會認為在第 13 行調用的函數字面量的使用是代碼異味,但是,第 16 行變數 `p` 的使用並不是。在 Go 中,人們總是會傳遞函數作為參數。特別是在構建 web 服務的時候。修複這個間接調用缺陷會協助減少 Go web 服務應用中的許多分配。這裡是一個你會在許多 web 服務應用中找到的例子。**代碼清單 2.2** <https://github.com/ardanlabs/gotraining/blob/master/topics/go/language/pointers/flaws/example2/example2_http_test.go>```gopackage flawsimport ( "net/http" "testing")func BenchmarkHandler(b *testing.B) { // Setup route with specific handler. h := func(w http.ResponseWriter, r *http.Request) error { // fmt.Println("Specific Request Handler") return nil } route := wrapHandler(h) // Execute route. for i := 0; i < b.N; i++ { var r http.Request route(nil, &r) // BAD: Cause of r escape }}type Handler func(w http.ResponseWriter, r *http.Request) errorfunc wrapHandler(h Handler) Handler { f := func(w http.ResponseWriter, r *http.Request) error { // fmt.Println("Boilerplate Code") return h(w, r) } return f}```在代碼清單 2.2 中,第 26 行聲明了一個通用的處理器封裝函數,該函數在另一個字面函數的範圍內封裝了一個處理器函數,以提供樣板代碼。然後在第 11 行,聲明了一個用於特定路由的處理函數,然後在第 15 行,它被傳給 `wrapHandler` 函數,以便可以與樣板代碼處理函數連結在一起。在第 19 行,建立了一個 `http.Request` 值,然後與第 20 行的 `route` 調用共用。調用 `route` 在功能上同時執行了樣板代碼和特定的要求處理常式。第 20 行的 `route` 調用屬於間接調用,因為 `route` 變數是一個函數變數。這會導致 `http.Request` 變數分配在堆上,這是沒有必要的。以下是運行基準測試的結果,以及一份逃逸分析報告。還包括了 pprof list 命令的輸出。**基準測試輸出**```$ go test -gcflags "-m -m" -run none -bench BenchmarkHandler -benchmem -memprofile mem.outBenchmarkHandler-8 20000000 72.4 ns/op 256 B/op 1 allocs/op```**逃逸分析報告**```./example2_http_test.go:20:14: &r escapes to heap./example2_http_test.go:20:14: from route(nil, &r) (parameter to indirect call) at ./example2_http_test.go:20:8./example2_http_test.go:19:7: moved to heap: r```**Pprof 輸出**```$ go tool pprof -alloc_space mem.outROUTINE ======================== 5.07GB 5.07GB (flat, cum) 100% of Total. . 14: }. . 15: route := wrapHandler(h). . 16:. . 17: // Execute route.. . 18: for i := 0; i < b.N; i++ { 5.07GB 5.07GB 19: var r http.Request. . 20: route(nil, &r) // BAD: Cause of r escape. . 21: }. . 22:}```在逃逸分析報告中,你可以看到這種分配的原因是 `(parameter to indirect call)`。pprof 報告顯示,`r` 變數正在分配。如前所述,這是人們在用 Go 構建 web 服務時編寫的常見代碼。修複這個缺陷會減少程式中大量的分配。## 切片和 Map 賦值“切片和 Map 賦值(Slice and Map Assignments)”缺陷與值在切片或者 Map 中共用時發生的分配有關。這裡是一個程式碼範例:**代碼清單 3** <https://github.com/ardanlabs/gotraining/blob/master/topics/go/language/pointers/flaws/example3/example3_test.go>```gopackage flawsimport "testing"func BenchmarkSliceMapAssignment(b *testing.B) { for i := 0; i < b.N; i++ { m := make(map[int]*int) var x1 int m[0] = &x1 // BAD: cause of x1 escape s := make([]*int, 1) var x2 int s[0] = &x2 // BAD: cause of x2 escape }}```在代碼清單 3 中,第 07 行建立了一個 map,它檔案類型 `int` 的值的地址。然後在第 08 行,建立了一個類型 `int` 的值,接著在第 09 行,在 map 中共用了這個值,map 的鍵為 0。在第 11 行儲存 `int` 地址的切片上也發生了同樣的事情。在建立切片後,索引 0 內共用了類型為 `int` 的值。以下是運行基準測試的結果,以及一份逃逸分析報告。還包括了 pprof list 命令的輸出。**基準測試輸出**```$ go test -gcflags "-m -m" -run none -bench . -benchmem -memprofile mem.outBenchmarkSliceMapAssignment-8 10000000 104 ns/op 16 B/op 2 allocs/op```**逃逸分析報告**```./example3_test.go:9:10: &x1 escapes to heap./example3_test.go:9:10: from m[0] (value of map put) at ./example3_test.go:9:8./example3_test.go:8:7: moved to heap: x1./example3_test.go:13:10: &x2 escapes to heap./example3_test.go:13:10: from s[0] (slice-element-equals) at ./example3_test.go:13:8./example3_test.go:12:7: moved to heap: x2./example3_test.go:7:12: BenchmarkSliceMapAssignment make(map[int]*int) does not escape./example3_test.go:11:12: BenchmarkSliceMapAssignment make([]*int, 1) does not escape```**Pprof 輸出**```$ go tool pprof -alloc_space mem.outROUTINE ======================== 162.50MB 162.50MB (flat, cum) 100% of Total. . 5:func BenchmarkSliceMapAssignment(b *testing.B) {. . 6: for i := 0; i < b.N; i++ {. . 7: m := make(map[int]*int) 107.50MB 107.50MB 8: var x1 int. . 9: m[0] = &x1 // BAD: cause of x1 escape. . 10:. . 11: s := make([]*int, 1) 55MB 55MB 12: var x2 int. . 13: s[0] = &x2 // BAD: cause of x2 escape. . 14: }. . 15:}```逃逸分析報告中給出的原因是 `(value of map put)` 和 `(slice-element-equals)`。更有趣的是,逃逸分析報告顯示,map 和切片資料結構不分配(不逃逸)。**不分配 Map 和切片**```./example3_test.go:7:12: BenchmarkSliceMapAssignment make(map[int]*int) does not escape./example3_test.go:11:12: BenchmarkSliceMapAssignment make([]*int, 1) does not escape```這進一步證明,程式碼範例中的 `x1` 和 `x2` 無需在堆上分配。我一直認為,在合理和實際的情況下,map 和切片中的資料應該作為值儲存。特別是當這些資料結構正儲存著一個請求或任務的核心資料的時候。這個缺陷為嘗試避免通過使用指標來儲存資料提供了另一個理由。修複這個缺陷可能幾乎沒有什麼回報,因為靜態大小的 map 和切片很少見。## 介面“介面(Interfaces)”缺陷與之前看到的“間接調用”缺陷有關。這是一個使用介面產生實際成本的缺陷。下面是一個程式碼範例:**代碼清單 4** <https://github.com/ardanlabs/gotraining/blob/master/topics/go/language/pointers/flaws/example4/example4_test.go>```gopackage flawsimport "testing"type Iface interface { Method()}type X struct { name string}func (x X) Method() {}func BenchmarkInterfaces(b *testing.B) { for i := 0; i < b.N; i++ { x1 := X{"bill"} var i1 Iface = x1 var i2 Iface = &x1 i1.Method() // BAD: cause copy of x1 to escape i2.Method() // BAD: cause x1 to escape x2 := X{"bill"} foo(x2) foo(&x2) }}func foo(i Iface) { i.Method() // BAD: cause value passed in to escape}```在代碼清單 4 中,在第 05 行聲明了一個名為 `Iface` 的介面,並且為了樣本目的,這個介面保持得非常簡單。然後,在第 09 行聲明了一個名為 `X` 的具體類型,並且,使用值接收器來實現 `Iface` 介面。在第 17 行中,構建了一個類型為 `X` 的值,然後將其賦給 `x1` 變數。在第 18 行,`x1` 變數的一個拷貝儲存在 `i1` 介面變數中,接著,在第 19 行,相同的 `x1` 變數與 `i2` 介面變數共用。在第 21 和 22 行,同時對 `i1` 和 `i2` 介面變數調用 `Method`。為了建立一個更實際的例子,在第 30 行聲明了一個名為 `foo` 的函數,它接受任何實現 `Iface` 介面的具體資料。然後,在第 31 行,對本地介面變數同樣調用 `Method`。`foo` 函數代表了大家在 Go 中寫的大量函數。在第 24 行,構造了一個類型為 `X` 名為 `x2` 的變數,然後將其作為拷貝傳遞給 `foo`,並分別在第 25 和 26 行中共用。以下是運行基準測試的結果,以及一份逃逸分析報告。還包括了 pprof list 命令的輸出。**基準測試輸出**```$ go test -gcflags "-m -m" -run none -bench . -benchmem -memprofile mem.outBenchmarkInterfaces-8 10000000 126 ns/op 64 B/op 4 allocs/op```**逃逸分析報告**```./example4_test.go:18:7: x1 escapes to heap./example4_test.go:18:7: from i1 (assigned) at ./example4_test.go:18:7./example4_test.go:18:7: from i1.Method() (receiver in indirect call) at ./example4_test.go:21:12./example4_test.go:19:7: &x1 escapes to heap./example4_test.go:19:7: from i2 (assigned) at ./example4_test.go:19:7./example4_test.go:19:7: from i2.Method() (receiver in indirect call) at ./example4_test.go:22:12./example4_test.go:19:18: &x1 escapes to heap./example4_test.go:19:18: from &x1 (interface-converted) at ./example4_test.go:19:7./example4_test.go:19:18: from i2 (assigned) at ./example4_test.go:19:7./example4_test.go:19:18: from i2.Method() (receiver in indirect call) at ./example4_test.go:22:12./example4_test.go:17:17: moved to heap: x1./example4_test.go:25:6: x2 escapes to heap./example4_test.go:25:6: from x2 (passed to call[argument escapes]) at ./example4_test.go:25:6./example4_test.go:26:7: &x2 escapes to heap./example4_test.go:26:7: from &x2 (passed to call[argument escapes]) at ./example4_test.go:26:6./example4_test.go:26:7: &x2 escapes to heap./example4_test.go:26:7: from &x2 (interface-converted) at ./example4_test.go:26:7./example4_test.go:26:7: from &x2 (passed to call[argument escapes]) at ./example4_test.go:26:6./example4_test.go:24:17: moved to heap: x2```**Pprof 輸出**```$ go tool pprof -alloc_space mem.outROUTINE ======================== 658.01MB 658.01MB (flat, cum) 100% of Total. . 12:. . 13:func (x X) Method() {}. . 14:. . 15:func BenchmarkInterfaces(b *testing.B) {. . 16: for i := 0; i < b.N; i++ { 167.50MB 167.50MB 17: x1 := X{"bill"} 163.50MB 163.50MB 18: var i1 Iface = x1. . 19: var i2 Iface = &x1. . 20:. . 21: i1.Method() // BAD: cause copy of x1 to escape. . 22: i2.Method() // BAD: cause x1 to escape. . 23: 163.50MB 163.50MB 24: x2 := X{"bill"} 163.50MB 163.50MB 25: foo(x2). . 26: foo(&x2). . 27: }. . 28:}```注意,在基準報告中有四個分配。這是因為代碼會複製 `x1` 和 `x2` 變數,這也會產生分配。在第 18 行中使用`x1` 變數進行賦值時,以及在第 25 行中對 `foo` 進行函數調用使用 `x2` 的值時,建立了這些副本。在逃逸分析報告中,為 `x1` 以及 `x1` 的副本逃逸提供的原因是 `(receiver in indirect call)`。這很有趣,因為第 21 和 22 行對 `Method` 的調用才是這個缺陷真正的罪魁禍首。請記住,針對介面的方法調用需要通過 iTable 進行間接調用。正如你之前看到的,間接調用是逃逸分析中的一個缺陷。逃逸分析報告為 `x2` 變數逃逸給出的原因是 `(passed to call[argument escapes])`。但是在這兩種情況下,`(interface-converted)` 是另一個原因,它描述了資料存放區在介面裡的事實。有趣的是,如果你移除第 31 行中 `foo` 函數裡的方法調用,那麼,分配就會消失。實際上,第 21,22 和 `foo` 中的 31 行中,通過介面變數對 `Method` 的間接調用才是問題所在。我總是在說,從 1.9 甚至更早的版本開始,使用介面會產生間接和分配的開銷。這是逃逸分析的缺陷,如果修正這一缺陷,會給 Go 程式帶來最大的影響。這可以減少單獨日誌包的大量分配。不要使用介面,除非它們(指介面)提供的價值是顯著的。## 未知這種類型的分配是某些我完全不明白的東東。即使在看了工具的輸出,還是沒搞明白。這裡,我把它們供出,期望能得到一些答案。下面是程式碼範例。**代碼清單 5** <https://github.com/ardanlabs/gotraining/blob/master/topics/go/language/pointers/flaws/example5/example5_test.go>```gopackage flawsimport ( "bytes" "testing")func BenchmarkUnknown(b *testing.B) { for i := 0; i < b.N; i++ { var buf bytes.Buffer buf.Write([]byte{1}) _ = buf.Bytes() }}```在代碼清單 5 中,第 10 行建立了一個類型為 `bytes.Buffer` 的值,並將其設定為零值。然後,在第 11 行構造了一個切片值,並將其傳遞給 `buf` 變數上的 `Write` 方法調用。最後,為了防止潛在的編譯器最佳化拋出所有的代碼,調用 Bytes 方法。該調用不是創造 `buf` 變數逃逸的必要條件。以下是運行基準測試的結果,以及一份逃逸分析報告。還包括了 pprof list 命令的輸出。**基準測試輸出**```$ go test -gcflags "-m -m" -run none -bench . -benchmem -memprofile mem.outBenchmark-8 20000000 50.8 ns/op 112 B/op 1 allocs/op```**逃逸分析報告**```./example5_test.go:11:6: buf escapes to heap./example5_test.go:11:6: from buf (passed to call[argument escapes]) at ./example5_test.go:11:12```**Pprof 輸出**```$ go tool pprof -alloc_space mem.outROUTINE ======================== 2.19GB 2.19GB (flat, cum) 100% of Total. . 8:func BenchmarkUnknown(b *testing.B) {. . 9: for i := 0; i < b.N; i++ { 2.19GB 2.19GB 10: var buf bytes.Buffer. . 11: buf.Write([]byte{1}). . 12: _ = buf.Bytes(). . 13: }. . 14:}```在這個代碼中,我沒有看到第 11 行對 `Write` 的方法調用引起逃逸的任何原因。我得到了一個看起來很有意思的指引,但我會留給你去進一步探索。_這可能與 `Buffer` 類型的引導數組有關。它意味著一種最佳化,但是從逃逸分析的角度來說,它讓 `Buffer` 指向自身,這是一種循環相依性,通常難以分析。或者也許是因為 `append`,又或者也許只是幾個因素和 `Buffer` 中非常複雜的代碼的組合。_**有這個問題,它與導致這種分配的引導數組有關:**[cmd/compile, bytes: bootstrap array causes bytes.Buffer to always be heap-allocated](https://github.com/golang/go/issues/7921)**CKS 在 reddit 上發布了此回複:**`Unknown` 情況下的逃逸是因為 Go 認為給 bytes.Buffer.Write() 的參數逃逸。如果你在 buffer 包的原始碼上運行逃逸分析,那麼它會輸出(對於 Write()):```./buffer.go:170:46: leaking param content: p./buffer.go:170:46: from *p (indirection) at ./buffer.go:170:46./buffer.go:170:46: from copy(b.buf[m:], p) (copied slice) at ./buffer.go:176:13(The line numbers are for the current git tip; they may be slightly off in other copies.)```考慮到 copy() 是語言內建函數,似乎編譯器應該知道這裡,源參數不逃逸。或者有可能編譯器在對 copy() 的實際實現做一些十分有趣的事情,以至於源在某些情況下會逃逸。## 總結我試圖指出 1.9 版本至今存在的一些更有趣的逃逸分析缺陷。介面缺陷可能是一旦修複就會對當今的 Go 程式產生最大影響的缺陷。我覺得最有意思的是,我們所有人都能從這些缺陷的修複中獲益,而不需要有這方面的個人專長。編譯器執行的靜態程式碼分析提供了諸多好處,例如最佳化你隨時寫入的代碼的能力。也許最大的好處是,消除或減少你不得不維持的認知負擔。

via: https://www.ardanlabs.com/blog/2018/01/escape-analysis-flaws.html

作者:William Kennedy 譯者:ictar 校對:polaris1119

本文由 GCTT 原創編譯,Go語言中文網 榮譽推出

本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽

1145 次點擊  ∙  1 贊  
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.