Go編程模式:切片,接口,時(shí)間和性能

更多奇技淫巧歡迎訂閱博客:https://fuckcloudnative.io
前言
在本篇文章中,我會(huì)對(duì) Go 語言編程模式的一些基本技術(shù)和要點(diǎn),這樣可以讓你更容易掌握 Go 語言編程。其中,主要包括,數(shù)組切片的一些小坑,還有接口編程,以及時(shí)間和程序運(yùn)行性能相關(guān)的話題。
本文是全系列中第 1 / 9 篇:Go 編程模式[1]
Go 編程模式:切片,接口,時(shí)間和性能 Go 編程模式:錯(cuò)誤處理[2] Go 編程模式:Functional Options[3] Go 編程模式:委托和反轉(zhuǎn)控制[4] Go 編程模式:Map-Reduce[5] Go 編程模式:Go Generation[6] Go 編程模式:修飾器[7] Go 編程模式:Pipeline[8] Go 編程模式:k8s Visitor 模式[9]
1. Slice
首先,我們先來討論一下 Slice,中文翻譯叫“切片”,這個(gè)東西在 Go 語言中不是數(shù)組,而是一個(gè)結(jié)構(gòu)體,其定義如下:
type?slice?struct?{
????array?unsafe.Pointer?//指向存放數(shù)據(jù)的數(shù)組指針
????len???int????????????//長(zhǎng)度有多大
????cap???int????????????//容量有多大
}
用圖示來看,一個(gè)空的 slice 的表現(xiàn)如下:

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

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

從上圖我們可以看以看到 append()操作讓 a 的容量變成了 64,而長(zhǎng)度是 33。這里,需要重點(diǎn)注意一下——append()這個(gè)函數(shù)在 cap 不夠用的時(shí)候就會(huì)重新分配內(nèi)存以擴(kuò)大容量,而如果夠用的時(shí)候不不會(huì)重新分享內(nèi)存!
我們?cè)倏磥砜匆粋€(gè)例子:
func?main()?{
????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?=>?BBBBBBBBB
????dir1?=?append(dir1,"suffix"...)
????fmt.Println("dir1?=>",string(dir1))?//prints:?dir1?=>?AAAAsuffix
????fmt.Println("dir2?=>",string(dir2))?//prints:?dir2?=>?uffixBBBB
}
上面這個(gè)例子中,dir1 和 dir2 共享內(nèi)存,雖然 dir1 有一個(gè) append() 操作,但是因?yàn)?cap 足夠,于是數(shù)據(jù)擴(kuò)展到了dir2 的空間。下面是相關(guān)的圖示(注意上圖中 dir1 和 dir2 結(jié)構(gòu)體中的 cap 和 len 的變化)

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


你可能還喜歡
點(diǎn)擊下方圖片即可閱讀

云原生是一種信仰???
掃碼關(guān)注公眾號(hào)
后臺(tái)回復(fù)?k8s?獲取史上最方便快捷的 Kubernetes 高可用部署工具,只需一條命令,連 ssh 都不需要!


點(diǎn)擊?"閱讀原文"?獲取更好的閱讀體驗(yàn)!
??給個(gè)「在看」,是對(duì)我最大的支持??

