年度語言 golang 使用感受

吳YH堅發表於2017-01-15

首先,無意進行語言之爭,畢竟,PHP是世界上最好的語言,沒有之一。這個話題可以停下來了。

2016年已經過去,16年的年度語言給了go語言,而正好這一年我都是用go用得比較多,而且版本從1.2一直用到了1.8,有一些感受,來說說我對這個年度程式語言的一些粗淺理解吧。之前也寫過一篇go語言的文章,但是那時候用得還不是很多,有些特性沒有用上,所以理解上和今天的有些不同。

這篇文章就不分什麼優勢和劣勢了,想到哪裡說到哪裡。

指標還是很重要

先看一個小坑,可能很多初次接觸go的會遇到,go的range迭代用得也很多,下面這個例子不知道你之前遇到過沒有,其實值是不會變的,還是1,2,3。

type a struct {
    b int
}
func main() {
    m := make([]a, 0)
    m = append(m, a{b: 0, c: 0})
    m = append(m, a{b: 1, c: 1})
    m = append(m, a{b: 2, c: 2})
    for _, e := range m {
        e.b = 9
    }
    for _, x := range m {
        fmt.Printf("%v\n", x.b)
    }
}複製程式碼

在range中,後面那個元素是值傳遞,這個很關鍵,所以修改不了元素的內容,而且如果元素很大的話,迭代的開銷還是挺大的,所以要麼你就變成for idx, _ := range m這樣的形式,用下標更新,要麼就變成m := make([]*a, 0)這樣的指標,這樣雖然傳的還是值,不過是個指標的值,一是開銷小,二是可以直接修改元素內容了。

所以說,指標在go中還是不可或缺的一個存在,這也是為什麼像我這種之前都是做C和C++的人喜歡go的原因,因為還是可以指標滿天飛,寫出只能自己看懂的程式碼出去裝逼,然後告訴別人,還是有指標效能好啊。

如果你之前對指標沒概念,或者一直沒怎麼理解指標,那go可能要用好還是要花點時間的,go確實入門很容易,但用好也不是那麼容易,之前我開始用的時候,沒仔細想過這方面的東西,而且特意減少了指標的使用,害怕出現C中的野指標的情況,後來越寫越覺得不是那個味道,go把指標這個功能保留下來還是讓你用起來的,後來寫的程式碼就又開始偏C風格了,指標到處飛。

雖然如此,但為了安全性的考慮,go的指標還是有一些侷限性的,各個型別之間的轉換是不行的,像C語言那樣把各種型別的變數通過指標轉來轉去是很難直接做到的,但是還是給有這種需求的人給開了個口子,那就是unsafe包,看這個包的名字就知道是警告你,這是不安全的啊,掛了別來找我,我出這個包只是為了給你裝逼用的。

比如我們有個需求,需要把一個結構體陣列序列成一個byte陣列後,還需要還原回來,一般的做法是序列化的方式,序列化成json或者用gob序列化成二進位制,然後在反序列化回來,程式碼一般是這樣的。

//do some append 
jsonbyte, err := json.Marshal(YYY)
//do some thing
structArray,err:=json.Unmarshal(jsonbyte,&XXX)複製程式碼

先不說序列化和反序列化都要耗費計算資源,影響速度,而且還有資料的拷貝,這對於一個效能裝逼語言寫的高效能服務怎麼能忍,那隻能祭出指標神器了,並且還得用unsafe包來加光環才行,一般情況下,序列化的過程中那次拷貝跑不掉,你總不需要需要序列化到本身吧,所以序列化的時候直接轉成byte陣列,當然,需要記錄長度。

buffer := new(bytes.Buffer)
err = binary.Write(buffer, binary.LittleEndian, YYY)
lens=len(YYY)
resBytes:=buffer.Buffer()複製程式碼

這時候,YYY結構體陣列就序列化成了resBytes這個byte陣列了,長度是lens,反序列化的時候,直接用指標和unsafe包就行了,整個過程沒有資料拷貝,也沒有序列化和反序列開銷,就像下面程式碼一樣。

XXX := *(*[]structNode)(unsafe.Pointer(&reflect.SliceHeader{
        Data: uintptr(unsafe.Pointer(&resBytes[0])),
        Len:  int(lens),
        Cap:  int(lens),
    }))複製程式碼

當然,這種適合的是structNode結構體裡面的元素是定長的,如果裡面的元素還有byte陣列或者string的話,甚至是int這種和作業系統體系結構相關的元素,就真的是unsafe了,呵呵。

上面兩個例子,告訴我們,指標在golang中保留下來後,對效能有強需求的開發還是有好處的,並且,unsafe包開放出來的功能,至少能讓你知道資料在底層到底存到什麼地址上,心裡面也有底了。

坑爹的map

讀寫安全

對於map不是協程安全這一點,還是有些想吐槽的,其他語言很多也不是執行緒安全的,這本來沒什麼可說的,自己寫程式碼的時候注意一下吧,但是golang本身就是以協程在語言中整合,開協程特別容易的語言,而且是鼓勵大家多多使用協程的思維來程式設計,但是作為一個基礎的整合到語言本身的資料結構,竟然不是協程安全的,我去,您至少提供一個協程安全的版本讓大家去選啊,雖然加讀寫鎖比較容易實現,但是也有幾個問題:

  • 有鎖就必然需要考慮出現死鎖的情況,而且問題還不好查。
  • 由於加入了defer保留字,很多人在使用鎖的時候基本上就是把開鎖和關鎖寫在一起了,這樣有時候程式碼改來改去,邏輯流程變了,容易導致死鎖。
  • 有人說可以把map自己封裝成結構體嘛,開關鎖就看不到了,但是很多時候,剛開始寫的程式碼是不需要多協程的,這時候你用的map都是內部的map,當你發現需要鎖這個map的時候,只有兩種選擇,一是把map封裝成結構體,然後把所有的用了這個map的地方都改掉,二是在外面加一把鎖,把會有衝突的地方鎖起來,第一種方式改動有些大,第二種方式可能會產生bug。

