當前位置:
首頁 > 知識 > Go 語言為何不受待見?

Go 語言為何不受待見?

Go語言為何不受待見?事實上,Go仍然是一種相當不錯的語言,並且逐漸取代Python成為很多人的首選語言。但是其卻有一些問題,使得開發速度大受影響。本文就跟隨作者一起解讀下Go中那些「硬傷」設計。

Go 語言為何不受待見?

打開今日頭條,查看更多圖片

作者 | Ben E. C. Boyter

譯者 | 蘇本如

責編 | 郭芮

出品 | CSDN(ID:CSDNnews)

以下為譯文:

Go作為一種編程語言來說是相當體面的,然而,我在公司Slack(譯者註:一種團隊協作工具)的編程頻道上對它的抱怨卻越來越多(猜到我是做啥了的吧?)。我想我還是把這些抱怨寫下來放在這裡,這樣當人們問我到底抱怨什麼時,我就可以給他們一個鏈接,讓他們直接到這裡來看。

Go 語言為何不受待見?

過去一年左右的時間我都一直在大量地使用Go語言來編程,寫一些命令行應用程序、scc(https://github.com/boyter/scc/)、lc(https://github.com/boyter/lc/)和一些API等等。其中包括一個供客戶端調用代碼高亮插件(https://github.com/boyter/searchcode-server-highlighter)的大規模API,這個代碼高亮插件很快將會在https://searchcode.com/網站中使用。

我在這裡的批評是專門針對Go編程語言的。然而,我對我使用的每種編程語言都有抱怨。這裡我引用一下C++編程語言之父Bjarne Stroustrup說過的一句話:


「世界上只有兩種編程語言:一種是人們抱怨的語言,另一種是沒人使用的語言。」——Bjarne Stroustrup

缺乏函數式編程

我不是一個函數式編程的狂熱分子。Lisp語言讓我首先想到的是語言障礙,

這可能是我使用Go語言編程時最痛苦的地方。

和大多數開發者不一樣,我不想要泛型,我認為這隻會給大多數Go項目增加不必要的複雜性。我想要的是一些可以應用於內置的Slice(切片)和Map類型之上的函數方法。Slice和Map這兩種類型都可以容納任何類型,而且是泛型的,這種在某種意義上很神奇。然而Go的泛型不使用介面的話,就無法實現自己,這樣就損失了所有的安全性和性能。

舉個例子,考慮下面的需求。

給定兩個字元串片斷,找出這兩段字元串片斷中都包含的相同的子字元串,並將其放入一個新的字元串片斷中,以便我們稍後處理它。

existsBoth := []string{}
for _, first := range firstSlice {
for _, second := range secondSlice {
if first == second {
existsBoth = append(existsBoth, proxy)
break
}
}
}

上面的Go語言的解決方法很簡單。但我們還有其他方法,如使用Map來解決這個問題,使用Map可以減少運行時間,但是如果我們的內存容量有限,或者我們沒有很大的片斷需要處理,那麼額外的運行時間並不足以抵消它帶來的複雜性。

讓我們將其與Java中使用Stream和函數編程來實現相同邏輯的代碼比較一下。

var existsBoth = firstList.stream()
.filter(x -> secondList.contains(x))
.collect(Collectors.toList());

上面的代碼確實將演算法的複雜性隱藏起來,從代碼上看清楚要實現的邏輯容易得多。

與實現同樣功能的Go代碼相比,上面的代碼的意圖是顯而易見的,真正的簡潔之處是添加額外的過濾器也變得非常簡單。而如果用Go語言實現像下面的示例一樣添加額外的過濾器,我們就必須在已經嵌套的for循環中再添加兩個if條件。

var existsBoth = firstList.stream()
.filter(x -> secondList.contains(x))
.filter(x -> x.startsWith(needle))
.filter(x -> x.length() >= 5)
.collect(Collectors.toList());

有一些使用Go Generate的項目可以為你做到上面所說的,但是如果沒有好的IDE支持的話,將上面的循環提取到它自己的方法中會非常笨重,而且會帶來更多的麻煩。

通道(channel)/並行切片(Slice)處理

Go的通道(channel)通常非常簡潔。雖然它們存在一些問題會導致它永久阻塞,但它們並不打算提供安全的並發性,因為通過競爭檢測機制可以很容易地擺脫這些問題。對於一個不知道有多少個值或何時結束的流,或者如果處理這些值的方法不受CPU的制約,那麼通道是一個很好的選擇。

通道不太擅長的是處理那些預先知道大小並希望並行處理的切片(Slice)。

並行處理在幾乎所有其他語言中都很常見,通常發生在你有一個大的列表或切片,使用並行流、並行LINQ(語言集成查詢)、Rayon(一種數據並行庫)、多進程或其他一些語法,使用所有可用的CPU,對該列表/切片進行迭代處理時。你將它們應用到你的列表上,然後返回處理好的元素列表。如果你的列表有太多的元素,或者你正在使用的函數太複雜,使用一個多核系統應該也可以更快地完成。

然而,在Go語言中,你需要怎麼實現它並不明確。

一種可能的解決方法是為切片中的每個元素生成一個goroutine。因為goroutine的開銷很低,所以在某種程度上,這是一個有效的策略。

toProcess := []int{1,2,3,4,5,6,7,8,9}
var wg sync.WaitGroup
for i, _ := range toProcess {
wg.Add(1)
go func(j int) {
toProcess[j] = someSlowCalculation(toProcess[j])
wg.Done()
}(i)
}
wg.Wait()
fmt.Println(toProcess)

上面的代碼會保持切片中元素的順序,但是我們的例子並不要求實現這點。

上面的問題首先是添加一個waitgroup,並且必須記住遞增並調用它。這對開發人員開說是額外負擔。如果弄錯了,這個程序將不會產生正確的輸出,可能是不確定的結果,也可能永遠不會執行完成。另外,如果你的列表很長,你要為列表中每個單獨的元素生成一個goroutine。正如我之前所說,這本身不是一個問題,因為Go語言能毫無問題地做到這一點。但問題是,每一個goroutine都要為使用CPU的時間片而競爭。因此這不是執行此任務的最有效方法。

你可能想做的是為每個CPU生成一個goroutine,並讓它們依次挑選處理它的列表。增加一個goroutine的開銷很小,但是對於一個迭代次數很多的循環來說,這個開銷並不算小。當我在為scc項目工作時,我遇到了這個問題,它在每個CPU的內核上創建了一個goroutine。如果要完全用Go語言的方式來解決這個問題,你就需要創建一個通道,然後循環你的每個切片元素,讓你的函數從該通道讀取,然後再從另一個通道讀取。

讓我們看看代碼。

toProcess := []int{1,2,3,4,5,6,7,8,9}
var input = make(chan int, len(toProcess))
for i, _ := range toProcess {
input <- i
}
close(input)
var wg sync.WaitGroup
for i := 0; i < runtime.NumCPU(); i++ {
wg.Add(1)
go func(input chan int, output []int) {
for j := range input {
toProcess[j] = someSlowCalculation(toProcess[j])
}
wg.Done()
}(input, toProcess)
}
wg.Wait()
fmt.Println(toProcess)

上面的代碼先是創建了一個通道,循環我們的切片並將每個值放入其中。接著,為每個CPU內核創建一個goroutine來處理輸入的值,然後等待它全部完成。要消化的代碼很多。

這不是一個你應該怎麼做的問題,因為如果你的切片非常大,你可能不想有一個具有相同長度的緩衝區的通道,所以你實際上應該創建另一個goroutine來循環切片,並將這些值放入通道。當處理完成後,它關閉通道。我已經刪除了這個代碼,因為它使代碼變得更長,而且我已經基本上知道怎麼做了。

Java的做法和上面大致相同。

var firstList = List.of(1,2,3,4,5,6,7,8,9);
firstList = firstList.parallelStream()
.map(this::someSlowCalculation)
.collect(Collectors.toList());

是的,Go語言的通道(channel)和Java中的流(stream)並不是一回事,通道更接近於Java中的隊列(queue),但我們這裡的目的不是1對1的對比。我們想要的是使用我們所有的CPU內核來處理一個切片/列表。

當然,如果某個slowcaluation實際上是一個在網路上調用的方法,或者是其他一些需要大量CPU的方法,那麼這就不是一個問題。在這種情況下,通道和goroutine都很出色。

這一問題與缺乏函數式編程有關。如果Go語言在slice/map對象之上有函數方法,那麼添加這個功能是可能的。這也很煩人,因為如果Go支持泛型的話,就會有人可以把上面談到的寫成一個函數庫,就像Rust的Rayon一樣,每個人都會受益。

順便說一句,我認為這一點阻礙了Go語言在數據科學領域的任何成功,因此,為什麼Python仍然是那裡的王者。而Go語言在數字操作中缺乏表現力和力量——以上就是原因。

垃圾回收(GC)

Go語言的垃圾回收機制非常可靠。每次Go語言版本更新,我都發現我的應用程序變得更快了,原因通常是因為GC的改進。將延遲的優先順序置於所有其它要求之上,對於API和UI來說,是一個完全可以接受的選擇。同樣地它也適用於任何有網路呼叫的情況,這些呼叫也會成為瓶頸。

關鍵是Go語言對UI功能的實現沒有任何好處(據我所知沒有合適的綁定),當你需要儘可能大的吞吐量時,這個選擇確實會傷害你。我在處理scc項目時遇到了一個大問題,scc是一個命令行應用程序,對CPU的要求很高。這是個問題,我添加了一個邏輯來關閉內存回收機制,直到內存使用量達到閾值。但是,我不能禁用它,因為程序在某些情況下工作時很快就會耗盡內存。

對GC缺乏控制有時令人沮喪。你學會了接受它,但有時你會說「嘿,這裡的代碼真的需要儘可能快的運行,所以如果能切換到高吞吐量模式一段時間,那就太好了。」

我認為隨著Go語言的1.12版本的發布,這一點變得越來越不可能了,在這個版本中,GC看起來再次得到了改進,但是僅僅關閉和打開GC並不是我想要的控制。有時間的話我會再次深入了解一下。

錯誤處理

我不是唯一一個對這點有抱怨的人,但我必須寫出來。

value, err := someFunc()
if err != nil {
// Do something here
}
err = someOtherFunc(value)
if err != nil {
// Do something here
}

上面的代碼看起來相當乏味。Go語言甚至不強制你處理大家建議的錯誤。你可以顯式忽略它(這是否算作處理它?),你甚至可以完全忽略它。例如,我可以像這樣重寫上面的內容:

value, _ := someFunc()
someOtherFunc(value)

你很容易發現我省略了somefunc返回的內容,但是someotherfunc(value)同時也可以返回錯誤,而我卻完全忽略了這個錯誤,對它不作任何處理。

老實說,我不知道有這裡有什麼解決辦法。不過我喜歡Rust言語的問號(?)操作符,它可以避免這個問題。另外V-Lang(https://vlang.io/)看起來也可能有一些有趣的解決方案。

另一個想法是可選類型(Optional Type)和刪除nil,但是這些在Go語言的2.0版本中是永遠不會出現的,因為它會破壞向後兼容性。

總結

總的來講,Go仍然是一種相當不錯的語言。如果你要我要寫一個API,或者一些需要快速進行大量磁碟/網路調用的應用,它仍然是我的第一選擇。事實上,我正處在這樣一個階段:Go已經取代Python,成為我要完成的大量的一次性任務的首選語言。數據合併任務除外,因為缺乏函數式編程仍然是一件痛苦的事情,這使得開發速度大受影響。

對諸如像字元串stringA == stringB和編譯錯誤的比較,你會發現Go語言的切片用在這裡非常合適。它不像我在上面用來比較的Java語言那樣經常有出人意料的結果。

Go的二進位文件的大小可以更小(一些編譯開關和upx(可執行文件壓縮工具)可以解決這個問題),我希望它在某些方面運行得更快一些,GOPATH不是很好,但也沒有每個人所說的那麼糟糕,默認的單元測試框架缺少很多功能,Mocking有點痛苦等等。

Go仍然是我使用過的一種更有效的語言。我會繼續使用它,儘管我希望https://vlang.io/ 最終能夠發布並且解決我的許多投訴的問題。它可能Go 2.0版,可能是Nim,也可能是Rust。現在有很多很酷的新語言可以玩。我們的開發人員真的被寵壞了。


原文:https://boyter.org/posts/my-personal-complaints-about-golang/

本文為 CSDN 翻譯,如需轉載,請註明來源出處。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 CSDN 的精彩文章:

為什麼那麼多人用「ji32k7au4a83」作密碼?
從模糊搜索 1.0 到 3.0 的演算法迭代歷程 | 技術頭條

TAG:CSDN |