GO編程模式系列(一):切片,接口,時間和性能

在本篇文章中,我會對Go語言編程模式的一些基本技術(shù)和要點(diǎn),這樣可以讓你更容易掌握Go語言編程。其中,主要包括,數(shù)組切片的一些小坑,還有接口編程,以及時間和程序運(yùn)行性能相關(guān)的話題。
本文是全系列中第1 / 9篇:Go編程模式
Go編程模式:切片,接口,時間和性能 Go 編程模式:錯誤處理 Go 編程模式:Functional Options Go編程模式:委托和反轉(zhuǎn)控制 Go編程模式:Map-Reduce Go 編程模式:Go Generation Go編程模式:修飾器 Go編程模式:Pipeline Go 編程模式:k8s Visitor 模式
目錄
Slice深度比較接口編程
接口完整性檢查
時間
性能提示
參考文檔
Slice
首先,我們先來討論一下Slice,中文翻譯叫“切片”,這個東西在Go語言中不是數(shù)組,而是一個結(jié)構(gòu)體,其定義如下:
type?slice?struct{???array?unsafe.Pointer?//指向存放數(shù)據(jù)的數(shù)組指針???len???int???????????//長度有多大???cap???int???????????//容量有多大}
用圖示來看,一個空的slice的表現(xiàn)如下:

熟悉C/C++的同學(xué)一定會知道,在結(jié)構(gòu)體里用數(shù)組指針的問題——數(shù)據(jù)會發(fā)生共享!下面我們來看一下slice的一些操作:
foo?=?make([]int,?5)foo[3]?=?42foo[4]?=?100bar??:=?foo[1:4]bar[1] = 99
對于上面這段代碼。
首先先創(chuàng)建一個foo的slice,其中的長度和容量都是5 然后開始對foo所指向的數(shù)組中的索引為3和4的元素進(jìn)行賦值 然后,對foo做切片后賦值給bar,再修改bar[1]

通過上圖我們可以看到,因為foo和bar的內(nèi)存是共享的,所以,foo和bar的對數(shù)組內(nèi)容的修改都會影響到對方。
接下來,我們再來看一個數(shù)據(jù)操作?append()?的示例:
a?:=?make([]int,?32)b?:=?a[1:16]a?=?append(a,?1)a[2] = 42
上面這段代碼中,把?a[1:16]?的切片賦給到了?b?,此時,a?和?b?的內(nèi)存空間是共享的,然后,對?a做了一個?append()的操作,這個操作會讓?a?重新分享內(nèi)存,導(dǎo)致?a?和?b?不再共享,如下圖所示:

從上圖我們可以看以看到?append()操作讓?a?的容量變成了64,而長度是33。這里,需要重點(diǎn)注意一下——append()這個函數(shù)在?cap?不夠用的時候就會重新分配內(nèi)存以擴(kuò)大容量,而如果夠用的時候不不會重新分享內(nèi)存!
我們再看來看一個例子:
funcmain(){?????path?:=?[]byte("AAAA/BBBBBBBBB")???sepIndex?:=?bytes.IndexByte(path,'/’)???dir1?:=?path[:sepIndex]dir2 := path[sepIndex+1:]???fmt.Println("dir1?=>",string(dir1))?//prints:?dir1?=>?AAAA???fmt.Println("dir2?=>",string(dir2))?//prints:?dir2?=>?BBBBBBBBBdir1 = append(dir1,"suffix"...)???fmt.Println("dir1?=>",string(dir1))?//prints:?dir1?=>?AAAAsuffix???fmt.Println("dir2?=>",string(dir2))?//prints:?dir2?=>?uffixBBBB}
上面這個例子中,dir1?和?dir2?共享內(nèi)存,雖然?dir1?有一個?append()?操作,但是因為 cap 足夠,于是數(shù)據(jù)擴(kuò)展到了dir2?的空間。下面是相關(guān)的圖示(注意上圖中?dir1?和?dir2?結(jié)構(gòu)體中的?cap?和?len?的變化)

如果要解決這個問題,我們只需要修改一行代碼。
dir1 := path[:sepIndex]修改為
dir1 := path[:sepIndex:sepIndex]新的代碼使用了 Full Slice Expression,其最后一個參數(shù)叫“Limited Capacity”,于是,后續(xù)的?append()?操作將會導(dǎo)致重新分配內(nèi)存。
深度比較
當(dāng)我們復(fù)雜一個對象時,這個對象可以是內(nèi)建數(shù)據(jù)類型,數(shù)組,結(jié)構(gòu)體,map……我們在復(fù)制結(jié)構(gòu)體的時候,當(dāng)我們需要比較兩個結(jié)構(gòu)體中的數(shù)據(jù)是否相同時,我們需要使用深度比較,而不是只是簡單地做淺度比較。這里需要使用到反射?reflect.DeepEqual()?,下面是幾個示例。
import(?????"fmt"???"reflect")funcmain(){???v1?:=?data{}???v2?:=?data{}???fmt.Println("v1?==?v2:",reflect.DeepEqual(v1,v2))??//prints:?v1?==?v2:?true???m1?:=?map[string]string{"one":?"a","two":?"b"}???m2?:=?map[string]string{"two":?"b",?"one":?"a"}???fmt.Println("m1?==?m2:",reflect.DeepEqual(m1,?m2))??//prints:?m1?==?m2:?true???s1?:=?[]int{1,?2,?3}???s2?:=?[]int{1,?2,?3}???fmt.Println("s1?==?s2:",reflect.DeepEqual(s1,?s2))??//prints:?s1?==?s2:?true}
接口編程
下面,我們來看段代碼,其中是兩個方法,它們都是要輸出一個結(jié)構(gòu)體,其中一個使用一個函數(shù),另一個使用一個“成員函數(shù)”。
funcPrintPerson(p?*Person){???fmt.Printf("Name=%s,?Sexual=%s,?Age=%d\n",?p.Name,?p.Sexual,?p.Age)}func(p?*Person)Print(){???fmt.Printf("Name=%s,?Sexual=%s,?Age=%d\n",?p.Name,?p.Sexual,?p.Age)}funcmain(){???var?p?=?Person{???????Name:?"Hao?Chen",???????Sexual:?"Male",Age: 44,???}???PrintPerson(&p)???p.Print()}
你更喜歡哪種方式呢?在 Go 語言中,使用“成員函數(shù)”的方式叫“Receiver”,這種方式是一種封裝,因為?PrintPerson()本來就是和?Person強(qiáng)耦合的,所以,理應(yīng)放在一起。更重要的是,這種方式可以進(jìn)行接口編程,對于接口編程來說,也就是一種抽象,主要是用在“多態(tài)”,這個技術(shù),在《Go語言簡介(上):接口與多態(tài)》中已經(jīng)講過。在這里,我想講另一個Go語言接口的編程模式。
首先,我們來看一下,有下面這段代碼:
type?Country?struct{???Name?string}type?City?struct{???Name?string}type?Printable?interface{???PrintStr()}func(c?Country)PrintStr(){???fmt.Println(c.Name)}func(c?City)PrintStr(){???fmt.Println(c.Name)}c1?:=?Country?{"China"}c2?:=?City?{"Beijing"}c1.PrintStr()c2.PrintStr()
其中,我們可以看到,其使用了一個?Printable?的接口,而?Country?和?City?都實現(xiàn)了接口方法?PrintStr()?而把自己輸出。然而,這些代碼都是一樣的。能不能省掉呢?
我們可以使用“結(jié)構(gòu)體嵌入”的方式來完成這個事,如下的代碼所示:
type?WithName?struct{???Name?string}type?Country?struct{???WithName}type?City?struct{???WithName}type?Printable?interface{???PrintStr()}func(w?WithName)PrintStr(){???fmt.Println(w.Name)}c1?:=?Country?{WithName{"China"}}c2 := City { WithName{"Beijing"}}c1.PrintStr()c2.PrintStr()
引入一個叫?WithName的結(jié)構(gòu)體,然而,所帶來的問題就是,在初始化的時候,變得有點(diǎn)亂。那么,我們有沒有更好的方法?下面是另外一個解。
type?Country?struct{???Name?string}type?City?struct{???Name?string}type?Stringable?interface{???ToString()string}func(c?Country)ToString()string{???return"Country?=?"?+?c.Name}func(c?City)ToString()string{???return"City?=?"?+?c.Name}funcPrintStr(p?Stringable){???fmt.Println(p.ToString())}d1?:=?Country?{"USA"}d2?:=?City{"Los?Angeles"}PrintStr(d1)PrintStr(d2)
上面這段代碼,我們可以看到——我們使用了一個叫Stringable?的接口,我們用這個接口把“業(yè)務(wù)類型”?Country?和?City?和“控制邏輯”?Print()?給解耦了。于是,只要實現(xiàn)了Stringable?接口,都可以傳給?PrintStr()?來使用。
這種編程模式在Go 的標(biāo)準(zhǔn)庫有很多的示例,最著名的就是?io.Read?和?ioutil.ReadAll?的玩法,其中?io.Read?是一個接口,你需要實現(xiàn)他的一個?Read(p []byte) (n int, err error)?接口方法,只要滿足這個規(guī)模,就可以被?ioutil.ReadAll這個方法所使用。這就是面向?qū)ο缶幊谭椒ǖ狞S金法則——“Program to an interface not an implementation”
接口完整性檢查
另外,我們可以看到,Go語言的編程器并沒有嚴(yán)格檢查一個對象是否實現(xiàn)了某接口所有的接口方法,如下面這個示例:
type?Shape?interface{???Sides()int???Area()int}type?Square?struct{???len?int}func(s*?Square)Sides()int{???return4}funcmain(){???s?:=?Square{len:?5}???fmt.Printf("%d\n",s.Sides())}
我們可以看到?Square?并沒有實現(xiàn)?Shape?接口的所有方法,程序雖然可以跑通,但是這樣編程的方式并不嚴(yán)謹(jǐn),如果我們需要強(qiáng)制實現(xiàn)接口的所有方法,那么我們應(yīng)該怎么辦呢?
在Go語言編程圈里有一個比較標(biāo)準(zhǔn)的作法:
var _ Shape = (*Square)(nil)聲明一個?_?變量(沒人用),其會把一個?nil?的空指針,從?Square?轉(zhuǎn)成?Shape,這樣,如果沒有實現(xiàn)完相關(guān)的接口方法,編譯器就會報錯:
cannot use (*Square)(nil) (type *Square) as type Shape in assignment: *Square does not implement Shape (missing Area method)
這樣就做到了個強(qiáng)驗證的方法。
時間
對于時間來說,這應(yīng)該是編程中比較復(fù)雜的問題了,相信我,時間是一種非常復(fù)雜的事(比如《你確信你了解時間嗎?》、《關(guān)于閏秒》等文章)。而且,時間有時區(qū)、格式、精度等等問題,其復(fù)雜度不是一般人能處理的。所以,一定要重用已有的時間處理,而不是自己干。
在 Go 語言中,你一定要使用?time.Time?和?time.Duration?兩個類型:
在命令行上, flag?通過?time.ParseDuration?支持了?time.DurationJSon 中的? encoding/json?中也可以把time.Time?編碼成?RFC 3339?的格式數(shù)據(jù)庫使用的? database/sql?也支持把?DATATIME?或?TIMESTAMP?類型轉(zhuǎn)成?time.TimeYAML你可以使用? gopkg.in/yaml.v2?也支持?time.Time?、time.Duration?和?RFC 3339?格式
如果你要和第三方交互,實在沒有辦法,也請使用?RFC 3339?的格式。
最后,如果你要做全球化跨時區(qū)的應(yīng)用,你一定要把所有服務(wù)器和時間全部使用UTC時間。
性能提示
Go 語言是一個高性能的語言,但并不是說這樣我們就不用關(guān)心性能了,我們還是需要關(guān)心的。下面是一個在編程方面和性能相關(guān)的提示。
如果需要把數(shù)字轉(zhuǎn)字符串,使用? strconv.Itoa()?會比?fmt.Sprintf()?要快一倍左右盡可能地避免把 String轉(zhuǎn)成[]Byte?。這個轉(zhuǎn)換會導(dǎo)致性能下降。如果在for-loop里對某個slice 使用? append()請先把 slice的容量很擴(kuò)充到位,這樣可以避免內(nèi)存重新分享以及系統(tǒng)自動按2的N次方冪進(jìn)行擴(kuò)展但又用不到,從而浪費(fèi)內(nèi)存。使用 StringBuffer?或是StringBuild?來拼接字符串,會比使用?+?或?+=?性能高三到四個數(shù)量級。盡可能的使用并發(fā)的 go routine,然后使用? sync.WaitGroup?來同步分片操作避免在熱代碼中進(jìn)行內(nèi)存分配,這樣會導(dǎo)致gc很忙。盡可能的使用? sync.Pool?來重用對象。使用 lock-free的操作,避免使用 mutex,盡可能使用? sync/Atomic包。(關(guān)于無鎖編程的相關(guān)話題,可參看《無鎖隊列實現(xiàn)》或《無鎖Hashmap實現(xiàn)》)使用 I/O緩沖,I/O是個非常非常慢的操作,使用? bufio.NewWrite()?和?bufio.NewReader()?可以帶來更高的性能。對于在for-loop里的固定的正則表達(dá)式,一定要使用? regexp.Compile()?編譯正則表達(dá)式。性能會得升兩個數(shù)量級。如果你需要更高性能的協(xié)議,你要考慮使用?protobuf?或?msgp?而不是JSON,因為JSON的序列化和反序列化里使用了反射。 你在使用map的時候,使用整型的key會比字符串的要快,因為整型比較比字符串比較要快。
參考文檔
還有很多不錯的技巧,下面的這些參考文檔可以讓你寫出更好的Go的代碼,必讀!
Effective?Go
https://golang.org/doc/effective_go.htmlUber?Go?Style
https://github.com/uber-go/guide/blob/master/style.md50 Shades of Go: Traps, Gotchas, and Common Mistakes for New Golang Devs
http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/Go?Advice
https://github.com/cristaloleg/go-advicePractical Go Benchmarks
https://www.instana.com/blog/practical-golang-benchmarks/Benchmarks of Go serialization methods
https://github.com/alecthomas/go_serialization_benchmarksDebugging?performance?issues?in?Go?programs
https://github.com/golang/go/wiki/PerformanceGo?code?refactoring:?the?23x?performance?hunt
https://medium.com/@val_deleplace/go-code-refactoring-the-23x-performance-hunt-156746b522f7
