免费观看又色又爽又黄的小说免费_美女福利视频国产片_亚洲欧美精品_美国一级大黄大色毛片

Go語言很好很強大,但我有幾個問題想吐槽

2021-01-30    分類: 網站建設

Go 是一門非常不錯的編程語言,并且逐漸取代 Python 成為很多人的選語言。但它也有一些缺點讓很多開發者忍不住吐槽,比如它在函數式編程、通道 / 并行切片處理、內存垃圾回收、錯誤處理等方面都有一些問題。本文作者將 Go 存在的“硬傷”設計記錄了下來,與大家分享、討論。

Go 是一門非常不錯的編程語言。然而,我在公司的 Slack 編程頻道中對 Go 的抱怨卻越來越多(猜到我是做啥了的吧?),因此我認為有必要把這些吐槽寫下來并放在這里,這樣當人們問我抱怨什么時,我給他們一個鏈接就行了。


先聲明一下,在過去的一年里,我大量地使用 Go 語言開發命令行應用程序、scc、lc 和 API。 其中既有供客戶端調用的大規模 API,也有即將在 https://searchcode.com/ 使用的 語法高亮顯示器。

我這些批評全部是針對 Go 語言的。但是,我對使用過的每種語言都有不滿。 我非常贊同下面的話:

“世界上只有兩種語言:人們抱怨的語言和沒人使用的語言。” —— Bjarne Stroustrup

1、不支持函數式編程

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

這可能是 Go 語言大的痛點了。 與大部分人不同,我不希望 Go 支持泛型,因為它會為多數 Go 項目帶來不必要的復雜性。 我希望 Go 語言支持適用于內置切片和 Map 的函數式方法。 切片和 Map 具有通用性,并且可以容納任何類型,從這個意義上講,它們已經非常神奇。在 Go 語言中只有利用接口才能實現類似效果,但這樣一來將喪失安全性和速度。

例如,請考慮下面的問題。

給定兩個字符串切片,找出二者都包含的字符串,并將其放入新的切片以備后用。

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

上面是一個用 Go 語言實現的簡單方案。當然還有其它方法,比如借助 Map 來減少運行時間。這里我們假設內存足夠用或者切片都不太大,同時假設優化運行時間帶來的復雜性遠超收益,因此不值得優化。作為對比,使用 Java 流和函數式編程把相同的邏輯重寫如下:

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

上面的代碼隱藏了算法的復雜性,但是,你更容易理解它實際做的事情。

與 Go 代碼相比,Java 代碼的意圖一目了然。 真正靈活之處在于,添加更多的過濾條件易如反掌。 如果使用 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 支持,抽取循環中的語句作為單獨的方法是一件低效又麻煩的事情 。

2、通道 / 并行切片處理

Go 通道通常都很好用。 但它并不能提供無限的并發能力。它確實存在一些會導致永久阻塞的問題,但這些問題用競爭檢測器能很容易地解決。對于數量不確定或不知何時結束的流式數據,以及非 CPU 密集型的數據處理方法,Go 通道都是很好的選擇。

Go 通道不太適合并行處理大小已知的切片。


3、多線程編程、理論和實踐

幾乎在其它任何語言中,當列表或切片很大時,為了充分利用所有 CPU 內核,通常都會使用并行流、并行 Linq、Rayon、多處理或其它語法來遍歷列表。遍歷后的返回值是一個包含已處理元素的列表。 如果元素足夠多,或者處理元素的函數足夠復雜,多核系統會更高效。

但是在 Go 語言中,實現高效處理所需要做的事情卻并不顯而易見。

一種可能的解決方案是為切片中的每個元素都創建一個 Go 例程。 由于 Go 例程的開銷很低,因此從某種程度上來說這是一個有效的策略。

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

上面的代碼會保持切片中元素的順序,但我們假設不必保持元素順序。

這段代碼的第一個問題是增加了一個 WaitGroup,并且必須要記得調用它的 Add 和 Done 方法。這增加了開發人員的工作量。如果弄錯了,這個程序不會產生正確的輸出,結果是要么輸出不確定,要么程序永不結束。此外,如果列表很長,你會為每個列表創建一個 Go 例程。正如我之前所說,這不是問題,因為 Go 能輕松搞定。問題在于,每個 Go 例程都會爭搶 CPU 時間片。因此,這不是執行該任務的最有效方式。

你可能希望為每個 CPU 內核創建一個 Go 例程,并讓這些例程選取列表并處理。創建 Go 例程的開銷很小,但是在一個非常緊湊的循環中創建它們會使開銷陡增。當我開發 scc 時就遇到了這種情況,因此我采用了每個 CPU 內核對應一個 Go 例程的策略。在 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.WaitGroupfor 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 內核創建一個 Go 例程,操作系統會報告并處理相應的輸入,然后等待,直到所有操作完成。這里有很多代碼需要理解。

