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一樣,也會嚴重干擾性能,因而不要長期開啟。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM