Go語言規範4 - 優化篇

@

優化篇

說明:本篇的意義是爲開發提供一些經過驗證的開發規則和建議,讓開發在開發過程中避免低級錯誤,從而提高代碼的質量保證和性能效率

4.1 質量保證

4.1.1 代碼質量保證優先原則

【原則4.1.1】代碼質量保證優先原則:
(1)正確性,指程序要實現設計要求的功能。
(2)簡潔性,指程序易於理解並且易於實現。
(3)可維護性,指程序被修改的能力,包括糾錯、改進、新需求或功能規格變化的適應能力。
(4)可靠性,指程序在給定時間間隔和環境條件下,按設計要求成功運行程序的概率。
(5)代碼可測試性,指軟件發現故障並隔離、定位故障的能力,以及在一定的時間和成本前提下,進行測試設計、測試執行的能力。
(6)代碼性能高效,指是儘可能少地佔用系統資源,包括內存和執行時間。
(7)可移植性,指爲了在原來設計的特定環境之外運行,對系統進行修改的能力。

4.1.2 對外接口原則

【原則4.1.2】對於主要功能模塊抽象模塊接口,通過interface提供對外功能。

說明:Go語言其中一個特殊的功能就是interface,它讓面向對象,內容組織實現非常的方便。正確的使用這個特性可以使模塊的可測試性和可維護性得到很大的提升。對於主要功能包(模塊),在package包主文件中通過interface對外提供功能。

示例:在buffer包的buffer.go中定義如下內容

    package buffer
    
    import (
        "policy_engine/models"
    )
    
    //other code …
    type MetricsBuffer interface {
        Store(metric *DataPoint) error
        Get(dataRange models.MatchPolicyDataRange) (*MetricDataBuf, error)
        Clear(redisKey string) error
        Stop()
        Stats() []MetrisBufferStat
        GetByKey(metricKey string) []DataPoint
    }

使用buffer package的代碼示例,通過interface定義,可以在不影響調用者使用的情況下替換package。基於這個特性,在測試過程中,也可以通過實現符合interface要求的類來打樁實現測試目的。

    package metrics
    
    import (
    ...//other import
        "policy_engine/worker/metrics/buffer"
    )
    
    type MetricsClient struct {
        logger            lager.Logger
        redisClient       *store.RedisClient
        conf              *config.Config
        metricsBuffer     buffer.MetricsBuffer //interface類型定義的成員
        metricsStatClient *metricstat.MetricsStatClient
        stopSignal        chan struct{}
    }
    
    func New(workerId string, redisClient *store.RedisClient, logger lager.Logger, conf *config.Config) *MetricsClient {
        var metricsBuffer MetricsBuffer
        if conf.MetricsBufferConfig.StoreType == config.METRICS_MEM_STORE {
            //具有interface定義函數的package實現,通過內存保存數據
            metricsBuffer = NewMemBuffer(logger, conf)  
        } else if conf.MetricsBufferConfig.StoreType == config.METRICS_REDIS_STORE {
            //具有interface定義函數的package實現,通過redis保存數據
            metricsBuffer = NewRedisBuffer(redisClient, logger, conf) 
        } else {
          ... //other code
        }
        ... //other code
    }

