Go 語言較之 C 語言一個(gè)很大的優(yōu)勢就是自帶 GC 功能,可 GC 并不是沒有代價(jià)的。寫 C 語言的時(shí)候,在一個(gè)函數(shù)內(nèi)聲明的變量,在函數(shù)退出后會(huì)自動(dòng)釋放掉,因?yàn)檫@些變量分配在棧上。如果你期望變量的數(shù)據(jù)可以在函數(shù)退出后仍然能被訪問,就需要調(diào)用 malloc 方法在堆上申請內(nèi)存,如果程序不再需要這塊內(nèi)存了,再調(diào)用 free 方法釋放掉。Go 語言不需要你主動(dòng)調(diào)用 malloc 來分配堆空間,編譯器會(huì)自動(dòng)分析,找出需要 malloc 的變量,使用堆內(nèi)存。編譯器的這個(gè)分析過程就叫做逃逸分析。
創(chuàng)新互聯(lián)建站網(wǎng)站建設(shè)公司提供網(wǎng)站設(shè)計(jì)和自適應(yīng)建站服務(wù)。團(tuán)隊(duì)由有經(jīng)驗(yàn)的網(wǎng)頁設(shè)計(jì)師、程序員和市場專家組成,能夠提供從HTML5,網(wǎng)站制作,一元廣告,模板建站到微信小程序定制開發(fā)等全方位服務(wù)。 以客戶為中心,致力于為客戶提供創(chuàng)新、高效的解決方案,幫助您打造成功的企業(yè)網(wǎng)站。
所以你在一個(gè)函數(shù)中通過 dict := make(map[string]int) 創(chuàng)建一個(gè) map 變量,其背后的數(shù)據(jù)是放在??臻g上還是堆空間上,是不一定的。這要看編譯器分析的結(jié)果。
可逃逸分析并不是百分百準(zhǔn)確的,它有缺陷。有的時(shí)候你會(huì)發(fā)現(xiàn)有些變量其實(shí)在??臻g上分配完全沒問題的,但編譯后程序還是把這些數(shù)據(jù)放在了堆上。如果你了解 Go 語言編譯器逃逸分析的機(jī)制,在寫代碼的時(shí)候就可以有意識地繞開這些缺陷,使你的程序更高效。
Go 語言雖然在內(nèi)存管理方面降低了編程門檻,即使你不了解堆棧也能正常開發(fā),但如果你要在性能上較真的話,還是要掌握這些基礎(chǔ)知識。
這里不對堆內(nèi)存和棧內(nèi)存的區(qū)別做太多闡述。簡單來說就是, 棧分配廉價(jià),堆分配昂貴。 ??臻g會(huì)隨著一個(gè)函數(shù)的結(jié)束自動(dòng)釋放,堆空間需要時(shí)間 GC 模塊不斷地跟蹤掃描回收。如果對這兩個(gè)概念有些迷糊,建議閱讀下面 2 個(gè)文章:
這里舉一個(gè)小例子,來對比下堆棧的差別:
stack 函數(shù)中的變量 i 在函數(shù)退出會(huì)自動(dòng)釋放;而 heap 函數(shù)返回的是對變量 i 的引用,也就是說 heap() 退出后,表示變量 i 還要能被訪問,它會(huì)自動(dòng)被分配到堆空間上。
他們編譯出來的代碼如下:
邏輯的復(fù)雜度不言而喻,從上面的匯編中可看到, heap() 函數(shù)調(diào)用了 runtime.newobject() 方法,它會(huì)調(diào)用 mallocgc 方法從 mcache 上申請內(nèi)存,申請的內(nèi)部邏輯前面文章已經(jīng)講述過。堆內(nèi)存分配不僅分配上邏輯比??臻g分配復(fù)雜,它最致命的是會(huì)帶來很大的管理成本,Go 語言要消耗很多的計(jì)算資源對其進(jìn)行標(biāo)記回收(也就是 GC 成本)。
Go 編輯器會(huì)自動(dòng)幫我們找出需要進(jìn)行動(dòng)態(tài)分配的變量,它是在編譯時(shí)追蹤一個(gè)變量的生命周期,如果能確認(rèn)一個(gè)數(shù)據(jù)只在函數(shù)空間內(nèi)訪問,不會(huì)被外部使用,則使用??臻g,否則就要使用堆空間。
我們在 go build 編譯代碼時(shí),可使用 -gcflags '-m' 參數(shù)來查看逃逸分析日志。
以上面的兩個(gè)函數(shù)為例,編譯的日志輸出是:
日志中的 i escapes to heap 表示該變量數(shù)據(jù)逃逸到了堆上。
需要使用堆空間,所以逃逸,這沒什么可爭議的。但編譯器有時(shí)會(huì)將 不需要 使用堆空間的變量,也逃逸掉。這里是容易出現(xiàn)性能問題的大坑。網(wǎng)上有很多相關(guān)文章,列舉了一些導(dǎo)致逃逸情況,其實(shí)總結(jié)起來就一句話:
多級間接賦值容易導(dǎo)致逃逸 。
這里的多級間接指的是,對某個(gè)引用類對象中的引用類成員進(jìn)行賦值。Go 語言中的引用類數(shù)據(jù)類型有 func , interface , slice , map , chan , *Type(指針) 。
記住公式 Data.Field = Value ,如果 Data , Field 都是引用類的數(shù)據(jù)類型,則會(huì)導(dǎo)致 Value 逃逸。這里的等號 = 不單單只賦值,也表示參數(shù)傳遞。
根據(jù)公式,我們假設(shè)一個(gè)變量 data 是以下幾種類型,相應(yīng)的可以得出結(jié)論:
下面給出一些實(shí)際的例子:
如果變量值是一個(gè)函數(shù),函數(shù)的參數(shù)又是引用類型,則傳遞給它的參數(shù)都會(huì)逃逸。
上例中 te 的類型是 func(*int) ,屬于引用類型,參數(shù) *int 也是引用類型,則調(diào)用 te(j) 形成了為 te 的參數(shù)(成員) *int 賦值的現(xiàn)象,即 te.i = j 會(huì)導(dǎo)致逃逸。代碼中其他幾種調(diào)用都沒有形成 多級間接賦值 情況。
同理,如果函數(shù)的參數(shù)類型是 slice , map 或 interface{} 都會(huì)導(dǎo)致參數(shù)逃逸。
匿名函數(shù)的調(diào)用也是一樣的,它本質(zhì)上也是一個(gè)函數(shù)變量。有興趣的可以自己測試一下。
只要使用了 Interface 類型(不是 interafce{} ),那么賦值給它的變量一定會(huì)逃逸。因?yàn)? interfaceVariable.Method() 先是間接的定位到它的實(shí)際值,再調(diào)用實(shí)際值的同名方法,執(zhí)行時(shí)實(shí)際值作為參數(shù)傳遞給方法。相當(dāng)于 interfaceVariable.Method.this = realValue
向 channel 中發(fā)送數(shù)據(jù),本質(zhì)上就是為 channel 內(nèi)部的成員賦值,就像給一個(gè) slice 中的某一項(xiàng)賦值一樣。所以 chan *Type , chan map[Type]Type , chan []Type , chan interface{} 類型都會(huì)導(dǎo)致發(fā)送到 channel 中的數(shù)據(jù)逃逸。
這本來也是情理之中的,發(fā)送給 channel 的數(shù)據(jù)是要與其他函數(shù)分享的,為了保證發(fā)送過去的指針依然可用,只能使用堆分配。
可變參數(shù)如 func(arg ...string) 實(shí)際與 func(arg []string) 是一樣的,會(huì)增加一層訪問路徑。這也是 fmt.Sprintf 總是會(huì)使參數(shù)逃逸的原因。
例子非常多,這里不能一一列舉,我們只需要記住分析方法就好,即,2 級或更多級的訪問賦值會(huì) 容易 導(dǎo)致數(shù)據(jù)逃逸。這里加上 容易 二字是因?yàn)殡S著語言的發(fā)展,相信這些問題會(huì)被慢慢解決,但現(xiàn)階段,這個(gè)可以作為我們分析逃逸現(xiàn)象的依據(jù)。
下面代碼中包含 2 種很常規(guī)的寫法,但他們卻有著很大的性能差距,建議自己想下為什么。
Benchmark 和 pprof 給出的結(jié)果:
熟悉堆棧概念可以讓我們更容易看透 Go 程序的性能問題,并進(jìn)行優(yōu)化。
多級間接賦值會(huì)導(dǎo)致 Go 編譯器出現(xiàn)不必要的逃逸,在一些情況下可能我們只需要修改一下數(shù)據(jù)結(jié)構(gòu)就會(huì)使性能有大幅提升。這也是很多人不推薦在 Go 中使用指針的原因,因?yàn)樗鼤?huì)增加一級訪問路徑,而 map , slice , interface{} 等類型是不可避免要用到的,為了減少不必要的逃逸,只能拿指針開刀了。
大多數(shù)情況下,性能優(yōu)化都會(huì)為程序帶來一定的復(fù)雜度。建議實(shí)際項(xiàng)目中還是怎么方便怎么寫,功能完成后通過性能分析找到瓶頸所在,再對局部進(jìn)行優(yōu)化。
1. 保留但大幅度簡化指針
Go語言保留著C中值和指針的區(qū)別,但是對于指針繁瑣用法進(jìn)行了大量的簡化,引入引用的概念。所以在Go語言中,你幾乎不用擔(dān)心會(huì)因?yàn)橹苯硬僮鲀?nèi)寸而引起各式各樣的錯(cuò)誤。
2. 多參數(shù)返回
還記得在C里面為了回饋多個(gè)參數(shù),不得不開辟幾段指針傳到目標(biāo)函數(shù)中讓其操作么?在Go里面這是完全不必要的。而且多參數(shù)的支持讓Go無需使用繁瑣的exceptions體系,一個(gè)函數(shù)可以返回期待的返回值加上error,調(diào)用函數(shù)后立刻處理錯(cuò)誤信息,清晰明了。
3. Array,slice,map等內(nèi)置基本數(shù)據(jù)結(jié)構(gòu)
如果你習(xí)慣了Python中簡潔的list和dict操作,在Go語言中,你不會(huì)感到孤單。一切都是那么熟悉,而且更加高效。如果你是C++程序員,你會(huì)發(fā)現(xiàn)你又找到了STL的vector 和 map這對朋友。
4. Interface
Go語言最讓人贊嘆不易的特性,就是interface的設(shè)計(jì)。任何數(shù)據(jù)結(jié)構(gòu),只要實(shí)現(xiàn)了interface所定義的函數(shù),自動(dòng)就implement了這個(gè)interface,沒有像Java那樣冗長的class申明,提供了靈活太多的設(shè)計(jì)度和OO抽象度,讓你的代碼也非常干凈。千萬不要以為你習(xí)慣了Java那種一條一條加implements的方式,感覺還行,等接口的設(shè)計(jì)越來越復(fù)雜的時(shí)候,無數(shù)Bug正在后面等著你。
同時(shí),正因?yàn)槿绱?,Go語言的interface可以用來表示任何generic的東西,比如一個(gè)空的interface,可以是string可以是int,可以是任何數(shù)據(jù)類型,因?yàn)檫@些數(shù)據(jù)類型都不需要實(shí)現(xiàn)任何函數(shù),自然就滿足空interface的定義了。加上Go語言的type assertion,可以提供一般動(dòng)態(tài)語言才有的duck typing特性, 而仍然能在compile中捕捉明顯的錯(cuò)誤。
5. OO
Go語言本質(zhì)上不是面向?qū)ο笳Z言,它還是過程化的。但是,在Go語言中, 你可以很輕易的做大部分你在別的OO語言中能做的事,用更簡單清晰的邏輯。是的,在這里,不需要class,仍然可以繼承,仍然可以多態(tài),但是速度卻快得多。因?yàn)楸举|(zhì)上,OO在Go語言中,就是普通的struct操作。
6. Goroutine
這個(gè)幾乎算是Go語言的招牌特性之一了,我也不想多提。如果你完全不了解Goroutine,那么你只需要知道,這玩意是超級輕量級的類似線程的東西,但通過它,你不需要復(fù)雜的線程操作鎖操作,不需要care調(diào)度,就能玩轉(zhuǎn)基本的并行程序。在Go語言里,觸發(fā)一個(gè)routine和erlang spawn一樣簡單。基本上要掌握Go語言,以Goroutine和channel為核心的內(nèi)存模型是必須要懂的。不過請放心,真的非常簡單。
7. 更多現(xiàn)代的特性
和C比較,Go語言完全就是一門現(xiàn)代化語言,原生支持的Unicode, garbage collection, Closures(是的,和functional programming language類似), function是first class object,等等等等。
看到這里,你可能會(huì)發(fā)現(xiàn),我用了很多輕易,簡單,快速之類的形容詞來形容Go語言的特點(diǎn)。我想說的是,一點(diǎn)都不夸張,連Go語言的入門學(xué)習(xí)到提高,都比別的語言門檻低太多太多。在大部分人都有C的背景的時(shí)代,對于Go語言,從入門到能夠上手做項(xiàng)目,最多不過半個(gè)月。Go語言給人的感覺就是太直接了,什么都直接,讀源代碼直接,寫自己的代碼也直接。
Go語言由Google公司開發(fā),并于2009年開源,相比Java/Python/C等語言,Go尤其擅長并發(fā)編程,性能堪比C語言,開發(fā)效率肩比Python,被譽(yù)為“21世紀(jì)的C語言”。
Go語言在云計(jì)算、大數(shù)據(jù)、微服務(wù)、高并發(fā)領(lǐng)域應(yīng)用應(yīng)用非常廣泛。BAT大廠正在把Go作為新項(xiàng)目開發(fā)的首選語言。
Go語言能干什么?
1、服務(wù)端開發(fā):以前你使用C或者C++做的那些事情,用Go來做很合適,例如日志處理、文件系統(tǒng)、監(jiān)控系統(tǒng)等;
2、DevOps:運(yùn)維生態(tài)中的Docker、K8s、prometheus、grafana、open-falcon等都是使用Go語言開發(fā);
3、網(wǎng)絡(luò)編程:大量優(yōu)秀的Web框架如Echo、Gin、Iris、beego等,而且Go內(nèi)置的 net/http包十分的優(yōu)秀;
4、Paas云平臺(tái)領(lǐng)域:Kubernetes和Docker Swarm等;
5、分布式存儲(chǔ)領(lǐng)域:etcd、Groupcache、TiDB、Cockroachdb、Influxdb等;
6、區(qū)塊鏈領(lǐng)域:區(qū)塊鏈里面有兩個(gè)明星項(xiàng)目以太坊和fabric都使用Go語言;
7、容器虛擬化:大名鼎鼎的Docker就是使用Go語言實(shí)現(xiàn)的;
8、爬蟲及大數(shù)據(jù):Go語言天生支持并發(fā),所以十分適合編寫分布式爬蟲及大數(shù)據(jù)處理。
1、簡單易學(xué)。
Go語言的作者本身就很懂C語言,所以同樣Go語言也會(huì)有C語言的基因,所以對于程序員來說,Go語言天生就會(huì)讓人很熟悉,容易上手。
2、并發(fā)性好。
Go語言天生支持并發(fā),可以充分利用多核,輕松地使用并發(fā)。 這是Go語言最大的特點(diǎn)。
描述
Go的語法接近C語言,但對于變量的聲明有所不同。Go支持垃圾回收功能。Go的并行模型是以東尼·霍爾的通信順序進(jìn)程(CSP)為基礎(chǔ),采取類似模型的其他語言包括Occam和Limbo,但它也具有Pi運(yùn)算的特征,比如通道傳輸。
在1.8版本中開放插件(Plugin)的支持,這意味著現(xiàn)在能從Go中動(dòng)態(tài)加載部分函數(shù)。
與C++相比,Go并不包括如枚舉、異常處理、繼承、泛型、斷言、虛函數(shù)等功能,但增加了 切片(Slice) 型、并發(fā)、管道、垃圾回收、接口(Interface)等特性的語言級支持。