Go语言很好很强大,但我有几个问题想吐槽
Go是一門非常不錯的編程語言。然而,我在公司的Slack編程頻道中對Go的抱怨卻越來越多(猜到我是做啥了的吧?),因此我認為有必要把這些吐槽寫下來并放在這里,這樣當(dāng)人們問我抱怨什么時,我給他們一個鏈接就行了。
先聲明一下,在過去的一年里,我大量地使用Go語言開發(fā)命令行應(yīng)用程序、scc、lc和API。 其中既有供客戶端調(diào)用的大規(guī)模API,也有即將在https://searchcode.com/ 使用的語法高亮顯示器。
我這些批評全部是針對Go語言的。但是,我對使用過的每種語言都有不滿。 我非常贊同下面的話:
“世界上只有兩種語言:人們抱怨的語言和沒人使用的語言。” —— Bjarne Stroustrup
1 不支持函數(shù)式編程
我并不是一個函數(shù)式編程狂熱者。 說到Lisp語言,我首先想到的是語言障礙。
這可能是Go語言最大的痛點了。 與大部分人不同,我不希望Go支持泛型,因為它會為多數(shù)Go項目帶來不必要的復(fù)雜性。 我希望Go語言支持適用于內(nèi)置切片和Map的函數(shù)式方法。 切片和Map具有通用性,并且可以容納任何類型,從這個意義上講,它們已經(jīng)非常神奇。在Go語言中只有利用接口才能實現(xiàn)類似效果,但這樣一來將喪失安全性和速度。
例如,請考慮下面的問題。
給定兩個字符串切片,找出二者都包含的字符串,并將其放入新的切片以備后用。
existsBoth := []string{}for _, first := range firstSlice {\tfor _, second := range secondSlice {\t\tif first == second {\t\t\texistsBoth = append(existsBoth, proxy)\t\t\tbreak\t\t}\t}}上面是一個用Go語言實現(xiàn)的簡單方案。當(dāng)然還有其它方法,比如借助Map來減少運行時間。這里我們假設(shè)內(nèi)存足夠用或者切片都不太大,同時假設(shè)優(yōu)化運行時間帶來的復(fù)雜性遠超收益,因此不值得優(yōu)化。作為對比,使用Java流和函數(shù)式編程把相同的邏輯重寫如下:
var existsBoth = firstList.stream() .filter(x -\u0026gt; secondList.contains(x)) .collect(Collectors.toList());上面的代碼隱藏了算法的復(fù)雜性,但是,你更容易理解它實際做的事情。
與Go代碼相比,Java代碼的意圖一目了然。 真正靈活之處在于,添加更多的過濾條件易如反掌。 如果使用Go語言添加下面例子中的過濾條件,我們需要在嵌套的for循環(huán)中再添加兩個if條件。
var existsBoth = firstList.stream() .filter(x -\u0026gt; secondList.contains(x)) .filter(x -\u0026gt; x.startsWith(needle)) .filter(x -\u0026gt; x.length() \u0026gt;= 5) .collect(Collectors.toList());有些借助go generate命令的項目可以幫你實現(xiàn)上面的一些功能。但是,如果缺少良好的IDE支持,抽取循環(huán)中的語句作為單獨的方法是一件低效又麻煩的事情 。
2 通道/并行切片處理
Go通道通常都很好用。 但它并不能提供無限的并發(fā)能力。它確實存在一些會導(dǎo)致永久阻塞的問題,但這些問題用競爭檢測器能很容易地解決。對于數(shù)量不確定或不知何時結(jié)束的流式數(shù)據(jù),以及非CPU密集型的數(shù)據(jù)處理方法,Go通道都是很好的選擇。
Go通道不太適合并行處理大小已知的切片。
多線程編程、理論和實踐
幾乎在其它任何語言中,當(dāng)列表或切片很大時,為了充分利用所有CPU內(nèi)核,通常都會使用并行流、并行Linq、Rayon、多處理或其它語法來遍歷列表。遍歷后的返回值是一個包含已處理元素的列表。 如果元素足夠多,或者處理元素的函數(shù)足夠復(fù)雜,多核系統(tǒng)會更高效。
但是在Go語言中,實現(xiàn)高效處理所需要做的事情卻并不顯而易見。
一種可能的解決方案是為切片中的每個元素都創(chuàng)建一個Go例程。 由于Go例程的開銷很低,因此從某種程度上來說這是一個有效的策略。
toProcess := []int{1,2,3,4,5,6,7,8,9}var wg sync.WaitGroupfor i, _ := range toProcess {\twg.Add(1)\tgo func(j int) {\t\ttoProcess[j] = someSlowCalculation(toProcess[j])\t\twg.Done()\t}(i)}wg.Wait()fmt.Println(toProcess)上面的代碼會保持切片中元素的順序,但我們假設(shè)不必保持元素順序。
這段代碼的第一個問題是增加了一個WaitGroup,并且必須要記得調(diào)用它的Add和Done方法。這增加了開發(fā)人員的工作量。如果弄錯了,這個程序不會產(chǎn)生正確的輸出,結(jié)果是要么輸出不確定,要么程序永不結(jié)束。此外,如果列表很長,你會為每個列表創(chuàng)建一個Go例程。正如我之前所說,這不是問題,因為Go能輕松搞定。問題在于,每個Go例程都會爭搶CPU時間片。因此,這不是執(zhí)行該任務(wù)的最有效方式。
你可能希望為每個CPU內(nèi)核創(chuàng)建一個Go例程,并讓這些例程選取列表并處理。創(chuàng)建Go例程的開銷很小,但是在一個非常緊湊的循環(huán)中創(chuàng)建它們會使開銷陡增。當(dāng)我開發(fā)scc時就遇到了這種情況,因此我采用了每個CPU內(nèi)核對應(yīng)一個Go例程的策略。在Go語言中,要這樣做的話,你首先要創(chuàng)建一個通道,然后遍歷切片中的元素,使函數(shù)從該通道讀取數(shù)據(jù),之后從另一個通道讀取。我們來看一下。
toProcess := []int{1,2,3,4,5,6,7,8,9}var input = make(chan int, len(toProcess))for i, _ := range toProcess {\tinput \u0026lt;- i}close(input)var wg sync.WaitGroupfor i := 0; i \u0026lt; runtime.NumCPU(); i++ {\twg.Add(1)\tgo func(input chan int, output []int) {\t\tfor j := range input {\t\t\ttoProcess[j] = someSlowCalculation(toProcess[j])\t\t}\t\twg.Done()\t}(input, toProcess)}wg.Wait()fmt.Println(toProcess)上面的代碼創(chuàng)建了一個通道,然后遍歷切片,將索引值放入通道。 接下來我們?yōu)槊總€CPU內(nèi)核創(chuàng)建一個Go例程,操作系統(tǒng)會報告并處理相應(yīng)的輸入,然后等待,直到所有操作完成。這里有很多代碼需要理解。
然而,這種實現(xiàn)有待商榷。如果切片非常大,通道的緩沖區(qū)長度和切片大小相同,你可能不希望創(chuàng)建一個有這么大緩沖區(qū)的通道。因此,你應(yīng)該創(chuàng)建另一個Go例程來遍歷切片,并將切片中的值放入通道,完成后關(guān)閉通道。 但這樣一來代碼會變得冗長,因此我把它去掉了。我希望可以大概地闡明基本思路。
使用Java語言大致這樣實現(xiàn):
var firstList = List.of(1,2,3,4,5,6,7,8,9);firstList = firstList.parallelStream() .map(this::someSlowCalculation) .collect(Collectors.toList());通道和流并不等價。 使用隊列去仿寫Go代碼的邏輯更好一些,因為它們更具有可比性,但我們的目的不是進行1對1的比較。 我們的目標是充分利用所有的CPU內(nèi)核處理切片或列表。
如果someSlowCalucation方法調(diào)用了網(wǎng)絡(luò)或其它非CPU密集型任務(wù),這當(dāng)然不是問題。 在這種情況下,通道和Go例程都會表現(xiàn)得很好。
這個問題與問題#1有關(guān)。如果Go語言支持適用于切片/Map對象的函數(shù)式方法,那么就能實現(xiàn)這個功能。 但是,如果Go語言支持泛型,有人就可以把上面的功能封裝成像Rust的Rayon一樣的庫,讓每個人都從中受益,這就很令人討厭了(我不希望Go支持泛型)。
順便說一下,我認為這個缺陷妨礙了Go語言在數(shù)據(jù)科學(xué)領(lǐng)域的成功,這也是為什么Python仍然是數(shù)據(jù)科學(xué)領(lǐng)域的王者。 Go語言在數(shù)值操作方面缺乏表現(xiàn)力和能力,原因就是以上討論的這些。
3 垃圾回收器
Go的垃圾回收器做得非常不錯。我開發(fā)的應(yīng)用程序通常都會因為新版本的改進而變得更快。但是,它以低延遲為最高優(yōu)先級。對于API和UI應(yīng)用來說,這個選擇完全可以接受。對于包含網(wǎng)絡(luò)調(diào)用的應(yīng)用,因為網(wǎng)絡(luò)調(diào)用往往會是瓶頸,所以它也沒問題。
我發(fā)現(xiàn)的問題是Go對UI應(yīng)用來講一點也不好(我不知道它有任何良好的支持)。如果你想要盡可能高的吞吐量,那這個選擇會讓你很受傷。這是我開發(fā)scc時遇到的一個主要問題。scc是一個CPU密集型的命令行工具。為了解決這個問題,我不得不在代碼里添加邏輯關(guān)閉GC,直到達到某個閾值。但是我又不能簡單的禁用它,因為有些任務(wù)會很快耗盡內(nèi)存。
缺乏對GC的控制時常令人沮喪。你得學(xué)會適應(yīng)它,但是,有時候如果能做到這樣該有多好:“嘿,這些代碼確實需要盡可能快地運行,所以如果你能在高吞吐模式運行一會,那就太好了。”
我認為這種情況在Go 1.12版本中有所改善,因為GC得到了進一步的改進。但僅僅是關(guān)閉和打開GC還不夠,我期望更多的控制。 如果有時間我會再進行研究。
4 錯誤處理
我并不是唯一一個抱怨這個問題的人,但我不吐不快。
value, err := someFunc()if err != nil {\t// Do something here}err = someOtherFunc(value)if err != nil {\t// Do something here}上面的代碼很乏味。 Go甚至不會像有些人建議的那樣強制你處理錯誤。 你可以使用“_”顯式忽略它(這是否算作對它進行了處理呢?),你還可以完全忽略它。比如上面的代碼可以重寫為:
value, _ := someFunc()someOtherFunc(value)很顯然,我顯式忽略了someFunc方法的返回。someOtherFunc(value)方法也可能返回錯誤值,但我完全忽略了它。 這里的錯誤都沒有得到處理。
說實話,我不知道如何解決這個問題。 我喜歡Rust中的“?” 運算符,它可以幫助避免這種情況。V-Lang https://vlang.io/ 看起來也可能有一些有趣的解決方案。
另一個辦法是使用可選類型(Optional types)并去掉nil,但這不會發(fā)生在Go語言里,即使是Go 2.0版本,因為它會破壞向后兼容性。
結(jié)語
Go仍然是一種非常不錯的語言。如果你讓我寫一個API,或者完成某個需要大量磁盤/網(wǎng)絡(luò)調(diào)用的任務(wù),它依然是我的首選。現(xiàn)在我會用Go而非Python去完成很多一次性任務(wù),數(shù)據(jù)合并任務(wù)是例外,因為函數(shù)式編程的缺失使執(zhí)行效率難以達到要求。
與Java不同,Go語言盡量遵循“最小驚喜“原則。比如可以這樣比較字兩個符串是否相等:stringA == stringB。但如果你這樣比較兩個切片,那么會產(chǎn)生編譯錯誤。這些都是很好的特性。
的確,二進制文件還可以變的更小(一些編譯標志和upx可以解決這個問題),我希望它在某些方面變得更快,GOPATH雖然不是很好,但也沒有人們想得那么糟糕,默認的單元測試框架缺少很多功能,模擬(mocking)有點讓人痛苦…
它仍然是我使用過的效率較高的語言之一。我會繼續(xù)使用它,雖然我希望https://vlang.io/能最終發(fā)布,并解決我的很多抱怨。V語言或Go 2.0,Nim或Rust。現(xiàn)在有很多很酷的新語言可以使用,我們開發(fā)人員真的要被寵壞了。
查看英文原文:
https://boyter.org/posts/my-personal-complaints-about-golang/
與50位技術(shù)專家面對面20年技術(shù)見證,附贈技術(shù)全景圖總結(jié)
以上是生活随笔為你收集整理的Go语言很好很强大,但我有几个问题想吐槽的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Ubuntu 19.04 Beta 发布
- 下一篇: k8s 简介