4.1.3 值與指針(T/*T)的使用原則

關於接收者對指針和值的規則是這樣的,值方法可以在指針和值上進行調用,而指針方法只能在指針上調用。這是因爲指針方法可以修改接收者;使用拷貝的值來調用它們,將會導致那些修改會被丟棄。

對於使用T還是*T作爲接收者,下面是一些建議:

【建議4.1.3.1】基本類型傳遞時,儘量使用值傳遞。

【建議4.1.3.2】如果傳遞字符串或者接口對象時,建議直接實例傳遞而不是指針傳遞。

【建議4.1.3.3】如果是map、func、chan,那麼直接用T。

【建議4.1.3.4】如果是slice,method裏面不重新reslice之類的就用T。

【建議4.1.3.5】如果想通過method改變裏面的屬性,那麼請使用*T。

【建議4.1.3.6】如果是struct,並且裏面包含了sync.Mutex之類的同步原語,那麼請使用*T,避免copy。

【建議4.1.3.7】如果是一個大型的struct或者array,那麼使用*T會比較輕量,效率更高。

【建議4.1.3.8】如果是struct、slice、array裏面的元素是一個指針類型,然後調用函數又會改變這個數據,那麼對於讀者來說採用*T比較容易懂。

【建議4.1.3.9】其它情況下,建議採用*T。

參考:https://github.com/golang/go/wiki/CodeReviewComments#pass-values

4.1.4 init的使用原則

每個源文件可以定義自己的不帶參數的init函數,來設置它所需的狀態。init是在程序包中所有變量聲明都被初始化,以及所有被導入的程序包中的變量初始化之後才被調用。

除了用於無法通過聲明來表示的初始化以外,init函數的一個常用法是在真正執行之前進行驗證或者修復程序狀態的正確性。

【規則4.1.4.1】一個文件只定義一個init函數。

【規則4.1.4.2】一個包內的如果存在多個init函數,不能有任何的依賴關係。

注意如果包內有多個init,每個init的執行順序是不確定的。

4.1.5 defer的使用原則

【建議4.1.5.1】如果函數存在多個返回的地方,則採用defer來完成如關閉資源、解鎖等清理操作。

說明:Go的defer語句用來調度一個函數調用(被延期的函數),在函數即將返回之前defer才被運行。這是一種不尋常但又很有效的方法,用於處理類似於不管函數通過哪個執行路徑返回,資源都必須要被釋放的情況。典型的例子是對一個互斥解鎖,或者關閉一個文件。

【建議4.1.5.2】defer會消耗更多的系統資源,不建議用於頻繁調用的方法中。

【建議4.1.5.3】避免在for循環中使用defer。

說明:一個完整defer過程要處理緩存對象、參數拷貝,以及多次函數調用,要比直接函數調用慢得多。

錯誤示例:實現一個加解鎖函數,解鎖過程使用defer處理。這是一個非常小的函數,並且能夠預知解鎖的位置,使用defer編譯後會使處理產生很多無用的過程導致性能下降。

    var lock sync.Mutex
    func testdefer() {
        lock.Lock()
        defer lock.Unlock()
    }
    
    func BenchmarkTestDefer(b *testing.B) {
        for i := 0; i < b.N; i++ {
            testdefer()
        }
    }
    
    // 耗時結果
    BenchmarkTestDefer 10000000 211 ns/op

推薦做法:如果能夠明確函數退出的位置,可以選擇不使用defer處理。保證功能不變的情況下,性能明顯提升,是耗時是使用defer的1/3。

    var lock sync.Mutex
    func testdefer() {
        lock.Lock()
        lock.Unlock() // ## 【修改】去除defer
    }
    
    func BenchmarkTestDefer(b *testing.B) {
        for i := 0; i < b.N; i++ {
            testdefer()
        }
    }
    
    // 耗時結果
    BenchmarkTest" 30000000 43.5 ns/op

4.1.6 Goroutine使用原則

【規則4.1.6.1】確保每個goroutine都能退出。

說明:Goroutine是Go並行設計的核心,在實現功能時不可避免會使用到,執行goroutine時會佔用一定的棧內存。

啓動goroutine就相當於啓動了一個線程,如果不設置線程退出的條件就相當於這個線程失去了控制,佔用的資源將無法回收,導致內存泄露。

錯誤示例:示例中ready()啓動了一個goroutine循環打印信息到屏幕上,這個goroutine無法終止退出。

    package main
    
    import (
        "fmt"
        "time"
    )
    
    func ready(w string, sec int) {    
        go func() { // ## 【錯誤】goroutine啓動之後無法終止
            for {
                time.Sleep(time.Duration(sec) * time.Second)
                fmt.Println(w, "is ready! ")
            }
        }()
    }
    
    func main() {
        ready("Tea", 2) 
        ready("Coffee", 1)
        fmt.Println("I'm waiting")
        time.Sleep(5 * time.Second)
    }

推薦做法:對於每個goroutine都需要有退出機制,能夠通過控制goroutine的退出,從而回收資源。通常退出的方式有:
 使用標誌位的方式;
 信號量;
 通過channel通道通知;

注意:channel是一個消息隊列,一個goroutine獲取signal後,另一個goroutine將無法獲取signal,以下場景下每個channel對應一個goroutine

    package main
    
    import (
        "fmt"
        "time"
    )
    
    func ready(w string, sec int, signal chan struct{}) {
        go func() {
            for {
                select {
                case <-time.Tick(time.Duration(sec) * time.Second):
                    fmt.Println(w, "is ready! ")
                case <-signal: // 對每個goroutie增加一個退出選項 
                    fmt.Println(w, "is close goroutine!")
                    return
                }
            }
        }()
    }
    
    func main() {
        signal1 := make(chan struct{}) // 增加一個signal
        ready("Tea", 2, signal1)
    
        signal2 := make(chan struct{}) // 增加一個signal
        ready("Coffee", 1, signal2)
    
        fmt.Println("I'm waiting")
        time.Sleep(4 * time.Second)
        signal1 <- struct{}{}
        signal2 <- struct{}{}
        time.Sleep(4 * time.Second)
    }

【規則4.1.6.2】禁止在閉包中直接引用閉包外部的循環變量。

說明:Go語言的特性決定了它會出現其它語言不存在的一些問題,比如在循環中啓動協程,當協程中使用到了循環的索引值,往往會出現意想不到的問題,通常需要程序員顯式地進行變量調用。

    for i := 0; i < limit; i++ {
        go func() { DoSomething(i) }()        //錯誤做法
        go func(i int) { DoSomething(i)}(i)   //正確做法
    }

參考:http://golang.org/doc/articles/race_detector.html#Race_on_loop_counter

4.1.7 Channel使用原則

【規則4.1.7.1】傳遞channel類型的參數時應該區分其職責。

在只發送的功能中,傳遞channel類型限定爲: c chan<- int
在只接收的功能中,傳遞channel類型限定爲: c <-chan int

【規則4.1.7.2】確保對channel是否關閉做檢查。

說明:在調用方法時不能想當然地認爲它們都會執行成功,當錯誤發生時往往會出現意想不到的行爲,因此必須嚴格校驗併合適處理函數的返回值。例如:channel在關閉後仍然支持讀操作,如果channel中的數據已經被讀取,再次讀取時會立即返回0值與一個channel關閉指示。如果不對channel關閉指示進行判斷,可能會誤認爲收到一個合法的值。因此在使用channel時,需要判斷channel是否已經關閉。

錯誤示例:下面代碼中若cc已被關閉,如果不對cc是否關閉做檢查,則會產生死循環。

    package main
    import (
        "errors"
        "fmt"
        "time"
    )
    
    func main() {
        var cc = make(chan int)
        go client(cc)
    
        for {
            select {
                case <-cc: //## 【錯誤】當channel cc被關閉後如果不做檢查則造成死循環
                fmt.Println("continue")
                case <-time.After(5 * time.Second):
                fmt.Println("timeout")
            }
        }
    }
    
    func client(c chan int) {
        defer close(c)
    
        for {
            err := processBusiness()
            if err != nil {
                c <- 0
                return
            }
            c <- 1
        }
    }
    
    func processBusiness() error {
        return errors.New("domo")
    }

推薦做法:對通道增加關閉判斷。

    // 前面代碼略……
    for {
        select {
        case _, ok := <-cc:
            // 增加對chnnel關閉的判斷,防止死循環
            if ok == false {
                fmt.Println("channel closed")
                return
            }
            fmt.Println("continue")
        case <-time.After(5 * time.Second):
            fmt.Println("timeout")
        }
    }
    // 後面代碼略……

【規則4.1.7.3】禁止重複釋放channel。

說明:重複釋放channel會觸發run-time panic,導致程序異常退出。重複釋放一般存在於異常流程判斷中,如果惡意攻擊者能夠構造成異常條件,則會利用程序的重複釋放漏洞實施DoS攻擊。

錯誤示例:

    func client(c chan int) {
        defer close(c)
        for {
            err := processBusiness()

            if err != nil {
                c <- 0
                close(c) // ## 【錯誤】可能會產生雙重釋放
                return
            }
            c <- 1
        }
    }

推薦做法:確保創建的channel只釋放一次。

    func client(c chan int) {
        defer close(c)
    
        for {
            err := processBusiness()
            if err != nil {
                c <- 0     // ## 【修改】使用defer延遲close後,不再單獨進行close
                return
            }
            c <- 1
        }
    }

4.1.8 其它

【建議4.1.8.1】使用go vet --shadow檢查變量覆蓋,以避免無意的變量覆蓋。

GO的變量賦值和聲明可以通過”:=”同時完成,但是由於Go可以初始化多個變量,所以這個語法容易引發錯誤。下面的例子是一個典型的變量覆蓋引起的錯誤,第二個val的作用域只限於for循環內部,賦值沒有影響到之前的val。

    package main
    
    import "fmt"
    import "strconv"
    
    func main() {
        var val int64
    
        if val, err := strconv.ParseInt("FF", 16, 64); nil != err {
            fmt.Printf("parse int failed with error %v\n", err)
        } else {
            fmt.Printf("inside  : val is %d\n", val)
        }
        fmt.Printf("outside : val is %d \n", val)
    }
    
    執行結果:
    inside  : val is 255
    outside : val is 0

正確的做法:

    package main
    
    import "fmt"
    import "strconv"
    
    func main() {
        var val int64
        var err error
    
        if val, err = strconv.ParseInt("FF", 16, 64); nil != err {
            fmt.Printf("parse int failed with error %v\n", err)
        } else {
            fmt.Printf("inside  : val is %d\n", val)
        }
        fmt.Printf("outside : val is %d \n", val)
    }
    
    執行結果:
    inside  : val is 255
    outside : val is 255

【建議4.1.8.2】GO的結構體中控制使用Slice和Map。

GO的slice和map等變量在賦值時,傳遞的是引用。從結果上看,是淺拷貝,會導致複製前後的兩個變量指向同一片數據。這一點和Go的數組、C/C++的數組行爲不同,很容易出錯。

    package main
    import "fmt"
    
    type Student struct {
        Name     string
        Subjects []string
    }
    
    func main() {
        sam := Student{
            Name: "Sam", Subjects: []string{"Math", "Music"},
        }
        clark := sam //clark.Subject和sam.Subject是同一個Slice的引用!
        clark.Name = "Clark"
        clark.Subjects[1] = "Philosophy" //sam.Subject[1]也變了!
        fmt.Printf("Sam : %v\n", sam)
        fmt.Printf("Clark : %v\n", clark)
    }
    
    執行結果:
    Sam : {Sam [Math Philosophy]}
    Clark : {Clark [Math Philosophy]}

作爲對比,請看作爲Array定義的Subjects的行爲:

    package main
    import "fmt"
    
    type Student struct {
        Name     string
        Subjects [2]string
    }
    
    func main() {
        var clark Student
        sam := Student{
            Name: "Sam", Subjects: [2]string{"Math", "Music"},
        }
    
        clark = sam //clark.Subject和sam.Subject不同的Array
        clark.Name = "Clark"
        clark.Subjects[1] = "Philosophy" //sam.Subject不受影響!
        fmt.Printf("Sam : %v\n", sam)
        fmt.Printf("Clark : %v\n", clark)
    }
    
    執行結果:
    Sam : {Sam [Math Music]}
    Clark : {Clark [Math Philosophy]}

編寫代碼時,建議這樣規避上述問題:
 結構體內儘可能不定義Slice、Maps成員;
 如果結構體有Slice、Maps成員,儘可能以小寫開頭、控制其訪問;
 結構體的賦值和複製,儘可能通過自定義的深度拷貝函數進行;

【規則4.1.8.3】避免在循環引用調用 runtime.SetFinalizer。

說明:指針構成的 "循環引用" 加上 runtime.SetFinalizer 會導致內存泄露。

runtime.SetFinalizer用於在一個對象 obj 被從內存移除前執行一些特殊操作,比如寫到日誌文件中。在對象被 GC 進程選中並從內存中移除以前,SetFinalizer 都不會執行,即使程序正常結束或者發生錯誤。

錯誤示例:垃圾回收器能正確處理 "指針循環引用",但無法確定 Finalizer 依賴次序,也就無法調用Finalizer 函數,這會導致目標對象無法變成不可達狀態,其所佔用內存無法被回收。

    package main
    
    import (
        "fmt"
        "runtime"
        "time"
    )
    
    type Data struct {
        d [1024 * 100]byte
        o *Data
    }
    
    func test() {
        var a, b Data
        a.o = &b
        b.o = &a
    
        // ## 【錯誤】循環和SetFinalize同時使用
        runtime.SetFinalizer(&a, func(d *Data) { fmt.Printf("a %p final.\n", d) })
        runtime.SetFinalizer(&b, func(d *Data) { fmt.Printf("b %p final.\n", d) })
    }
    
    func main() {    
        for { // ## 【錯誤】循環和SetFinalize同時使用
            test()
            time.Sleep(time.Millisecond)
        }
    }

通過跟蹤GC的處理過程,可以看到如上代碼內存在不斷的泄露:
go build -gcflags "-N -l" && GODEBUG="gctrace=1" ./test
gc11(1): 2+0+0 ms, 104 -> 104 MB 1127 -> 1127 (1180-53) objects
gc12(1): 4+0+0 ms, 208 -> 208 MB 2151 -> 2151 (2226-75) objects
gc13(1): 8+0+1 ms, 416 -> 416 MB 4198 -> 4198 (4307-109) objects
以上結果標紅的部分代表對象數量,我們在代碼中申請的對象都是局部變量,在正常處理過程中GC會持續的回收局部變量佔用的內存。但是在當前的處理過程中,內存無法被GC回收,目標對象無法變成不可達狀態。

推薦做法:需要避免內存指針的循環引用以及runtime.SetFinalizer同時使用。

【規則4.1.8.4】避免在for循環中使用time.Tick()函數。

如果在for循環中使用time.Tick(),它會每次創建一個新的對象返回,應該在for循環之外初始化一個ticker後,再在循環中使用:

    ticker := time.Tick(time.Second)
    for {
        select {
            case <-ticker:
            // …
        }
    }

4.2 性能效率

4.2.1 Memory優化

【建議4.2.1.1】將多次分配小對象組合爲一次分配大對象。

比如, 將 *bytes.Buffer 結構體成員替換爲bytes。緩衝區 (你可以預分配然後通過調用bytes.Buffer.Grow爲寫做準備) 。這將減少很多內存分配(更快)並且減緩垃圾回收器的壓力(更快的垃圾回收) 。

【建議4.2.1.2】將多個不同的小對象綁成一個大結構,可以減少內存分配的次數。

比如:將

    for k, v := range m {
       k, v := k, v   // copy for capturing by the goroutine
       go func() {
         // use k and v
       }()
    }

替換爲:

    for k, v := range m {
       x := struct{ k, v string }{k, v}   // copy for capturing by the goroutine

       go func() {

           // use x.k and x.v
       }()
    }

這就將多次內存分配(分別爲k、v分配內存)替換爲了一次(爲x分配內存)。然而,這樣的優化方式會影響代碼的可讀性,因此要合理地使用它。

【建議4.2.1.3】組合內存分配的一個特殊情形是對分片數組進行預分配。

如果清楚一個特定的分片的大小,可以對數組進行預分配:

    type X struct {
        buf      []byte
        bufArray [16]byte // Buf usually does not grow beyond 16 bytes.
    }
    
    
    func MakeX() *X {
        x := &X{}
        // Preinitialize buf with the backing array.
        x.buf = x.bufArray[:0]
        return x
    }

【建議4.2.1.4】儘可能使用小數據類型,並儘可能滿足硬件流水線(Pipeline)的操作,如對齊數據預取邊界。

說明:不包含任何指針的對象(注意 strings,slices,maps 和 chans 包含隱含指針)不會被垃圾回收器掃描到。

比如,1GB 的分片實際上不會影響垃圾回收時間。因此如果你刪除被頻繁使用的對象指針,它會對垃圾回收時間造成影響。一些建議:使用索引替換指針,將對象分割爲其中之一不含指針的兩部分。

【建議4.2.1.5】使用對象池來重用臨時對象,減少內存分配。

標準庫包含的sync.Pool類型可以實現垃圾回收期間多次重用同一個對象。然而需要注意的是,對於任何手動內存管理的方案來說,不正確地使用sync.Pool會導致 use-after-free bug。

4.2.2 GC 優化

【建議4.2.2.1】設置GOMAXPROCS爲CPU的核心數目,或者稍高的數值。

GC是並行的,而且一般在並行硬件上具有良好可擴展性。所以給 GOMAXPROCS 設置較高的值是有意義的,就算是對連續的程序來說也能夠提高垃圾回收速度。但是,要注意,目前垃圾回收器線程的數量被限制在 8 個以內。

【建議4.2.2.2】避免頻繁創建對象導致GC處理性能問題。

說明:儘可能少的申請內存,減少內存增量,可以減少甚至避免GC的性能衝擊,提升性能。
Go語言申請的臨時局部變量(對象)內存,都會受GC(垃圾回收)控制內存的回收,其實我們在編程實現功能時申請的大部分內存都屬於局部變量,所以與GC有很大的關係。

Go在GC的時候會發生Stop the world,整個程序會暫停,然後去標記整個內存裏面可以被回收的變量,標記完成之後再恢復程序執行,最後異步地去回收內存。(暫停的時間主要取決於需要標記的臨時變量個數,臨時變量數量越多,時間越長。Go 1.7以上的版本大幅優化了GC的停頓時間, Go 1.8下,通常的GC停頓的時間<100μs)

目前GC的優化方式原則就是儘可能少的聲明臨時變量:
 局部變量儘量利用
 如果局部變量過多,可以把這些變量放到一個大結構體內,這樣掃描的時候可以只掃描一個變量,回收掉它包含的很多內存

本規則所說的創建對象包含:
 &obj{}
 new(abc{})
 make()

我們在編程實現功能時申請的大部分內存都屬於局部變量,下面這個例子說明的是我們實現功能時需要注意的一個問題,適當的調整可以減少GC的性能消耗。

錯誤示例:
代碼中定義了一個tables對象,每個tables對象裏面有一堆類似tableA和tableC這樣的一對一的數據,也有一堆類似tableB這樣的一對多的數據。假設有1萬個玩家,每個玩家都有一條tableA和一條tableC的數據,又各有10條tableB的數據,那麼將總的產生1w (tables) + 1w (tableA) + 1w (tableC) + 10w (tableB)的對象。

不好的例子:

    // 對象數據表的集合
    type tables struct {
        tableA *tableA
        tableB *tableB
        tableC *tableC
        // 此處省略一些表
    }
    
    // 每個對象只會有一條tableA記錄
    type tableA struct {
        fieldA int
        fieldB string
    }
    
    // 每個對象有多條tableB記錄
    type tableB struct {
        city string
        code int
        next *tableB // 指向下一條記錄
    }
    
    // 每個對象只有一條tableC記錄
    type tableC struct {
        id    int
        value int64
    }

建議一對一表用結構體,一對多表用slice,每個表都加一個_is_nil的字段,用來表示當前的數據是否是有用的數據,這樣修改的結果是,一萬個玩家,產生的對象總量是1w(tables)+1w([]tablesB),跟前面的差別很明顯:

    // 對象數據表的集合
    type tables struct {
            tableA tableA
            tableB []tableB
            tableC tableC
        // 此處省略一些表
    }
    
    // 每個對象只會有一條tableA記錄
    type tableA struct {
        _is_nil bool 
        fieldA  int
        fieldB  string
    }
    

    // 每個對象有多條tableB記錄
    type tableB struct {
        _is_nil bool 
        city    string
        code    int
        next *tableB // 指向下一條記錄
    }
    
    // 每個對象只有一條tableC記錄
    type tableC struct {
        _is_nil bool
        id      int
        value   int64
    }

4.2.3 其它優化建議

【建議4.2.3.1】減少[]byte和string之間的轉換,儘量使用[]byte來處理字符。

說明:Go裏面string類型是immutable類型,而[]byte是切片類型,是可以修改的,所以Go爲了保證語法上面沒有二義性,在string和[]byte之間進行轉換的時候是一個實實在在的值copy,所以我們要儘量的減少不必要的這個轉變。

下面這個例子展示了傳遞slice但是進行了string的轉化,

    func PrefixForBytes(b []byte) string {
            return "Hello" + string(b)
    }

所以我們可以有兩種方式,一種是保持全部的都是slice的操作,如下:

    func PrefixForBytes(b []byte) []byte {
        return append([]byte(“Hello”,b…))
    }

還有一種就是全部是string的操作方式

    func PrefixForBytes(str string) string {
            return "Hello" + str
    }

推薦閱讀:https://blog.golang.org/strings

【建議4.2.3.2】make申請slice/map時,根據預估大小來申請合適內存。

說明:map和數組不同,可以根據新增的<key,value>對動態的伸縮,因此它不存在固定長度或者最大限制。

map的空間擴展是一個相對複雜的過程,每次擴容會增加到上次大小的兩倍。它的結構體中有一個buckets和oldbuckets,用來實現增量擴容,正常情況下直接使用buckets,oldbuckets爲空,如果當前哈希表正在擴容,則oldbuckets不爲空,且buckets大小是oldbuckets大小的兩倍。對於大的map或者會快速擴張的map,即便只是大概知道容量,也最好先標明。

slice是一個C語言動態數組的實現,在對slice進行append等操作時,可能會造成slice的自動擴容,其擴容規則:
 如果新的大小是當前大小2倍以上,則大小增長爲新大小
 否則循環以下操作:如果當前大小小於1024,按每次2倍增長,否則每次按當前大小1/4增長,直到增長的大小超過或者等於新大小

推薦做法:在初始化map時指明map的容量。

  1. map := make(map[string]float, 100)

【建議4.2.3.3】字符串拼接優先考慮bytes.Buffer。

Golang字符串拼接常見有如下方式:
 fmt.Sprintf
 strings.Join
 string +
 bytes.Buffer

fmt.Sprintf會動態解析參數,效率通常是最差的,而string是隻讀的,string+會導致多次對象分配與值拷貝,而bytes.Buffer在預設大小情況下,通常只會有一次拷貝和分配,不會重複拷貝和複製,故效率是最佳的。

推薦做法:優先使用bytes.Buffer,非關鍵路徑,若考慮簡潔,可考慮其它方式,比如錯誤日誌拼接使用fmt.Sprintf,但接口日誌使用就不合適。

【建議4.2.3.4】避免使用CGO或者減少跨CGO調用次數。

說明:Go可以調用C庫函數,但是Go帶有垃圾收集器且Go的棧是可變長,跟C實際是不能直接對接的,Go的環境轉入C代碼執行前,必須爲C新創建一個新的調用棧,把棧變量賦值給C調用棧,調用結束後再拷貝回來,這個調用開銷非常大,相比直接GO語言調用,單純的調用開銷,可能有2個甚至3個數量級以上,且Go目前還存在版本兼容性問題。

推薦做法:儘量避免使用CGO,無法避免時,要減少跨CGO調用次數。

【建議4.2.3.5】避免高併發調用同步系統接口。

說明:編程世界同步場景更普遍,GO提供了輕量級的routine,用同步來模擬異步操作,故在高併發下的,相比線程,同步模擬代價比較小,可以輕易創建數萬個併發調用。然而有些API是系統函數,而這些系統函數未提供異步實現,程序中最常見的posix規範的文件讀寫都是同步,epoll異步可解決網絡IO,而對regular file是無法工作的。Go的運行時環境不可能提供超越操作系統API的能力,它依賴於系統syscall文件中暴露的api能力,而1.6版本還是多線程模擬,線程創建切換的代價也非常巨大,開源庫中有filepoller來模擬異步其實也基於這兩種思路,效率上也會大打折扣。

推薦做法:把諸如寫文件這樣的同步系統調用,要隔離到可控的routine中,而不是直接高併發調用。

【建議4.2.3.6】高併發時避免共享對象互斥。

說明:在Go中,可以輕易創建10000個routine而對系統資源通常就是100M的內存要求,但是併發數多了,在多線程中,當併發衝突在4個到8個線程間時,性能可能就開始出現拐點,急劇下降,這同樣適應於Go,Go可以輕易創建routine,但對併發衝突的風險必須要做實現的處理。

推薦做法:routine需要是獨立的,無衝突的執行,若routine間有併發衝突,則必須控制可能發生衝突的併發routine個數,避免出現性能惡化拐點。

【建議4.2.3.7】長調用鏈或在函數中避免申明較多較大臨時變量。

routine的調用棧默認大小1.7版本已修改爲2K,當棧大小不夠時,Go運行時環境會做擴棧處理,創建10000個routine佔用空間才20M,所以routine非常輕量級,可以創建大量的併發執行邏輯。而線程棧默認大小是1M,當然也可以設置到8K(有些系統可以設置4K),一般不會這麼做,因爲線程棧大小是固定的,不能隨需而變大,不過實際CPU核一般都在100以內,線程數是足夠的。

routine是怎麼實現可變長棧呢?當棧大小不夠時,它會新創建一個棧,通常是2倍大小增長,然後把棧賦值過來,而棧中的指針變量需要搜索出來重新指向新的棧地址,好處不是隨便有的,這裏就明顯有性能開銷,而且這個開銷不小。

說明:頻繁創建的routine,要注意棧生長帶來的性能風險,比如棧最終是2M大小,極端情況下就會有數10次擴棧操作,從而讓性能急劇下降。所以必須控制調用棧和函數的複雜度,routine就意味着輕量級。

對於比較穩定的routine,也要注意它的棧生長後會導致內存飆升。

【建議4.2.3.8】爲高併發的輕量級任務處理創建routine池。

說明:Routine是輕量級的,但對於高併發的輕量級任務處理,頻繁創建routine來執行,執行效率也是非常低效率的。

推薦做法:高併發的輕量級任務處理,需要使用routine池,避免對調度和GC帶來衝擊。

【建議4.2.3.9】建議版本提供性能/內存監控的功能,並動態開啓關閉,但不要長期開啓pprof提供的CPU與MEM profile功能。

Go提供了pprof工具包,可以運行時開啓CPU與內存的profile信息,便於定位熱點函數的性能問題,而MEM的profile可以定位內存分配和泄漏相關問題。開啓相關統計,跟GC一樣,也會嚴重干擾性能,因而不要長期開啓。

推薦做法:做測試和問題定位時短暫開啓,現網運行,可以開啓短暫時間收集相關信息,同時要確保能夠自動關閉掉,避免長期打開。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章