然而,這種實現有待商榷。如果切片非常大,通道的緩沖區長度和切片大小相同,你可能不希望創建一個有這么大緩沖區的通道。因此,你應該創建另一個 Go 例程來遍歷切片,并將切片中的值放入通道,完成后關閉通道。 但這樣一來代碼會變得冗長,因此我把它去掉了。我希望可以大概地闡明基本思路。

使用 Java 語言大致這樣實現:

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 內核處理切片或列表。

如果 someSlowCalucation 方法調用了網絡或其它非 CPU 密集型任務,這當然不是問題。 在這種情況下,通道和 Go 例程都會表現得很好。

這個問題與問題#1 有關。如果 Go 語言支持適用于切片 /Map 對象的函數式方法,那么就能實現這個功能。 但是,如果 Go 語言支持泛型,有人就可以把上面的功能封裝成像 Rust 的 Rayon 一樣的庫,讓每個人都從中受益,這就很令人討厭了(我不希望 Go 支持泛型)。

順便說一下,我認為這個缺陷妨礙了 Go 語言在數據科學領域的成功,這也是為什么 Python 仍然是數據科學領域的王者。 Go 語言在數值操作方面缺乏表現力和能力,原因就是以上討論的這些。

4、垃圾回收器

Go 的垃圾回收器做得非常不錯。我開發的應用程序通常都會因為新版本的改進而變得更快。但是,它以低延遲為高優先級。對于 API 和 UI 應用來說,這個選擇完全可以接受。對于包含網絡調用的應用,因為網絡調用往往會是瓶頸,所以它也沒問題。

我發現的問題是 Go 對 UI 應用來講一點也不好(我不知道它有任何良好的支持)。如果你想要盡可能高的吞吐量,那這個選擇會讓你很受傷。這是我開發 scc 時遇到的一個主要問題。scc 是一個 CPU 密集型的命令行工具。為了解決這個問題,我不得不在代碼里添加邏輯關閉 GC,直到達到某個閾值。但是我又不能簡單的禁用它,因為有些任務會很快耗盡內存。

缺乏對 GC 的控制時常令人沮喪。你得學會適應它,但是,有時候如果能做到這樣該有多好:“嘿,這些代碼確實需要盡可能快地運行,所以如果你能在高吞吐模式運行一會,那就太好了。”




我認為這種情況在 Go 1.12 版本中有所改善,因為 GC 得到了進一步的改進。但僅僅是關閉和打開 GC 還不夠,我期望更多的控制。 如果有時間我會再進行研究。

5、錯誤處理

我并不是唯一一個抱怨這個問題的人,但我不吐不快。

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 types)并去掉 nil,但這不會發生在 Go 語言里,即使是 Go 2.0 版本,因為它會破壞向后兼容性。

結 語

Go 仍然是一種非常不錯的語言。如果你讓我寫一個 API,或者完成某個需要大量磁盤 / 網絡調用的任務,它依然是我的選。現在我會用 Go 而非 Python 去完成很多一次性任務,數據合并任務是例外,因為函數式編程的缺失使執行效率難以達到要求。

與 Java 不同,Go 語言盡量遵循“最小驚喜“原則。比如可以這樣比較字兩個符串是否相等:stringA == stringB。但如果你這樣比較兩個切片,那么會產生編譯錯誤。這些都是很好的特性。

的確,二進制文件還可以變的更小(一些 編譯標志和 upx 可以解決這個問題),我希望它在某些方面變得更快,GOPATH 雖然不是很好,但也沒有人們想得那么糟糕,默認的單元測試框架缺少很多功能,模擬(mocking)有點讓人痛苦......

它仍然是我使用過的效率較高的語言之一。我會繼續使用它,雖然我希望 https://vlang.io/ 能最終發布,并解決我的很多抱怨。V 語言或 Go 2.0,Nim 或 Rust。現在有很多很酷的新語言可以使用,我們開發人員真的要被寵壞了。

本文標題:Go語言很好很強大,但我有幾個問題想吐槽
標題URL:http://m.newbst.com/news7/98207.html

成都網站建設公司_創新互聯,為您提供網站營銷網站排名網站導航手機網站建設網站設計公司品牌網站設計

廣告

聲明:本網站發布的內容(圖片、視頻和文字)以用戶投稿、用戶轉載內容為主,如果涉及侵權請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網站立場,如需處理請聯系客服。電話:028-86922220;郵箱:631063699@qq.com。內容未經允許不得轉載,或轉載時需注明來源: 創新互聯

商城網站建設