<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          我為什么放棄Go語(yǔ)言?

          共 17677字,需瀏覽 36分鐘

           ·

          2023-06-05 23:33

          af3f8b4553cdd8839749eb0d9fe1ac38.webp

          97607fad4d23b1f2469717e2fdabb0cb.webp


          ??騰小云導(dǎo)讀

          你在什么時(shí)候會(huì)產(chǎn)生“想要放棄用 Go 語(yǔ)言”的念頭?也許是在用 Go 開(kāi)發(fā)過(guò)程中,接連不斷踩坑的時(shí)候。本文作者提煉和總結(jié)《100 Go Mistakes and How to Avoid Them》里的精華內(nèi)容,并結(jié)合自身的工作經(jīng)驗(yàn),盤(pán)點(diǎn)了 Go 的常見(jiàn)典型錯(cuò)誤,撰寫(xiě)了這篇超全避坑指南。讓我們跟隨文章,一起重拾用 Go 的信心~ ?? 1?注意 shadow 變量 2 慎用 init 函數(shù) 3 embed types 優(yōu)缺點(diǎn) 4?Functional Options Pattern 傳遞參數(shù) 5?小心八進(jìn)制整數(shù) 6?float 的精度問(wèn)題 7?slice 相關(guān)注意點(diǎn) slice 相關(guān)注意點(diǎn) 8 注意 range 9?注意 break 作用域 10?defer 11?string 相關(guān) 12?interface 類(lèi)型返回的非 nil 問(wèn)題 13 Error? 14?happens before 保證 15?Context Values 16?應(yīng)多關(guān)注 goroutine 何時(shí)停止 17?Channel 18?string format 帶來(lái)的 dead lock 19?錯(cuò)誤使用 sync.WaitGroup 20?不要拷貝 sync 類(lèi)型 21?time.After 內(nèi)存泄露 22?HTTP body 忘記 Close 導(dǎo)致的泄露 23?Cache line 24?關(guān)于 False Sharing 造成的性能問(wèn)題 25?內(nèi)存對(duì)齊 26?逃逸分析 27?byte slice 和 string 的轉(zhuǎn)換優(yōu)化 28?容器中的 GOMAXPROCS 29 總結(jié)


          01

          注意 shadow 變量
                    var?client *http.Client
          ??if?tracing {
          ????client, err := createClientWithTracing()
          ????if?err != nil?{
          ??????return?err
          ????}
          ????log.Println(client)
          ??} else?{
          ????client, err := createDefaultClient()
          ????if?err != nil?{
          ??????return?err
          ????}
          ????log.Println(client)
          ??}

          在上面這段代碼中,聲明了一個(gè) client 變量,然后使用 tracing 控制變量的初始化,可能是因?yàn)闆](méi)有聲明 err 的緣故,使用的是 := 進(jìn)行初始化,那么會(huì)導(dǎo)致外層的 client 變量永遠(yuǎn)是 nil。這個(gè)例子實(shí)際上是很容易發(fā)生在我們實(shí)際的開(kāi)發(fā)中,尤其需要注意。
          如果是因?yàn)?err 沒(méi)有初始化的緣故,我們?cè)诔跏蓟臅r(shí)候可以這么做:
                var?client *http.Client
          ??var?err error
          ??if?tracing {
          ????client, err = createClientWithTracing()
          ??} else?{
          ????...
          ??}
          ????if?err != nil?{ // 防止重復(fù)代碼
          ????????return?err
          ????}

          或者內(nèi)層的變量聲明換一個(gè)變量名字,這樣就不容易出錯(cuò)了。
          我們也可以使用工具分析代碼是否有 shadow,先安裝一下工具:
                go install?golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow

          然后使用 shadow 命令:
                go?vet -vettool=C:\Users\luozhiyun\go\bin\shadow.exe?.\main.go
          # command-line-arguments
          .\main.go:15:3: declaration of "client"?shadows declaration at line?13
          .\main.go:21:3: declaration of "client"?shadows declaration at line?13



          02

          慎用 init 函數(shù)
          使用 init 函數(shù)之前需要注意下面幾件事:

          ? ? ? 2.1 init 函數(shù)會(huì)在全局變量之后被執(zhí)行


          init 函數(shù)并不是最先被執(zhí)行的,如果聲明了 const 或全局變量,那么 init 函數(shù)會(huì)在它們之后執(zhí)行:
                package?main

          import?"fmt"

          var?a = func()?int?{
          ??fmt.Println("a")
          ??return?0
          }()

          func?init()?{
          ??fmt.Println("init")
          }

          func?main()?{
          ??fmt.Println("main")
          }

          // output
          a
          init
          main

          ? ? ? 2.2 init 初始化按解析的依賴關(guān)系順序執(zhí)行


          比如 main 包里面有 init 函數(shù),依賴了 redis 包,main 函數(shù)執(zhí)行了 redis 包的 Store 函數(shù),恰好 redis 包里面也有 init 函數(shù),那么執(zhí)行順序會(huì)是:
          3d727947b4bf81909ed712233c4de69a.webp
          還有一種情況,如果是使用? "import _ foo" ?這種方式引入的,也是會(huì)先調(diào)用 foo 包中的 init 函數(shù)。

          ? ? ? 2.3 擾亂單元測(cè)試


          比如我們?cè)?init 函數(shù)中初始了一個(gè)全局的變量,但是單測(cè)中并不需要,那么實(shí)際上會(huì)增加單測(cè)得復(fù)雜度,比如:
                var?db *sql.DB
          func?init(){
          ??dataSourceName := os.Getenv("MYSQL_DATA_SOURCE_NAME")
          ????d, err := sql.Open("mysql", dataSourceName)
          ????if?err != nil?{
          ????????log.Panic(err)
          ????}
          ????db = d
          }

          在上面這個(gè)例子中 init 函數(shù)初始化了一個(gè) db 全局變量,那么在單測(cè)的時(shí)候也會(huì)初始化一個(gè)這樣的變量,但是很多單測(cè)其實(shí)是很簡(jiǎn)單的,并不需要依賴這個(gè)東西。


          03

          embed types 優(yōu)缺點(diǎn)
          embed types 指的是我們?cè)?struct 里面定義的匿名的字段,如:
                type?Foo struct?{
          ??Bar
          }
          type?Bar struct?{
          ??Baz int
          }

          那么在上面這個(gè)例子中,我們可以通過(guò)? Foo.Baz 直接訪問(wèn)到成員變量,當(dāng)然也可以通過(guò)? Foo.Bar.Baz 訪問(wèn)。
          這樣在很多時(shí)候可以增加我們使用的便捷性,如果沒(méi)有使用 embed types 那么可能需要很多代碼,如下:
                type?Logger struct?{
          ????????writeCloser io.WriteCloser
          }

          func?(l Logger)?Write(p []byte)?(int, error)?{
          ????????return?l.writeCloser.Write(p)
          }

          func?(l Logger)?Close()?error?{
          ????????return?l.writeCloser.Close()
          }

          func?main()?{
          ????????l := Logger{writeCloser: os.Stdout}
          ????????_, _ = l.Write([]byte("foo"))
          ????????_ = l.Close()
          }

          如果使用了?embed types 我們的代碼可以變得很簡(jiǎn)潔
                type?Logger struct?{
          ????????io.WriteCloser
          }

          func?main()?{
          ????????l := Logger{WriteCloser: os.Stdout}
          ????????_, _ = l.Write([]byte("foo"))
          ????????_ = l.Close()
          }

          但是同樣它也有缺點(diǎn),有些字段我們并不想 export ,但是 embed types 可能給我們帶出去,例如:
                type?InMem struct?{
          ??sync.Mutex
          ??m map[string]int
          }

          func?New()?*InMem?{
          ???return?&InMem{m: make(map[string]int)}
          }

          Mutex 一般并不想 export, 只想在 InMem 自己的函數(shù)中使用,如:
                func?(i *InMem)?Get(key string)?(int, bool)?{
          ??i.Lock()
          ??v, contains := i.m[key]
          ??i.Unlock()
          ??return?v, contains
          }

          但是這么寫(xiě)卻可以讓拿到 InMem 類(lèi)型的變量都可以使用它里面的 Lock 方法:
                m := inmem.New()
          m.Lock() // ??



          04

          Functional Options Pattern傳遞參數(shù)
          這種方法在很多 Go 開(kāi)源庫(kù)都有看到過(guò)使用,比如 zap、GRPC 等。
          它經(jīng)常用在需要傳遞和初始化校驗(yàn)參數(shù)列表的時(shí)候使用,比如我們現(xiàn)在需要初始化一個(gè) HTTP server,里面可能包含了 port、timeout 等等信息,但是參數(shù)列表很多,不能直接寫(xiě)在函數(shù)上,并且我們要滿足靈活配置的要求,畢竟不是每個(gè) server 都需要很多參數(shù)。那么我們可以:
          • 設(shè)置一個(gè)不導(dǎo)出的 struct 叫 options,用來(lái)存放配置參數(shù);
          • 創(chuàng)建一個(gè)類(lèi)型 type Option func(options *options) error ,用這個(gè)類(lèi)型來(lái)作為返回值;

          比如我們現(xiàn)在要給 HTTP server 里面設(shè)置一個(gè) port 參數(shù),那么我們可以這么聲明一個(gè) WithPort 函數(shù),返回 Option 類(lèi)型的閉包,當(dāng)這個(gè)閉包執(zhí)行的時(shí)候會(huì)將 options 的 port 填充進(jìn)去:
                type?options struct?{
          ????????port *int
          }

          type?Option func(options *options)?error

          func?WithPort(port int)?Option
          ?{
          ?????????// 所有的類(lèi)型校驗(yàn),賦值,初始化啥的都可以放到這個(gè)閉包里面做
          ????????return?func(options *options)?error?{
          ????????????????if?port < 0?{
          ????????????????????????return?errors.New("port should be positive")
          ????????????????}
          ????????????????options.port = &port
          ????????????????return?nil
          ????????}
          }

          假如我們現(xiàn)在有一個(gè)這樣的 Option 函數(shù)集,除了上面的 port 以外,還可以填充 timeout 等。然后我們可以利用 NewServer 創(chuàng)建我們的 server:
                func?NewServer(addr string, opts ...Option)?(*http.Server, error)?{
          ????????var?options options
          ????????// 遍歷所有的 Option
          ????????for?_, opt := range?opts {
          ????????????????// 執(zhí)行閉包
          ????????????????err := opt(&options)
          ????????????????if?err != nil?{
          ????????????????????????return?nil, err
          ????????????????}
          ????????}

          ????????// 接下來(lái)可以填充我們的業(yè)務(wù)邏輯,比如這里設(shè)置默認(rèn)的port 等等
          ????????var?port int
          ????????if?options.port == nil?{
          ????????????????port = defaultHTTPPort
          ????????} else?{
          ????????????????if?*options.port == 0?{
          ????????????????????????port = randomPort()
          ????????????????} else?{
          ????????????????????????port = *options.port
          ????????????????}
          ????????}

          ????????// ...
          }

          初始化 server:
                server, err := httplib.NewServer("localhost", 
          ???????????????httplib.WithPort(8080),
          ???????????????httplib.WithTimeout(time.Second))

          這樣寫(xiě)的話就比較靈活,如果只想生成一個(gè)簡(jiǎn)單的 server,我們的代碼可以變得很簡(jiǎn)單:
                server, err := httplib.NewServer("localhost")



          05

          小心八進(jìn)制整數(shù)
          比如下面例子:
                sum := 100?+ 010
          ??fmt.Println(sum)

          你以為要輸出110,其實(shí)輸出的是 108,因?yàn)?strong>在 Go 中以 0 開(kāi)頭的整數(shù)表示八進(jìn)制。
          它經(jīng)常用在處理 Linux 權(quán)限相關(guān)的代碼上,如下面打開(kāi)一個(gè)文件:
                file, err := os.OpenFile("foo", os.O_RDONLY, 0644)

          所以為了可讀性,我們?cè)谟冒诉M(jìn)制的時(shí)候最好使用 "0o" 的方式表示,比如上面這段代碼可以表示為:
                file, err := os.OpenFile("foo", os.O_RDONLY, 0o644)



          06

          float 的精度問(wèn)題
          在 Go 中浮點(diǎn)數(shù)表示方式和其他語(yǔ)言一樣,都是通過(guò)科學(xué)計(jì)數(shù)法表示,float 在存儲(chǔ)中分為三部分:
          • 符號(hào)位 (Sign) : 0代表正,1代表為負(fù)
          • 指數(shù)位 (Exponent) :用于存儲(chǔ)科學(xué)計(jì)數(shù)法中的指數(shù)數(shù)據(jù),并且采用移位存儲(chǔ)
          • 尾數(shù)部分 (Mantissa) :尾數(shù)部分

          846c0b5ddbed5da8589c46cf424d4ee5.webp
          計(jì)算規(guī)則我就不在這里展示了,感興趣的可以自己去查查,我這里說(shuō)說(shuō)這種計(jì)數(shù)法在 Go 里面會(huì)有哪些問(wèn)題。
                func?f1(n int)?float64?{
          ??result := 10_000.
          ??for?i := 0; i < n; i++ {
          ????result += 1.0001
          ??}
          ??return?result
          }

          func?f2(n int)?float64?{
          ??result := 0.
          ??for?i := 0; i < n; i++ {
          ????result += 1.0001
          ??}
          ??return?result + 10_000.
          }

          在上面這段代碼中,我們簡(jiǎn)單地做了一下加法:
          n Exact result f1 f2
          10 10010.001 10010.001 10010.001
          1k 11000.1 11000.1 11000.1
          1m 1.01E+06 1.01E+06 1.01E+06

          可以看到 n 越大,誤差就越大,并且 f2 的誤差是小于 f1的。
          對(duì)于乘法我們可以做下面的實(shí)驗(yàn):
                a := 100000.001
          b := 1.0001
          c := 1.0002

          fmt.Println(a * (b + c))
          fmt.Println(a*b + a*c)

          輸出:
                200030.00200030004
          200030.0020003
          正確輸出應(yīng)該是 200030.0020003,所以它們實(shí)際上都有一定的誤差,但是可以看到先乘再加精度丟失會(huì)更小。
          如果想要準(zhǔn)確計(jì)算浮點(diǎn)的話,可以嘗試? "github.com/shopspring/decimal" 庫(kù),換成這個(gè)庫(kù)我們?cè)賮?lái)計(jì)算一下:
                a := decimal.NewFromFloat(100000.001)
          b := decimal.NewFromFloat(1.0001)
          c := decimal.NewFromFloat(1.0002)

          fmt.Println(a.Mul(b.Add(c))) //200030.0020003



          07

          slice 相關(guān)注意點(diǎn)

          ? ? ? 7.1 區(qū)分 slice 的 length 和 capacity


          首先讓我們初始化一個(gè)帶有 length 和 capacity 的 slice :
                s := make([]int, 3, 6)

          在 make 函數(shù)里面,capacity 是可選的參數(shù)。上面這段代碼我們創(chuàng)建了一個(gè) length 是 3,capacity 是 6 的 slice,那么底層的數(shù)據(jù)結(jié)構(gòu)是這樣的:
          7e7f9021e7817ccd0396d595d84b6455.webp
          slice 的底層實(shí)際上指向了一個(gè)數(shù)組。當(dāng)然,由于我們的 length 是 3,所以這樣設(shè)置? s[4] = 0 ?會(huì) panic 的。需要使用 append 才能添加新元素。
                panic: runtime?error: index?out of range?[4] with length 3

          當(dāng) appned 超過(guò) cap 大小的時(shí)候,slice 會(huì)自動(dòng)幫我們擴(kuò)容,在元素?cái)?shù)量小于 1024 的時(shí)候每次會(huì)擴(kuò)大一倍,當(dāng)超過(guò)了 1024 個(gè)元素每次擴(kuò)大 25%。
          有時(shí)候我們會(huì)使用? 操作符從另一個(gè) slice 上面創(chuàng)建一個(gè)新切片:
                s1 := make([]int, 3, 6)
          s2 := s1[1:3]

          實(shí)際上這兩個(gè) slice 還是指向了底層同樣的數(shù)組,構(gòu)如下:
          78ba6d1be969ce6d353349c2d2204839.webp
          由于指向了同一個(gè)數(shù)組,那么當(dāng)我們改變第一個(gè)槽位的時(shí)候,比如? s1[1]=2 ,實(shí)際上兩個(gè) slice 的數(shù)據(jù)都會(huì)發(fā)生改變:
          b81a4ff4f790714122aee6c374df3001.webp
          但是當(dāng)我們使用 append 的時(shí)候情況會(huì)有所不同:
                s2 = append(s2, 3)

          fmt.Println(s1) // [0 2 0]
          fmt.Println(s2) // [2 0 3]

          bf9e7b3c6c279e70dea9a1a9289e7d56.webp


          s1 的 len 并沒(méi)有被改變,所以看到的還是3元素。
          還有一件比較有趣的細(xì)節(jié)是,如果再接著 append s1 那么第四個(gè)元素會(huì)被覆蓋掉:
                s1 = append(s1, 4)
          ??fmt.Println(s1) // [0 2 0 4]
          ??fmt.Println(s2) // [2 0 4]


          cef2c5f7600ad35ec130faffcfe54fcc.webp


          我們?cè)倮^續(xù) append s2 直到 s2 發(fā)生擴(kuò)容,這個(gè)時(shí)候會(huì)發(fā)現(xiàn) s2 實(shí)際上和 s1 指向的不是同一個(gè)數(shù)組了:
                s2 = append(s2, 5, 6, 7)
          fmt.Println(s1) //[0 2 0 4]
          fmt.Println(s2) //[2 0 4 5 6 7]


          4cf10067f27184f21ebce9dad2d1b612.webp


          除了上面這種情況,還有一種情況 append 會(huì)產(chǎn)生意想不到的效果:
                s1 := []int{1, 2, 3}
          s2 := s1[1:2]
          s3 := append(s2, 10)

          1231a550a5d317c1c79f0a9fc4732f20.webp


          如果 print 它們應(yīng)該是這樣:
                s1=[1?2?10], s2=[2], s3=[2?10]

          ? ? ? 7.2 slice 初始化


          對(duì)于 slice 的初始化實(shí)際上有很多種方式:
                func?main()?{
          ????????var?s []string
          ????????log(1, s)

          ????????s = []string(nil)
          ????????log(2, s)

          ????????s = []string{}
          ????????log(3, s)

          ????????s = make([]string, 0)
          ????????log(4, s)
          }

          func?log(i int, s []string)?{
          ????????fmt.Printf("%d: empty=%t\tnil=%t\n", i, len(s) == 0, s == nil)
          }

          輸出:
                1: empty=true???nil=true
          2: empty=true???nil=true
          3: empty=true???nil=false
          4: empty=true???nil=false

          前兩種方式會(huì)創(chuàng)建一個(gè) nil 的 slice,后兩種會(huì)進(jìn)行初始化,并且這些 slice 的大小都為 0 。
          對(duì)于? var s []string 這種方式來(lái)說(shuō),好處就是不用做任何的內(nèi)存分配。比如下面場(chǎng)景可能可以節(jié)省一次內(nèi)存分配:
                func?f()?[]string?{
          ????????var?s []string
          ????????if?foo() {
          ????????????????s = append(s, "foo")
          ????????}
          ????????if?bar() {
          ????????????????s = append(s, "bar")
          ????????}
          ????????return?s
          }

          對(duì)于? s := []string{} 這種方式來(lái)說(shuō),它比較適合初始化一個(gè)已知元素的 slice
                s := []string{"foo", "bar", "baz"}
          如果沒(méi)有這個(gè)需求其實(shí)用? var s []string ?比較好,反正在使用的適合都是通過(guò) append 添加元素,? var s []string ?還能節(jié)省一次內(nèi)存分配。
          如果我們初始化了一個(gè)空的 slice, 那么最好是使用? len(xxx) == 0 ?來(lái)判斷 slice 是不是空的,如果使用 nil 來(lái)判斷可能會(huì)永遠(yuǎn)非空的情況,因?yàn)閷?duì)于? s := []string{} ?和? s = make([]string, 0) 這兩種初始化都是非 nil 的。
          對(duì)于? []string(nil) ?這種初始化的方式,使用場(chǎng)景很少,一種比較方便地使用場(chǎng)景是用它來(lái)進(jìn)行 slice 的 copy:
                src := []int{0, 1, 2}
          dst := append([]int(nil), src...)

          對(duì)于 make 來(lái)說(shuō),它可以初始化 slice 的 length 和 capacity,如果我們能確定 slice 里面會(huì)存放多少元素,從性能的角度考慮最好使用 make 初始化好,因?yàn)閷?duì)于一個(gè)空的 slice append 元素進(jìn)去每次達(dá)到閾值都需要進(jìn)行擴(kuò)容,下面是填充 100 萬(wàn)元素的 benchmark:
                BenchmarkConvert_EmptySlice-4 22 49739882 ns/op
          BenchmarkConvert_GivenCapacity-4 86 13438544 ns/op
          BenchmarkConvert_GivenLength-4 91 12800411 ns/op

          可以看到,如果我們提前填充好 slice 的容量大小,性能是空 slice 的四倍,因?yàn)樯倭藬U(kuò)容時(shí)元素復(fù)制以及重新申請(qǐng)新數(shù)組的開(kāi)銷(xiāo)。

          ? ? ? 7.3 copy slice


                src := []int{0, 1, 2}
          var?dst []int
          copy(dst, src)
          fmt.Println(dst) // []

          使用 copy 函數(shù) copy slice 的時(shí)候需要注意,上面這種情況實(shí)際上會(huì) copy 失敗,因?yàn)閷?duì) slice 來(lái)說(shuō)是由 length 來(lái)控制可用數(shù)據(jù),copy 并沒(méi)有復(fù)制這個(gè)字段,要想 copy 我們可以這么做:
                src := []int{0, 1, 2}
          dst := make([]int, len(src))
          copy(dst, src)
          fmt.Println(dst) //[0 1 2]

          除此之外也可以用上面提到的:
                src := []int{0, 1, 2}
          dst := append([]int(nil), src...)

          ? ? ? 7.4 slice capacity內(nèi)存釋放問(wèn)題


          先來(lái)看個(gè)例子:
                type?Foo struct?{
          ??v []byte
          }

          func?keepFirstTwoElementsOnly(foos []Foo)?[]Foo?{
          ??return?foos[:2]
          }

          func?main()?{
          ??foos := make([]Foo, 1_000)
          ??printAlloc()

          ??for?i := 0; i < len(foos); i++ {
          ????foos[i] = Foo{
          ??????v: make([]byte, 1024*1024),
          ????}
          ??}
          ??printAlloc()

          ??two := keepFirstTwoElementsOnly(foos)
          ??runtime.GC()
          ??printAlloc()
          ??runtime.KeepAlive(two)
          }

          上面這個(gè)例子中使用 printAlloc 函數(shù)來(lái)打印內(nèi)存占用:
                func?printAlloc()?{
          ??var?m runtime.MemStats
          ??runtime.ReadMemStats(&m)
          ??fmt.Printf("%d KB\n", m.Alloc/1024)
          }

          上面 foos 初始化了 1000 個(gè)容量的 slice ,里面 Foo struct 每個(gè)都持有 1M 內(nèi)存的 slice,然后通過(guò) keepFirstTwoElementsOnly 返回持有前兩個(gè)元素的 Foo 切片,我們的想法是手動(dòng)執(zhí)行 GC 之后其他的 998 個(gè) Foo 會(huì)被 GC 銷(xiāo)毀,但是輸出結(jié)果如下:
                387 KB
          1024315 KB
          1024319 KB

          實(shí)際上并沒(méi)有,原因就是實(shí)際上 keepFirstTwoElementsOnly 返回的 slice 底層持有的數(shù)組是和 foos 持有的同一個(gè):
          0ed791af4a4d2d1526726835884b99bf.webp
          所以我們真的要只返回 slice 的前2個(gè)元素的話應(yīng)該這樣做:
                func?keepFirstTwoElementsOnly(foos?[]Foo) []Foo?{
          ????????res := make([]Foo, 2)
          ????????copy(res, foos)
          ????????return res
          }

          不過(guò)上面這種方法會(huì)初始化一個(gè)新的 slice,然后將兩個(gè)元素 copy 過(guò)去。不想進(jìn)行多余的分配可以這么做:
                func?keepFirstTwoElementsOnly(foos []Foo)?[]Foo?{
          ????????for?i := 2; i < len(foos); i++ {
          ????????????????foos[i].v = nil
          ????????}
          ????????return?foos[:2]
          }



          08

          注意 range

          ? ? ? 8.1 copy 的問(wèn)題


          使用 range 的時(shí)候如果我們直接修改它返回的數(shù)據(jù)會(huì)不生效,因?yàn)榉祷氐臄?shù)據(jù)并不是原始數(shù)據(jù):
                type?account struct?{
          ??balance float32
          }
          ?
          ??accounts := []account{
          ????{balance: 100.},
          ????{balance: 200.},
          ????{balance: 300.},
          ??}
          ??for?_, a := range?accounts {
          ????a.balance += 1000
          ??}

          如果像上面這么做,那么輸出的 ac counts 是:

                [{100} {200} {300}]
                

          所以我們想要改變 range 中的數(shù)據(jù)可以這么做:

                for?i := range?accounts {
          ??accounts[i].balance += 1000
          }

          range slice 的話也會(huì) copy 一份:
                s := []int{0, 1, 2}
          for?range?s {
          ??s = append(s, 10)
          }

          這份代碼在 range 的時(shí)候會(huì) copy 一份,因此只會(huì)調(diào)用三次 append 后停止。

          ? ? ? 8.2 指針問(wèn)題


          比方我們想要 range slice 并將返回值存到 map 里面供后面業(yè)務(wù)使用,類(lèi)似這樣:
                type?Customer struct?{
          ????ID string
          ????Balance float64
          }

          test := []Customer{
          ??????{ID: "1", Balance: 10},
          ??????{ID: "2", Balance: -10},
          ??????{ID: "3", Balance: 0},
          }

          var?m map[string]*Customer
          for?_, customer := range?test {
          ????m[customer.ID] = &customer
          }

          但是這樣遍歷 map 里面存的并不是我們想要的,你會(huì)發(fā)現(xiàn)存的 value 都是最后一個(gè):
                {"1":{"ID":"3","Balance":0},"2":{"ID":"3","Balance":0},"3":{"ID":"3","Balance":0}}

          這是因?yàn)楫?dāng)我們使用 range 遍歷 slice 的時(shí)候,返回的 customer 變量實(shí)際上是一個(gè)固定的地址:
                for?_, customer := range?test {
          ????fmt.Printf("%p\n", &customer) //我們想要獲取這個(gè)指針的時(shí)候
          }

          輸出:
                0x1400000e240
          0x1400000e240
          0x1400000e240

          這是因?yàn)榈鲿?huì)把數(shù)據(jù)都放入到 0x1400000e240 這塊空間里面:
          b020bcba1858d14dc5fc896f59436efc.webp
          所以我們可以這樣在 range 里面獲取指針:
                for?_, customer := range?test {
          ????current := customer // 使用局部變量
          ????fmt.Printf("%p\n", ¤t) // 這里獲取的指針是 range copy 出來(lái)元素的指針
          ??}

          或者:
                for?i := range?test {
          ????current := &test[i] // 使用局部變量
          ????fmt.Printf("%p\n", current)
          ??}



          09

          注意break作用域
          比方說(shuō):
                for?i := 0; i < 5; i++ {
          ??????fmt.Printf("%d ", i)

          ??????switch?i {
          ??????default:
          ??????case?2:
          ??????????????break
          ??????}
          ??}

          上面這個(gè)代碼本來(lái)想 break 停止遍歷,實(shí)際上只是 break 了 switch 作用域,print 依然會(huì)打?。?,1,2,3,4。
          正確做法應(yīng)該是通過(guò) label 的方式 break:
                loop:
          ??for?i := 0; i < 5; i++ {
          ????fmt.Printf("%d ", i)
          ????switch?i {
          ????default:
          ????case?2:
          ??????break?loop
          ????}
          ??}

          有時(shí)候我們會(huì)沒(méi)注意到自己的錯(cuò)誤用法,比如下面:
                for?{
          ????select?{
          ????case?<-ch:
          ??????// Do something
          ????case?<-ctx.Done():
          ??????break
          ????}
          ??}

          上面這種寫(xiě)法會(huì)導(dǎo)致只跳出了 select,并沒(méi)有終止 for 循環(huán),正確寫(xiě)法應(yīng)該這樣:
                loop:
          ??for?{
          ????select?{
          ????case?<-ch:
          ??????// Do something
          ????case?<-ctx.Done():
          ??????break?loop
          ????}
          ??}



          10

          defer

          ? ? ? 10.1 注意 defer 的調(diào)用時(shí)機(jī)


          有時(shí)候我們會(huì)像下面一樣使用 defer 去關(guān)閉一些資源:
                func?readFiles(ch <-chan?string)?error?{
          ????????????for?path := range?ch {
          ????????????????????file, err := os.Open(path)
          ????????????????????if?err != nil?{
          ????????????????????????????return?err
          ????????????????????}
          ????
          ????????????????????defer?file.Close()
          ????
          ????????????????????// Do something with file
          ????????????}
          ????????????return?nil
          }

          因?yàn)閐efer會(huì)在方法結(jié)束的時(shí)候調(diào)用,但是如果上面的 readFiles 函數(shù)永遠(yuǎn)沒(méi)有 return,那么 defer 將永遠(yuǎn)不會(huì)被調(diào)用,從而造成內(nèi)存泄露。并且 defer 寫(xiě)在 for 循環(huán)里面,編譯器也無(wú)法做優(yōu)化,會(huì)影響代碼執(zhí)行性能。
          為了避免這種情況,我們可以 wrap 一層:
                func?readFiles(ch <-chan?string)?error?{
          ??????for?path := range?ch {
          ??????????if?err := readFile(path); err != nil?{
          ??????????????????return?err
          ??????????}
          ??????}
          ??????return?nil
          }

          func?readFile(path string)?error?{
          ??????file, err := os.Open(path)
          ??????if?err != nil?{
          ??????????????return?err
          ??????}

          ??????defer?file.Close()

          ??????// Do something with file
          ??????return?nil
          }

          ? ? ? 10.2 注意 defer 的參數(shù)


          defer 聲明時(shí)會(huì)先計(jì)算確定參數(shù)的值。
                func?a()?{
          ????i := 0
          ????defer?notice(i) // 0
          ????i++
          ????return
          }

          func?notice(i int)?{
          ??fmt.Println(i)
          }

          在這個(gè)例子中,變量 i 在? defer 被調(diào)用的時(shí)候就已經(jīng)確定了,而不是在? defer 執(zhí)行的時(shí)候,所以上面的語(yǔ)句輸出的是 0。
          所以我們想要獲取這個(gè)變量的真實(shí)值,應(yīng)該用引用:
                func?a()?{
          ??i := 0
          ??defer?notice(&i) // 1
          ??i++
          ??return
          }

          ? ? ? 10.2 defer 下的閉包


                func?a()?int?{
          ??i := 0
          ??defer?func()?{
          ????fmt.Println(i + 1) //12
          ??}()
          ??i++
          ??return?i+10??
          }

          func?TestA(t *testing.T)?{
          ??fmt.Println(a()) //11
          }

          如果換成閉包的話,實(shí)際上閉包中對(duì)變量i是通過(guò)指針傳遞,所以可以讀到真實(shí)的值。但是上面的例子中 a 函數(shù)返回的是 11 是因?yàn)閳?zhí)行順序是:
                先計(jì)算(i+10)-> (call?defer) -> (return)



          11

          string 相關(guān)

          ? ? ? 11.1 迭代帶來(lái)的問(wèn)題


          在 Go 語(yǔ)言中,字符串是一種基本類(lèi)型,默認(rèn)是通過(guò) utf8 編碼的字符序列,當(dāng)字符為 ASCII 碼時(shí)則占用 1 個(gè)字節(jié),其他字符根據(jù)需要占用 2-4 個(gè)字節(jié),比如中文編碼通常需要 3 個(gè)字節(jié)。
          那么我們?cè)谧?string 迭代的時(shí)候可能會(huì)產(chǎn)生意想不到的問(wèn)題:
                  s := "hêllo"
          ??for?i := range?s {
          ????fmt.Printf("position %d: %c\n", i, s[i])
          ??}
          ??fmt.Printf("len=%d\n", len(s))

          輸出:
                position?0: h
          position 1: ?
          position 3: l
          position 4: l
          position 5: o
          len=6

          上面的輸出中發(fā)現(xiàn)第二個(gè)字符是 ?,不是 ê,并且位置2的輸出”消失“了,這其實(shí)就是因?yàn)?ê 在 utf8 里面實(shí)際上占用 2 個(gè) byte:
          s h ê l l o
          []byte(s) 68 c3 aa 6c 6c 6f

          所以我們?cè)诘臅r(shí)候 s[1] 等于 c3 這個(gè) byte 等價(jià) ? 這個(gè) utf8 值,所以輸 出的是? h?llo ?而不是? hêllo 。
          那么根據(jù)上面的分析,我們就可以知道在迭代獲取字符的時(shí)候不能只獲取單個(gè) byte,應(yīng)該使用 range 返回的 value值:
                s := "hêllo"
          ??for?i, v := range?s {
          ????fmt.Printf("position %d: %c\n", i, v)
          ??}

          者我們可以把 string 轉(zhuǎn)成 rune 數(shù)組,在 go 中 rune 代表 Unicode 碼位,用它可以輸出單個(gè)字符:
                s := "hêllo"
          ??runes := []rune(s)
          ??for?i, _ := range?runes {
          ????fmt.Printf("position %d: %c\n", i, runes[i])
          ??}

          輸出:
                position?0: h
          position 1: ê
          position 2: l
          position 3: l
          position 4: o

          ? ? ? 11.2 截?cái)鄮?lái)的問(wèn)題


          在上面我們講 slice 的時(shí)候也提到了,在對(duì)slice使用? 操作符進(jìn)行截?cái)嗟臅r(shí)候,底層的數(shù)組實(shí)際上指向同一個(gè),在 string 里面也需要注意這個(gè)問(wèn)題,比如下面:
                func?(s store)?handleLog(log string)?error?{
          ????????????if?len(log) < 36?{
          ????????????????????return?errors.New("log is not correctly formatted")
          ????????????}
          ????????????uuid := log[:36]
          ????????????s.store(uuid)
          ????????????// Do something
          ????}

          這段代碼用了?:操作符進(jìn)行截?cái)?,但是如?log 這個(gè)對(duì)象很大,比如上面的 store 方法把 uuid 一直存在內(nèi)存里,可能會(huì)造成底層的數(shù)組一直不釋放,從而造成內(nèi)存泄露。
          為了解決這個(gè)問(wèn)題,我們可以先復(fù)制一份再處理:
                func?(s store)?handleLog(log string)?error?{
          ????????????if?len(log) < 36?{
          ????????????????????return?errors.New("log is not correctly formatted")
          ????????????}
          ????????????uuid := strings.Clone(log[:36]) // copy一份
          ????????????s.store(uuid)
          ????????????// Do something
          ????}



          12

          interface 類(lèi)型返回的非 nil 問(wèn)題
          假如我們想要繼承 error 接口實(shí)現(xiàn)一個(gè)自己的 MultiError:
                type?MultiError struct?{
          ??errs []string
          }

          func?(m *MultiError)?Add(err error)?{
          ??m.errs = append(m.errs, err.Error())
          }

          func?(m *MultiError)?Error()?string?{
          ??return?strings.Join(m.errs, ";")
          }

          然后在使用的時(shí)候返回 error,并且想通過(guò) error 是否為 nil 判斷是否有錯(cuò)誤:
                func?Validate(age int, name string)?error?{
          ??var?m *MultiError
          ??if?age < 0?{
          ????m = &MultiError{}
          ????m.Add(errors.New("age is negative"))
          ??}
          ??if?name == ""?{
          ????if?m == nil?{
          ??????m = &MultiError{}
          ????}
          ????m.Add(errors.New("name is nil"))
          ??}

          ??return?m
          }

          func?Test(t *testing.T)?{
          ??if?err := Validate(10, "a"); err != nil?{
          ????t.Errorf("invalid")
          ??}
          }

          實(shí)際上 Validate 返回的 err 會(huì)總是為非 nil 的,也就是上面代碼只會(huì)輸出? invalid :
                invalid 

          1f25a5d783d02342ac0ecf288e419b4b.webp


          13

          Error

          ? ? ? 13.1 error wrap


          對(duì)于 err 的 return 我們一般可以這么處理:
                err:= xxx()
          ?if?err != nil?{
          ???return?err
          ?}

          但是這樣處理只是簡(jiǎn)單地將原始的錯(cuò)誤拋出去了,無(wú)法知道當(dāng)前處理的這段程序的上下文信息,這個(gè)時(shí)候我們可能會(huì)自定義個(gè) error 結(jié)構(gòu)體,繼承 error 接口:

                err:= xxx()
          ?if?err != nil?{
          ???return?XXError{Err: err}
          ?}

          然后我們把上下文信息都加到 XXError 中,但是這樣雖然可以添加一些上下文信息,但是每次都需要?jiǎng)?chuàng)建一個(gè)特定類(lèi)型的 error 類(lèi)會(huì)變得很麻煩,那么在 1.13 之后,我們可以使用? %w ?進(jìn)行 wrap。
                if?err != nil?{
          ???return?fmt.Errorf("xxx failed: %w", err)
          ?}

          當(dāng)然除了上面這種做法以外,我們還可以直接? %v 直接格式化我們的錯(cuò)誤信息:
                if?err != nil?{
          ???return?fmt.Errorf("xxx failed: %v", err)
          ?}

          這樣做的缺點(diǎn)就是我們會(huì)丟失這個(gè) err 的類(lèi)型信息,如果不需要這個(gè)類(lèi)型信息,只是想往上拋打印一些日志當(dāng)然也無(wú)所謂。

          ? ? ? 13.2 error Is & As


          因?yàn)槲覀兊?error 可以會(huì)被 wrap 好幾層,那么使用? == ?是可能無(wú)法判斷我們的 error 究竟是不是我們想要的特定的 error,那么可以用? errors.Is
                var?BaseErr = errors.New("base error")

          func?main()?{
          ???err1 := fmt.Errorf("wrap base: %w", BaseErr)
          ???err2 := fmt.Errorf("wrap err1: %w", err1)
          ???println(err2 == BaseErr)
          ???
          ???if?!errors.Is(err2, BaseErr) {
          ??????panic("err2 is not BaseErr")
          ???}
          ???println("err2 is BaseErr")
          }

          輸出:
                false
          err2 is?BaseErr

          在上面,我們通過(guò)? errors.Is 就可以判斷出 err2 里面包含了 BaseErr 錯(cuò)誤。 errors.Is 里面會(huì)遞歸調(diào)用? Unwrap ?方法拆包裝,然后挨個(gè)使用? == ?判斷是否和指定類(lèi)型的 error 相等。
          errors.As 主要用來(lái)做類(lèi)型判斷,原因也是和上面一樣,error 被 wrap 之后我們通過(guò)? err.(type) 無(wú)法直接判斷, errors.As 會(huì)用? Unwrap ?方法拆包裝,然后挨個(gè)判斷類(lèi)型。使用如下:
                type?TypicalErr struct?{
          ???e string
          }

          func?(t TypicalErr)?Error()?string?{
          ???return?t.e
          }

          func?main()?{
          ???err := TypicalErr{"typical error"}
          ???err1 := fmt.Errorf("wrap err: %w", err)
          ???err2 := fmt.Errorf("wrap err1: %w", err1)
          ???var?e TypicalErr
          ???if?!errors.As(err2, &e) {
          ??????panic("TypicalErr is not on the chain of err2")
          ???}
          ???println("TypicalErr is on the chain of err2")
          ???println(err == e)
          }

          輸出:
                TypicalErr is?on?the chain of?err2
          true

          ? ? ? 13.3 處理 defer 中的 error


          比如下面代碼,我們?nèi)绻谡{(diào)用 Close 的時(shí)候報(bào)錯(cuò)是沒(méi)有處理的:
                func?getBalance(db *sql.DB, clientID string)?(
          ????????????float32, error)
          ?{
          ????????????rows, err := db.Query(query, clientID)
          ????????????if?err != nil?{
          ????????????????????return?0, err
          ????????????}
          ????????????defer?rows.Close()
          ????
          ????????????// Use rows
          ????}

          那么也許我們可以在 defer 中打印一些 log,但是無(wú)法 return,defer 不接受一個(gè) err 類(lèi)型的返回值:
                defer?func()?{
          ????????????err := rows.Close()
          ????????????if?err != nil?{
          ????????????????????log.Printf("failed to close rows: %v", err)
          ????????????}
          ????????????return?err //無(wú)法通過(guò)編譯
          ????}()

          那么我們可能想通過(guò)默認(rèn) err 返回值的方式將 defer 的 error 也返回了:
                func?getBalance(db *sql.DB, clientID string)?(balance float32, err error)?{
          ????????????rows, err = db.Query(query, clientID)
          ????????????if?err != nil?{
          ????????????????????return?0, err
          ????????????}
          ????????????defer?func()?{
          ????????????????????err = rows.Close()
          ????????????}()
          ????
          ????????????// Use rows
          ????}

          上面代碼看起來(lái)沒(méi)問(wèn)題,那么假如 Query 的時(shí)候和 Close 的時(shí)候同時(shí)發(fā)生異常呢?其中有一個(gè) error 會(huì)被覆蓋,那么我們可以根據(jù)自己的需求選擇一個(gè)打印日志,另一個(gè) error 返回:
                defer?func()?{
          ????????????closeErr := rows.Close()
          ????????????if?err != nil?{
          ????????????????????if?closeErr != nil?{
          ????????????????????????????log.Printf("failed to close rows: %v", err)
          ????????????????????}
          ????????????????????return
          ????????????}
          ????????????err = closeErr
          ????}()



          14

          happens before 保證
          創(chuàng)建 goroutine 發(fā)生先于 goroutine 執(zhí)行,所以下面這段代碼先讀一個(gè)變量,然后在 goroutine 中寫(xiě)變量不會(huì)發(fā)生 data race 問(wèn)題:

                    i := 0
          ????go?func()?{
          ????????????i++
          ????}()

          goroutine 退出沒(méi)有任何 happen before保證,例如下面代碼會(huì)有 data race :
                i := 0
          ????go?func()?{
          ????????????i++
          ????}()
          ????fmt.Println(i)

          channel 操作中 send 操作是 happens before receive 操作 :
                var?c = make(chan?int, 10)
          var?a string

          func?f()?{
          ??a = "hello, world"
          ??c <- 0
          }

          func?main()?{
          ??go?f()
          ??<-c
          ??print(a)
          }

          上面執(zhí)行順序應(yīng)該是:
                variable change?-》channel send -》channel receive -》variable?read

          上面能夠保證一定輸出? "hello, world" 。
          close channel 是 happens before receive 操作,所以下面這個(gè)例子中也不會(huì)有 data race 問(wèn)題:
                i := 0
          ????ch := make(chan?struct{})
          ????go?func()?{
          ????????????<-ch
          ????????????fmt.Println(i)
          ????}()
          ????i++
          ????close(ch)

          在無(wú)緩沖的 channel 中 receive 操作是 happens before send 操作的,例如:

                var?c = make(chan?int)
          var?a string

          func?f()?{
          ??a = "hello, world"
          ??<-c
          }

          func?main()?{
          ??go?f()
          ??c <- 0
          ??print(a)
          }

          這里同樣能保證輸出? hello, world 。


          15

          Context Values
          在 context 里面我們可以通過(guò) key value 的形式傳遞一些信息:
          context.WithValue 是從 parentCtx 創(chuàng)建,所以創(chuàng)建出來(lái)的 ctx 既包含了父類(lèi)的上下文信息,也包含了當(dāng)前新加的上下文。
                  fmt.Println(ctx.Value("key"))

          使用的時(shí)候可以直接通過(guò) Value 函數(shù)輸出。那么其實(shí)就可以想到,如果 key 相同的話后面的值會(huì)覆蓋前面的值的,所以在寫(xiě) key 的時(shí)候可以自 定義一個(gè)非導(dǎo)出的類(lèi)型作為 key 來(lái)保證唯一

                  package?provider
          ????
          ????type?key string
          ????
          ????const?myCustomKey key = "key"
          ????
          ????func?f(ctx context.Context)?{
          ????????????ctx = context.WithValue(ctx, myCustomKey, "foo")
          ????????????// ...
          ????}



          16

          應(yīng)多關(guān)注 goroutine 何時(shí)停止
          很多同學(xué)覺(jué)得 goroutine 比較輕量,認(rèn)為可以隨意地啟動(dòng) goroutine 去執(zhí)行任何而不會(huì)有很大的性能損耗。這個(gè)觀點(diǎn)基本沒(méi)錯(cuò),但是如果在 goroutine 啟動(dòng)之后因?yàn)榇a問(wèn)題導(dǎo)致它一直占用,沒(méi)有停止,數(shù)量多了之后可能會(huì)造成內(nèi)存泄漏
          比如下面的例子:
                  ch := foo()
          ????go?func()?{
          ????????????for?v := range?ch {
          ????????????????????// ...
          ????????????}
          ????}()

          如果在該 goroutine 中的 channel 一直沒(méi)有關(guān)閉,那么這個(gè) goroutine 就不會(huì)結(jié)束,會(huì)一直掛著占用一部分內(nèi)存。
          還有一種情況是我們的主進(jìn)程已經(jīng)停止運(yùn)行了,但是 goroutine 里面的任務(wù)還沒(méi)結(jié)束就被主進(jìn)程殺掉了,那么這樣也可能造成我們的任務(wù)執(zhí)行出問(wèn)題,比如資源沒(méi)有釋放,抑或是數(shù)據(jù)還沒(méi)處理完等等,如下:
                  func?main()?{
          ????????????newWatcher()
          ????
          ????????????// Run the application
          ????}
          ????
          ????type?watcher struct?{ /* Some resources */?}
          ????
          ????func?newWatcher()?{
          ????????????w := watcher{}
          ????????????go?w.watch()
          ????}

          上面這段代碼就可能出現(xiàn)主進(jìn)程已經(jīng)執(zhí)行 over 了,但是 watch 函數(shù)還沒(méi)跑完的情況,那么其實(shí)可以通過(guò)設(shè)置 stop 函數(shù),讓主進(jìn)程執(zhí)行完之后執(zhí)行 stop 函數(shù)即可:
                  func?main()?{
          ????????????w := newWatcher()
          ????????????defer?w.close()
          ????
          ????????????// Run the application
          ????}
          ????
          ????func?newWatcher()?watcher?{
          ????????????w := watcher{}
          ????????????go?w.watch()
          ????????????return?w
          ????}
          ????
          ????func?(w watcher)?close()?{
          ????????????// Close the resources
          ????}



          17

          Channel

          ? ? ? 17.1 select & channel


          select 和 channel 搭配起來(lái)往往有意想不到的效果,比如下面:
                  for?{
          ????????????select?{
          ????????????case?v := <-messageCh:
          ????????????????????fmt.Println(v)
          ????????????case?<-disconnectCh:
          ????????????????????fmt.Println("disconnection, return")
          ????????????????????return
          ????????????}
          ????}

          上面代碼中接受了 messageCh 和 disconnectCh 兩個(gè) channel 的數(shù)據(jù),如果我們想先接受 messageCh 的數(shù)組再接受 disconnectCh 的數(shù)據(jù),那么上面代碼會(huì)產(chǎn)生bug ,如:
                  for?i := 0; i < 10; i++ {
          ????????????messageCh <- i
          ????}
          ????disconnectCh <- struct{}{}

          我們想要上面的 select 先輸出完 messageCh 里面的數(shù)據(jù),然后再 return,實(shí)際上可能會(huì)輸出:
                  0
          1
          2
          3
          4
          disconnection, return

          這是因?yàn)?select 不像 switch 會(huì)依次匹配 case 分支,select 會(huì)隨機(jī)執(zhí)行下面的 case 分支,所以想要做到先消費(fèi) messageCh channel 數(shù)據(jù),如果只有單個(gè) goroutine?生產(chǎn)數(shù)據(jù)可以這樣做:
          • 使用無(wú)緩沖的 messageCh channel,這樣在發(fā)送數(shù)據(jù)的時(shí)候會(huì)一直等待,直到數(shù)據(jù)被消費(fèi)了才會(huì)往下走,相當(dāng)于是個(gè)同步模型了 (無(wú)緩沖的 channel 是 receive happens before send) ;
          • 在 select 里面使用單個(gè)channel,比如面的 demo 中我們可以定義一種特殊的 tag 來(lái)結(jié)束 channel,當(dāng)讀到這個(gè)特殊的 tag 的時(shí)候 return,這樣就沒(méi)必要用兩個(gè) channel 了。

          如果有多個(gè) goroutine 生產(chǎn)數(shù)據(jù),那么可以這樣:
                  for?{
          ???select?{
          ????case?v := <-messageCh:
          ????????fmt.Println(v)
          ????case?<-disconnectCh:
          ????????for?{
          ???????????select?{
          ????????????????case?v := <-messageCh:
          ? ? ? ? ? ? ? ????? ????fmt.Println(v)
          ????????????????default:
          ? ? ? ? ? ? ? ? ????????fmt.Println("disconnection, return")
          ????????????????????return
          ????????????????????????????}
          ????????????????????}
          ????????????}
          ????}

          在讀取 disconnectCh 的時(shí)候里面再套一個(gè)循環(huán)讀取 messageCh,讀完了之后會(huì)調(diào)用 default 分支進(jìn)行 return。

          ? ? ? 17.2 不要使用 nil channel


          使用 nil channel 進(jìn)行收發(fā)數(shù)據(jù)的時(shí)候會(huì)永遠(yuǎn)阻塞,例如發(fā)送數(shù)據(jù):
                  var?ch chan?int
          ch <- 0?//block

          接收數(shù)據(jù):
                  var?ch chan?int
          <-ch //block

          ? ? ? 17.3 Channel 的 close 問(wèn)題


          channel 在 close 之后仍然可以接收數(shù)據(jù)的 ,例如:
                  ch1 := make(chan?int, 1)
          ??close(ch1)
          ??for?{
          ????v := <-ch1
          ????fmt.Println(v)
          ??}

          這段代碼會(huì)一直 print 0。這會(huì)導(dǎo)致什么問(wèn)題呢?比如我們想要將兩個(gè) channel 的數(shù)據(jù)匯集到另一個(gè) channel 中:
                  func?merge(ch1, ch2 <-chan?int)?<-chan?int?{
          ????????ch := make(chan?int, 1)
          ????????go?func()?{
          ??????????for?{
          ????????????select?{
          ????????????case?v:= <-ch1:
          ??????????????ch <- v
          ????????????case?v:= <-ch2:
          ??????????????ch <- v
          ????????????}
          ??????????}
          ??????????close(ch) // 永遠(yuǎn)運(yùn)行不到
          ????????}()
          ????????return?ch
          }

          由于 channel 被 close 了還可以接收到數(shù)據(jù),所以上面代碼中,即使 ch1 和 ch2 都被 close 了,也是運(yùn)行不到? close(ch) ?這段代碼,并且還一直將 0 推入到 ch channel 中。所以為了感知到 channel 被關(guān)閉了,我們應(yīng)該使用 channel 返回的兩個(gè)參數(shù):
                  ???v, open?:= <-ch1
          ???fmt.Print(v, open) //open返回false 表示沒(méi)有被關(guān)閉

          那么回到我們上面的例子中,就可以這樣做:
                  func?merge(ch1, ch2 <-chan?int)?<-chan?int?{
          ??ch := make(chan?int, 1)
          ??ch1Closed := false
          ??ch2Closed := false

          ??go?func()?{
          ????for?{
          ??????select?{
          ??????case?v, open := <-ch1:
          ????????if?!open { // 如果已經(jīng)關(guān)閉
          ??????????ch1Closed = true?//標(biāo)記為true
          ??????????break
          ????????}
          ????????ch <- v
          ??????case?v, open := <-ch2:
          ????????if?!open { // 如果已經(jīng)關(guān)閉
          ??????????ch2Closed = true//標(biāo)記為true
          ??????????break
          ????????}
          ????????ch <- v
          ??????}

          ??????if?ch1Closed && ch2Closed {//都關(guān)閉了
          ????????close(ch)//關(guān)閉ch
          ????????return
          ??????}
          ????}
          ??}()
          ??return?ch
          }

          通過(guò)兩個(gè)標(biāo)記以及返回的 open 變量就可以判斷 channel 是否被關(guān)閉了,如果都關(guān)閉了,那么執(zhí)行? close(ch) 。


          18

          string format 帶來(lái)的 dead lock
          如果類(lèi)型定義了 String()?方法,它會(huì)被用在 fmt.Printf()?中生成默認(rèn)的輸出:等同于使用格式化描述符 %v 產(chǎn)生的輸出。還有 fmt.Print()?和 fmt.Println()?也會(huì)自動(dòng)使用 String()?方法。
          那么我們看看下面的例子:
                  type?Customer struct?{
          ??mutex sync.RWMutex
          ??id string
          ??age int
          }

          func?(c *Customer)?UpdateAge(age int)?error?{
          ??c.mutex.Lock()
          ??defer?c.mutex.Unlock()

          ??if?age < 0?{
          ????return?fmt.Errorf("age should be positive for customer %v", c)
          ??}

          ??c.age = age
          ??return?nil
          }

          func?(c *Customer)?String()?string?{
          ??fmt.Println("enter string method")
          ??c.mutex.RLock()
          ??defer?c.mutex.RUnlock()
          ??return?fmt.Sprintf("id %s, age %d", c.id, c.age)
          }

          這個(gè)例子中,如果調(diào)用 UpdateAge 方法 age 小于0會(huì)調(diào)用 fmt.Errorf,格式化輸出,這個(gè)時(shí)候 String()?方法里面也進(jìn)行了加鎖,那么這樣會(huì)造成死鎖。
                  mutex.Lock?-> check?age?-> Format?error?-> call?String() -> mutex.RLock

          解決方法也很簡(jiǎn)單,一個(gè)是縮小鎖的范圍,在 check age 之后再加鎖,另一種方法是 Format error 的時(shí)候不要 Format 整個(gè)結(jié)構(gòu)體,可以改成 Format id 就行了。


          19

          錯(cuò)誤使用 sync.WaitGroup
          sync.WaitGroup 通常用在并發(fā)中等待 goroutines 任務(wù)完成,用 Add 方法添加計(jì)數(shù)器,當(dāng)任務(wù)完成后需要調(diào)用 Done 方法讓計(jì)數(shù)器減一。等待的線程會(huì)調(diào)用 Wait 方法等待,直到 sync.WaitGroup ?內(nèi)計(jì)數(shù)器為零。
          需要注意的是 Add 方法是怎么使用的,如下:
                  wg := sync.WaitGroup{}
          ????var?v uint64
          ????
          ????for?i := 0; i < 3; i++ {
          ????????????go?func()?{
          ????????????????????wg.Add(1)
          ????????????????????atomic.AddUint64(&v, 1)
          ????????????????????wg.Done()
          ????????????}()
          ????}
          ????
          ????wg.Wait()
          ????fmt.Println(v)

          這樣使用可能會(huì)導(dǎo)致 v 不一定等于3,因?yàn)樵?for 循環(huán)里面創(chuàng)建的 3 個(gè) goroutines 不一定比外面的主線程先執(zhí)行,從而導(dǎo)致在調(diào)用 Add 方法之前可能 Wait 方法就執(zhí)行了,并且恰好 sync.WaitGroup 里面計(jì)數(shù)器是零,然后就通過(guò)了。
          正確的做法應(yīng)該是在創(chuàng)建 goroutines 之前就將要?jiǎng)?chuàng)建多少個(gè) goroutines 通過(guò) Add 方法添加進(jìn)去。


          20

          不要拷貝 sync 類(lèi)型
          sync 包里面提供一些并發(fā)操作的類(lèi)型,如 mutex、condition、wait gorup 等等,這些類(lèi)型都不應(yīng)該被拷貝之后使用。
          有時(shí)候我們?cè)谑褂玫臅r(shí)候拷貝是很隱秘的,比如下面:
                  type?Counter struct?{
          ??mu sync.Mutex
          ??counters map[string]int
          }

          func?(c Counter)?Increment(name string)?{
          ??c.mu.Lock()
          ??defer?c.mu.Unlock()
          ??c.counters[name]++
          }

          func?NewCounter()?Counter?{
          ??return?Counter{counters: map[string]int{}}
          }

          func?main()?{
          ??counter := NewCounter()
          ??go?counter.Increment("aa")
          ??go?counter.Increment("bb")
          }

          receiver 是一個(gè)值類(lèi)型,所以調(diào)用 Increment 方法的時(shí)候?qū)嶋H上拷貝了一份 Counter 里面的變量。這里我們可以將 receiver 改成一個(gè)指針,或者將? sync.Mutex ?變量改成指針類(lèi)型。
          所以如果:
          • receiver 是值類(lèi)型;
          • 函數(shù)參數(shù)是 sync 包類(lèi)型;
          • 函數(shù)參數(shù)的結(jié)構(gòu)體里面包含了 sync 包類(lèi)型;

          遇到這種情況需要注意檢查一下,我們可以借用 go vet 來(lái)檢測(cè),比如上面如果并發(fā)調(diào)用了就可以檢測(cè)出來(lái):
                  ? go?vet . bear@BEARLUO-MB7
          # github.com/cch123/gogctuner/main
          ./main.go:53:9: Increment passes lock by value: github.com/cch123/gogctuner/main.Counter contains sync.Mutex



          21

          time.After 內(nèi)存泄露
          我們用一個(gè)簡(jiǎn)單的例子模擬一下:
                  package?main

          import?(
          ????"fmt"
          ????"time"
          )
          //define a channel
          var?chs chan?int

          func?Get()?{
          ????for?{
          ????????select?{
          ????????????case?v := <- chs:
          ????????????????fmt.Printf("print:%v\n", v)
          ????????????case?<- time.After(3?* time.Minute):
          ????????????????fmt.Printf("time.After:%v", time.Now().Unix())
          ????????}
          ????}
          }

          func?Put()?{
          ????var?i = 0
          ????for?{
          ????????i++
          ????????chs <- i
          ????}
          }

          func?main()?{
          ????chs = make(chan?int, 100)
          ????go?Put()
          ????Get()
          }

          邏輯很簡(jiǎn)單就是先往 channel 里面存數(shù)據(jù),然后不停地使用 for select case 語(yǔ)法從 channel 里面取數(shù)據(jù),為了防止長(zhǎng)時(shí)間取不到數(shù)據(jù),所以在上面加了 time.After 定時(shí)器,這里只是簡(jiǎn)單打印一下。
          然后我沒(méi)用 pprof 看一下內(nèi)存占用:
                  $ go?tool pprof -http=:8081?http://localhost:6060/debug/pprof/heap

          773be8434ec2d8bb534421a4e4a2c700.webp


          發(fā)現(xiàn)不一會(huì)兒 Timer 的內(nèi)存占用很高了。這是因?yàn)樵谟?jì)時(shí)器觸發(fā)之前,垃圾收 集器不會(huì)回收 Timer,但是在循環(huán)里面每次都調(diào)用? time.After 都會(huì)實(shí)例化一個(gè)一個(gè)新的定時(shí)器,并且這個(gè)定時(shí)器會(huì)在激活之后才會(huì)被清除。
          為了避免這種情況我們可以使用 下面代碼:
                  func?Get() {
          ????delay := time.NewTimer(3 * time.Minute)

          ????defer delay.Stop()

          ????for {
          ????????delay.Reset(3 * time.Minute)

          ????????select {
          ????????????case v := <- chs:
          ????????????????fmt.Printf("print:%v\n", v)
          ????????????case <- delay.C:
          ????????????????fmt.Printf("time.After:%v", time.Now().Unix())
          ????????}
          ????}
          }



          22

          HTTP body 忘記 Close 導(dǎo)致的泄露
                  type?handler struct?{
          ????????client http.Client
          ????????url string
          }

          func?(h handler)?getBody()?(string, error)?{
          ????????resp, err := h.client.Get(h.url)
          ????????if?err != nil?{
          ????????????????return?"", err
          ????????}

          ????????body, err := io.ReadAll(resp.Body)
          ????????if?err != nil?{
          ????????????????return?"", err
          ????????}

          ????????return?string(body), nil
          }

          上面這段代碼看起來(lái)沒(méi)什么問(wèn)題,但是 resp 是? *http.Response ?類(lèi)型,里面包含了? Body io.ReadCloser ?對(duì)象,它是一個(gè) io 類(lèi),必須要正確關(guān)閉,否則是會(huì)產(chǎn)生資源泄露的。一般我們可以這么做:
                  defer?func() {
          ????????err := resp.Body.Close()
          ????????if err != nil {
          ????????????????log.Printf("failed to close response: %v\n", err)
          ????????}
          }()



          23

          Cache line
          目前在計(jì)算機(jī)中,主要有兩大存儲(chǔ)器 SRAM 和 DRAM。主存儲(chǔ)器是由 DRAM 實(shí)現(xiàn)的,也就是我們常說(shuō)的內(nèi)存,在 CPU 里通常會(huì)有 L1、L2、L3 這樣三層高速緩存是用 SRAM 實(shí)現(xiàn)的。
          da894a783f72b67fe103d13b46001e28.webp
          當(dāng)從內(nèi)存中取單元到 cache 中時(shí),會(huì)一次取一個(gè) cacheline 大小的內(nèi)存區(qū)域到 cache 中,然后存進(jìn)相應(yīng)的 cacheline 中,所以當(dāng)你讀取一個(gè)變量的時(shí)候,可能會(huì)把它相鄰的變量也讀取到 CPU 的緩存中(如果正好在一個(gè) cacheline 中),因?yàn)橛泻艽蟮膸茁誓銜?huì)繼續(xù)訪問(wèn)相鄰的變量,這樣 CPU 利用緩存就可以加速對(duì)內(nèi)存的訪問(wèn)。
          cacheline 大小通常有 32 bit,64 bit, 128 bit。拿我電腦的 64 bit 舉例:
                  cat?/sys/devices/system/cpu/cpu1/cache/index0/coherency_line_size 
          64

          我們?cè)O(shè)置兩個(gè)函數(shù),一個(gè) index 加2,一個(gè) index 加8:
                  func?sum2(s []int64)?int64?{
          ??var?total int64
          ??for?i := 0; i < len(s); i += 2?{
          ????total += s[i]
          ??}
          ??return?total
          }

          func?sum8(s []int64)?int64?{
          ??var?total int64
          ??for?i := 0; i < len(s); i += 8?{
          ????total += s[i]
          ??}
          ??return?total
          }

          這看起來(lái) sum8 處理的元素比 sum2 少四倍,那么性能應(yīng)該也快四倍左右,書(shū)上說(shuō)只快了10%,但是我沒(méi)測(cè)出來(lái)這個(gè)數(shù)據(jù),無(wú)所謂了大家知道因?yàn)?cacheline 的存在,并且數(shù)據(jù)在 L1 緩存里面性能很高就行了。
          然后再看看 slice 類(lèi)型的結(jié)構(gòu)體和結(jié)構(gòu)體里包含 slice:
                  type?Foo struct?{
          ????????a int64
          ????????b int64
          }

          func?sumFoo(foos []Foo)?int64?{
          ????????var?total int64
          ????????for?i := 0; i < len(foos); i++ {
          ????????????????total += foos[i].a
          ????????}
          ????????return?total
          }

          Foo 里面包含了兩個(gè)字段 a 和 b, sumFoo 會(huì)遍歷 Foo slice 將所有 a 字段加起來(lái)返回。
                  type?Bar struct?{
          ????????a []int64
          ????????b []int64
          }

          func?sumBar(bar Bar)?int64?{
          ????????var?total int64
          ????????for?i := 0; i < len(bar.a); i++ {
          ????????????????total += bar.a[i]
          ????????}
          ????????return?total
          }

          Bar 里面是包含了 a,b 兩個(gè) slice,sumBar 會(huì)將 Bar 里面的 a 的元素和相加返回。我們同樣用兩個(gè) benchmark 測(cè)試一下:
                  func?Benchmark_sumBar(b *testing.B)?{
          ??s := Bar{
          ????a: make([]int64, 16),
          ????b: make([]int64, 16),
          ??}

          ??b.RunParallel(func(pb *testing.PB)?{
          ????for?pb.Next() {
          ??????sumBar(s)
          ????}
          ??})
          }

          func?Benchmark_sumFoo(b *testing.B)?{
          ??s := make([]Foo, 16)

          ??b.RunParallel(func(pb *testing.PB)?{
          ????for?pb.Next() {
          ??????sumFoo(s)
          ????}
          ??})
          }

          測(cè)試結(jié)果:
                  #?go test?-gcflags "-N -l"?-bench .
          Benchmark_sumBar-16 249029368 4.855 ns/op
          Benchmark_sumFoo-16 238571205 5.056 ns/op

          sumBar 會(huì)比 sumFoo 快一點(diǎn)的。這是因?yàn)閷?duì)于 sumFoo 來(lái)說(shuō)要讀完整個(gè)數(shù)據(jù)才行,而對(duì)于 sumBar 來(lái)說(shuō)只需要讀前16 bytes 讀入到 cache line:
          0b58a40b585a36a7ac49d4bcf13f5141.webp


          24

          關(guān)于 False Sharing 造成的性能問(wèn)題
          False Sharing 是由于多線程對(duì)于同一片內(nèi)存進(jìn)行并行讀寫(xiě)操作的時(shí)候會(huì)造成內(nèi)存緩存失效,而反復(fù)將數(shù)據(jù)載入緩存所造成的性能問(wèn)題。
          因?yàn)楝F(xiàn)在 CPU 的緩存都是分級(jí)的,對(duì)于 L1 緩存來(lái)說(shuō)是每個(gè) Core 所獨(dú)享的,那么就有可能面臨緩存數(shù)據(jù)失效的問(wèn)題。
          如果同一片數(shù)據(jù)被多個(gè) Core 同時(shí)加載,那么它就是共享狀態(tài)在共享狀態(tài)下想要修改數(shù)據(jù)要先向所有的其他 CPU 核心廣播一個(gè)請(qǐng)求,要求先把其他 CPU 核心里面的 cache ,都變成無(wú)效的狀態(tài),然后再更新當(dāng)前 cache 里面的數(shù)據(jù)。
          CPU 核心里面的 cache 變成無(wú)效之后就不能使用了,需要重新加載,因?yàn)椴煌?jí)別的緩存的速度是差異很大的,所以這其實(shí)性能影響還蠻大的,我們寫(xiě)個(gè)測(cè)試看看。
                  type?MyAtomic interface?{
          ??IncreaseAllEles()
          }

          type?Pad struct?{
          ??a uint64
          ??_p1 [15]uint64
          ??b uint64
          ??_p2 [15]uint64
          ??c uint64
          ??_p3 [15]uint64
          }

          func?(myatomic *Pad)?IncreaseAllEles()?{
          ??atomic.AddUint64(&myatomic.a, 1)
          ??atomic.AddUint64(&myatomic.b, 1)
          ??atomic.AddUint64(&myatomic.c, 1)
          }

          type?NoPad struct?{
          ??a uint64
          ??b uint64
          ??c uint64
          }

          func?(myatomic *NoPad)?IncreaseAllEles()?{
          ??atomic.AddUint64(&myatomic.a, 1)
          ??atomic.AddUint64(&myatomic.b, 1)
          ??atomic.AddUint64(&myatomic.c, 1)
          }

          這里我定義了兩個(gè)結(jié)構(gòu)體 Pad 和 NoPad。然后我們定義一個(gè) benchmark 進(jìn)行多線程測(cè)試:
                  func?testAtomicIncrease(myatomic MyAtomic)?{
          ??paraNum := 1000
          ??addTimes := 1000
          ??var?wg sync.WaitGroup
          ??wg.Add(paraNum)
          ??for?i := 0; i < paraNum; i++ {
          ????go?func()?{
          ??????for?j := 0; j < addTimes; j++ {
          ????????myatomic.IncreaseAllEles()
          ??????}
          ??????wg.Done()
          ????}()
          ??}
          ??wg.Wait()

          }
          func?BenchmarkNoPad(b *testing.B)?{
          ??myatomic := &NoPad{}
          ??b.ResetTimer()
          ??testAtomicIncrease(myatomic)
          }

          func?BenchmarkPad(b *testing.B)?{
          ??myatomic := &Pad{}
          ??b.ResetTimer()
          ??testAtomicIncrease(myatomic)
          }

          結(jié)果可以看到快了 40% 左右:
                  BenchmarkNoPad
          BenchmarkNoPad-10??????1000000000???????????0.1360?ns/op
          BenchmarkPad
          BenchmarkPad-10????????1000000000???????????0.08887?ns/op

          如果沒(méi)有 pad 話,變量數(shù)據(jù)都會(huì)在一條 cache line 里面,這樣如果其中一個(gè)線程修改了數(shù)據(jù)會(huì)導(dǎo)致另一個(gè)線程的 cache line 無(wú)效,需要重新加載:
          41429a8c91406cfe4eb53b71baa5cd13.webp
          加了 padding 之后數(shù)據(jù)都不在同一個(gè) cache line 上了,即使發(fā)生了修改 invalid 不是同一行數(shù)據(jù)也不需要重新加載。
          5c6390f57a58bb2bf229b3bc5ea2ee1e.webp


          25

          內(nèi)存對(duì)齊
          簡(jiǎn)而言之,現(xiàn)在的 CPU 訪問(wèn)內(nèi)存的時(shí)候是一次性訪問(wèn)多個(gè) bytes,比如64位架構(gòu)一次訪問(wèn) 8bytes ,該處理器只能從地址為8的倍數(shù)的內(nèi)存開(kāi)始讀取數(shù)據(jù),所以要求數(shù)據(jù)在存放的時(shí)候首地址的值是8的倍數(shù)存放,這就是所謂的內(nèi)存對(duì)齊。
          比如下面的例子中因?yàn)閮?nèi)存對(duì)齊的存在,所以下面的例子中 b 這個(gè)字段只能在后面另外找地址為8的倍數(shù)地址開(kāi)始存放:
          9c6400906a91a88edfdd7ea4a9ab9018.webp
          除此之外還有一個(gè)零大小字段對(duì)齊的問(wèn)題,如果結(jié)構(gòu)體或數(shù)組類(lèi)型不包含大小大于零的字段或元素,那么它的大小就為0。比如 x [0]int8 , 空結(jié)構(gòu)體struct{}? 。當(dāng)它作為字段時(shí)不需要對(duì)齊,但是作為結(jié)構(gòu)體最后一個(gè)字段時(shí)需要對(duì)齊。我們拿空結(jié)構(gòu)體來(lái)舉個(gè)例子:
                  type?M struct?{
          ????m int64
          ????x struct{}
          }

          type?N struct?{
          ????x struct{}
          ????n int64
          }

          func?main()?{
          ????m := M{}
          ????n := N{}
          ????fmt.Printf("as final field size:%d\nnot as final field size:%d\n", unsafe.Sizeof(m), unsafe.Sizeof(n))
          }

          輸出:
                  as?final?field size:16
          not as?final?field size:8

          當(dāng)然,我們不可能手動(dòng)去調(diào)整內(nèi)存對(duì)齊,我們可以通過(guò)使用工具 fieldalignment:
                  $ go install?golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest

          $ fieldalignment -fix .\main\my.go
          main\my.go:13:9: struct?of?size?24?could be 16



          26

          逃逸分析
          Go 是通過(guò)在編譯器里做逃逸分析 (escape analysis) 來(lái)決定一個(gè)對(duì)象放棧上還是放堆上,不逃逸的對(duì)象放棧上,可能逃逸的放堆上。對(duì)于 Go 來(lái)說(shuō),我們可以通過(guò)下面指令來(lái)看變量是否逃逸:
                  go?run -gcflags '-m -l'?main.go

          • -m 會(huì)打印出逃逸分析的優(yōu)化策略,實(shí)際上最多總共可以用 4 個(gè) -m ,但是信息量較大,一般用 1 個(gè)就可以了。
          • -l 會(huì)禁用函數(shù)內(nèi)聯(lián),在這里禁用掉 內(nèi)聯(lián) 能更好的觀察逃逸情況,減少干擾。

          ? ? ? 26.1 指針逃逸


          在函數(shù)中創(chuàng)建了一個(gè)對(duì)象,返回了這個(gè)對(duì)象的指針。這種情況下,函數(shù)雖然退出了,但是因?yàn)橹羔樀拇嬖?,?duì)象的內(nèi)存不能隨著函數(shù)結(jié)束而回收,因此只能分配在堆上。
                  type?Demo struct?{
          ??name string
          }

          func?createDemo(name string)?*Demo?{
          ??d := new(Demo) // 局部變量 d 逃逸到堆
          ??d.name = name
          ??return?d
          }

          func?main()?{
          ??demo := createDemo("demo")
          ??fmt.Println(demo)
          }

          我們檢測(cè)一下:
                  go?run -gcflags '-m -l'??.\main\main.go
          # command-line-arguments
          main\main.go:12:17: leaking param: name
          main\main.go:13:10: new(Demo) escapes to?heap
          main\main.go:20:13: ... argument?does not escape
          &{demo}

          ? ? ? 26.2 interface{}/any 動(dòng)態(tài)類(lèi)型逃逸


          因?yàn)榫幾g期間很難確定其參數(shù)的具體類(lèi)型,也會(huì)發(fā)生逃逸,例如這樣:
                  func?createDemo(name string)?any?{
          ??d := new(Demo) // 局部變量 d 逃逸到堆
          ??d.name = name
          ??return?d
          }

          ? ? ? 26.3 切片長(zhǎng)度或容量沒(méi)指定逃逸


          如果使用局部切片時(shí),已知切片的長(zhǎng)度或容量,請(qǐng)使用常量或數(shù)值字面量來(lái)定義,否則也會(huì)逃逸:
                  func?main()?{
          ????number := 10
          ????s1 := make([]int, 0, number)
          ????for?i := 0; i < number; i++ {
          ????????s1 = append(s1, i)
          ????}
          ????s2 := make([]int, 0, 10)
          ????for?i := 0; i < 10; i++ {
          ????????s2 = append(s2, i)
          ????}
          }
          ?輸出一下:
                  go?run -gcflags '-m -l'??main.go????
          ?
          ./main.go:65:12: make([]int, 0, number) escapes to?heap
          ./main.go:69:12: make([]int, 0, 10) does not escape

          ? ? ? 26.4 閉包


          例如下面: Increase() ?返回值是一個(gè)閉包函數(shù),該閉包函數(shù)訪問(wèn)了外部變量 n,那變量 n 將會(huì)一直存在,直到? in ?被銷(xiāo)毀。很顯然,變量 n 占用的內(nèi)存不能隨著函數(shù)? Increase() ?的退出而回收,因此將會(huì)逃逸到堆上。
                  func?Increase() func() int?{
          ??n := 0
          ??return func() int {
          ????n++
          ????return n
          ??}
          }

          func?main() {
          ??in := Increase()
          ??fmt.Println(in()) // 1
          ??fmt.Println(in()) // 2
          }

          輸出:
                  go?run -gcflags '-m -l'??main.go??
          ?
          ./main.go:64:5: moved to?heap: n
          ./main.go:65:12: func literal escapes to?heap



          27

          byte slice 和 string 的轉(zhuǎn)換優(yōu)化
          直接通過(guò)強(qiáng)轉(zhuǎn) string(bytes) 或者 []byte(str) 會(huì)帶來(lái)數(shù)據(jù)的復(fù)制,性能不佳,所以在追求極致性能場(chǎng)景使用 unsafe 包的方式直接進(jìn)行轉(zhuǎn)換來(lái)提升性能:
                  // toBytes performs unholy acts to avoid allocations
          func?toBytes(s string)?[]byte?{
          ??return?*(*[]byte)(unsafe.Pointer(&s))
          }
          // toString performs unholy acts to avoid allocations
          func?toString(b []byte)?string?{
          ??return?*(*string)(unsafe.Pointer(&b))
          }

          在 Go 1.12 中,增加了幾個(gè)方法 String 、 StringData Slice SliceData ,用來(lái)做這種性能轉(zhuǎn)換。


          28

          容器中的 GOMAXPROCS
          自 Go 1.5 開(kāi)始, Go 的 GOMAXPROCS 默認(rèn)值已經(jīng)設(shè)置為 CPU 的核數(shù),但是在 Docker 或 k8s 容器中 runtime.GOMAXPROCS() ?獲取的是 宿主機(jī)的 CPU 核數(shù) 。這樣會(huì)導(dǎo)致 P ?值設(shè)置過(guò)大,導(dǎo)致生成線程過(guò)多,會(huì)增加上 下文切換的負(fù)擔(dān),導(dǎo)致嚴(yán)重的上下文切換,浪費(fèi) CPU。
          所以可以使用 uber 的 automaxprocs 庫(kù),大致原理是讀取? CGroup ?值識(shí)別容器的 CPU quota,計(jì)算得到實(shí)際核心數(shù),并自動(dòng)設(shè)置? GOMAXPROCS ?線程數(shù)量。
                  import?_ "go.uber.org/automaxprocs"

          func?main()?{
          ??// Your application logic here
          }



          29

          總結(jié)
          以上就是本篇文章對(duì)《100 Go Mistakes How to Avoid Them》書(shū)中內(nèi)容的技術(shù)總結(jié),也是一些在日常使用 Go 在工作中容易忽視掉的問(wèn)題。內(nèi)容量較大,常見(jiàn)錯(cuò)誤和技巧也很多,可以反復(fù)閱讀,感興趣的開(kāi)發(fā)者可以收藏下來(lái)慢慢研究。

          參考:

          https://go.dev/ref/mem

          https://colobu.com/2019/01/24/cacheline-affects-performance-in-go/

          https://teivah.medium.com/go-and-cpu-caches-af5d32cc5592

          https://geektutu.com/post/hpg-escape-analysis.html

          https://github.com/uber-go/automaxprocs

          https://gfw.go101.org/article/unsafe.html


          -End- 原創(chuàng)作者|羅志贇 技術(shù)責(zé)編|吳連火

          c1829031441c4278d8c6e89fbaa56d5e.webp


          使用Go語(yǔ)言時(shí)還有什么易錯(cuò)點(diǎn)?歡迎在評(píng)論區(qū)分享。我們將選取1則最有意義的分享,送出騰訊云開(kāi)發(fā)者-文化衫1件(見(jiàn)下圖)。6月12日中午12點(diǎn)開(kāi)獎(jiǎng)。


          180e84dcfb117d117ded9ccae423bfcf.webp 6e1841e649bcd57b7210da821c1ec7c9.webp

          acaf5183e6aba5f638664108a24db45e.webp

          37b271d5e7a1bdde00be83bd023c9f50.webp 13a2885ef10b360d55b6b859e8f00f03.webp

          關(guān)注星標(biāo)騰訊云開(kāi)發(fā)者

          第一時(shí)間看鵝廠技術(shù)干貨

          瀏覽 56
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  去干网69 | 日逼AV | 丁香成人婷婷 | 日韩成人三级 | 青娱乐成人视频 |