要是有一個可以選的map實現方式就好了,要競爭的時候選讀寫安全的,不競爭的時候選簡單粗暴的。

記憶體池的小坑

很多時候,我們會因為GC的問題,想自己做一個記憶體池,比較主流的做法就是用管道的方式來申請釋放記憶體,現在也有sync.pool包了。

但是用管道的方式來做記憶體池,只適合陣列型別的資料,不適合map,因為陣列的話,你只需要把len置為0,cap不變,吐出去就行了,這樣會減少記憶體的申請開銷,但是map的話,不刪除key,這個key永遠在,所以想用記憶體池來申請map是不行的。

當然,一般情況下也沒有語言能支援map的記憶體池,只不過因為go的管道概念,讓大家都覺得什麼都可以往裡面丟,做記憶體池的時候順便就把map給支援了,這個坑就大了。呵呵,我就是。。。。。。

map和結構體

如果一個map的value是一個結構體的話,那你不能用map[key].sturct.ele=XX給這個map中的這個結構體的元素賦值,還好是編譯性的語言,會蹦一條編譯cannot assign to錯誤出來,算個小坑吧,由於map會在使用的過程中不斷的申請新記憶體,拷貝物件到新記憶體中,所以直接的定址是不支援的。

關於泛型

沒有泛型是很多人覺得go語言不夠人情味的一個地方,我也是其中之一,居然沒有泛型,你叫人怎麼寫出裝逼的,簡潔的程式碼??!!而且golang的設計者們居然說不準備支援泛型(不過目前好像改口了,說Go2.0會考慮支援泛型,呵呵),這點簡直了,為什麼不支援泛型,難道interface{}就夠用了?不停的型別判斷必然導致程式碼的難看和效能的損失,這點都想不清楚嗎?但是。。。。。

但是如果我們仔細想想泛型的實現就稍微理解了他們了,首先,泛型的實現有兩種方式,一種是C++的模板方式,一種是JAVA的型別擦除(好像叫這個名字吧)方式,我們來看看這兩種方式的泛型,再來猜猜看golang為什麼不支援了。

  • C++方式的泛型實現是通過模板的,簡單的說就是編譯的時候通過分析這個泛型函式的呼叫方,然後產生出對應的函式,這樣做的好處和壞處都很明顯。
    • 好處就是不需要執行的時候進行型別的判斷從而節省了執行時的時間。
    • 壞處主要有兩點,一是編譯時間變長,二是如果型別很多的話,會造成最後的生成程式碼變得很多。
  • JAVA的泛型是通過型別擦除的方式來實現的,我本身不是寫JAVA的,對這部分研究也不是很清楚,只知道他不是編譯時替換型別的,而是把型別都擦除了,比如都變成obj了,在執行時需要的時候再轉回來(對java這段描述不是很確定哈),個人覺得就是先把型別轉成*void,這不就擦除了麼,然後用的時候再轉回來就行了哈。優勢和劣勢也很明顯
    • 好處就是程式碼不會膨脹了。
    • 劣勢就是這樣的話,執行時還是需要做型別的判斷,增加了消耗,可能還會不安全,因為只要是執行時判斷,你就有可能對一個int型別插入一個string。

好了,我們簡單的說了一下泛型的原理,那麼如果go要實現泛型的話,基本上就是這兩種方式,第二種方式是不是感覺和interface有種似曾相識的趕腳呢?恩,看上去一樣,還是有本質區別的,第二種java那種方式是JIT實現,而interface是runtime的執行時實現,效率差得不是一點半點的。如果用第一種方式進行編譯時的模板擴充套件呢?同樣會遇到程式碼增多的情況,golang的目標檔案本來就是把所有東西都整合進行來了,本來就很大了,再這麼整一下,估計目標檔案更大了。

我覺得即便golang開放泛型,估計也是用第一種方式,因為如果用執行時的方式的話,給runtime排程器平增不少壓力,而golang肯定不會用JIT吧,所以第二種實現方式估計有點夠嗆。

一些其他的

對於GC,就不吐槽了,因為畢竟,真有GC問題的話,我就用CGO了,呵呵,或者說在設計的時候就會直接考慮某些模組用C來做了,而且目前的go版本,GC已經很不錯了,大部分應用沒啥問題了,golang把協程整合進語言中,勢必導致大家不計效能問題,奔放的開協程,那這個坑就只能google自己來填了,新版本(1.8)對GC的支援已經很好了,但是,對效能有強要求的服務,某些程式碼,還是用C吧,哈哈。

當然,要是go有一個不帶gc的實現,自己來管理記憶體的版本,那就好了,語法比C舒服,還有協程和管道這些東西,要是能自己管理堆記憶體的話,那就完美了。

最後,還有一些沒有說完的,這篇就不說了,下次接著聊聊channel和goroutine,以及和C的混合程式設計。


如果你覺得不錯,歡迎轉發給更多人看到,也歡迎關注我的公眾號,主要聊聊搜尋,推薦,廣告技術,還有瞎扯。。文章會在這裡首先發出來:)掃描或者搜尋微訊號XJJ267或者搜尋西加加語言就行

年度語言 golang 使用感受

相關文章