本文目錄一覽:
Go:互斥體和飢餓
在Golang中進行開發時,互斥鎖在不斷嘗試獲取永遠無法獲取的鎖時會遇到 飢餓 問題。在本文中,我們將探討影響Go 1.8的飢餓問題,該問題已在Go 1.9中解決。
為了說明互斥鎖的飢餓狀況,我將以 拉斯·考克斯 ( Russ Cox)提出的 關於他們討論互斥鎖改進的 問題 為例:
starvation.go
此示例基於兩個goroutine:
兩者都具有100微秒的周期,但是由於goroutine 1一直在請求鎖定,因此可以預期它將更頻繁地獲得鎖定。
這是一個用Go 1.8進行的示例,該示例具有10次迭代的循環的鎖分配:
該互斥鎖已被第二個goroutine捕獲了十次,而第一個則超過了700萬次。讓我們分析一下這裡發生了什麼。
首先,goroutine 1將獲得鎖定並睡眠100微秒。當goroutine 2嘗試獲取鎖時,它將被添加到鎖的隊列(FIFO順序)中,並且goroutine將進入等待狀態:
Figure 1 — lock acquisition
然後,當goroutine 1完成工作時,它將釋放鎖定。此版本將通知隊列喚醒goroutine 2。Goroutine 2將被標記為可運行,並正在等待Go Scheduler在線程上運行:
Figure 2— goroutine 2 is awoke
但是,在goroutine 2等待運行時,goroutine 1將再次請求鎖定:
Figure 3— goroutine 2 is waiting to run
當goroutine 2嘗試獲取鎖時,它將看到它已經具有保持狀態並進入等待模式,如圖2所示:
Figure 4— goroutine 2 tries again to get the lock
goroutine 2對鎖的獲取將取決於它在線程上運行所花費的時間。
現在已經確定了問題,讓我們回顧可能的解決方案。
處理互斥量的方法有很多,例如:
barging mode
Go 1.8就是這樣設計的,它反映了我們之前看到的內容。
handoff mode
我們可以 在Linux內核 的 互斥體中 找到此邏輯:
在我們的情況下,互斥鎖切換會完美平衡兩個goroutine之間的鎖分配,但是會降低性能,因為這將迫使第一個goroutine即使未持有也要等待鎖。
spinning mode
Go 1.8也使用此策略。當試圖獲取已經持有的鎖時,如果本地隊列為空且處理器數量大於一,則goroutine將旋轉幾次-如果僅使用一個處理器旋轉就會阻塞程序。旋轉後,goroutine將停放。如果程序大量使用鎖,它可以作為快速路徑。
有關如何設計鎖的更多信息( 插入 ,越區切換,自旋鎖),通常, Filip Pizlo撰寫 了必讀的文章「 WebKit中的鎖定 」。
在Go 1.9之前,Go結合了插入和旋轉模式。在1.9版中,Go通過添加新的飢餓模式解決了先前解釋的問題,該模式將導致在解鎖模式期間進行切換。
所有等待鎖定時間超過一毫秒的goroutine,也稱為 有界等待 ,將被標記為飢餓。當標記為飢餓時,解鎖方法現在將把鎖直接移交給第一位服務員。這是工作流程:
starvation mode
由於進入的goroutine將不會獲取任何為下一個服務員保留的鎖,因此在飢餓模式下也將禁用旋轉。
讓我們使用Go 1.9和新的starvation模式運行前面的示例:
現在的結果更加公平。現在,我們想知道新的控制層是否會對互斥體不處於飢餓狀態的其他情況產生影響。正如我們在該程序包的基準測試(Go 1.8與Go 1.9)中所看到的,在其他情況下,性能並沒有下降(不同處理器數量下, 性能會略有變化 ):
翻譯自:
駁狗屎文 “我為什麼放棄Go語言
此篇文章流傳甚廣, 其實裡面沒啥乾貨, 而且裡面很多觀點是有問題的. 這個文章在 golang-china 很早就討論過了.
最近因為 Rust 1.0 和 1.1 的發布, 導致這個文章又出來毒害讀者.
所以寫了這篇反駁文章, 指出其中的問題.
有好幾次,當我想起來的時候,總是會問自己:我為什麼要放棄Go語言?這個決定是正確的嗎?是明智和理性的嗎?其實我一直在認真思考這個問題。
開門見山地說,我當初放棄Go語言(golang),就是因為兩個「不爽」:第一,對Go語言本身不爽;第二,對Go語言社區里的某些人不爽。毫無疑問,這是非常主觀的結論。但是我有足夠詳實的客觀的論據,用以支撐這個看似主觀的結論。
文末附有本文更新日誌。
確實是非常主觀的結論, 因為裡面有不少有問題的觀點(用來忽悠Go小白還行).
第0節:我的Go語言經歷
先說說我的經歷吧,以避免被無緣無故地當作Go語言的低級黑。
2009年底,Go語言(golang)第一個公開版本發布,籠罩著「Google公司製造」的光環,吸引了許多慕名而來的嘗鮮者,我(Liigo)也身居其中,籠統的看了一些Go語言的資料,學習了基礎的教程,因對其語法中的分號和花括弧不滿,很快就遺忘掉了,沒拿它當一回事。
在2009年Go剛發布時, 確實是因為「Google公司製造」的光環而吸引了(包括文章作者和諸多IT記者)很多低級的嘗鮮者.
還好, 經過5年的發展, 這些純粹因為光環來的投機者所剩已經不多了(Google趨勢).
目前, 真正的Go用戶早就將Go用於實際的生產了.
說到 其語法中的分號和花括弧不滿, 我想說這只是你的 個人主觀感受, 還有很多人對Go的分號和花括弧很滿意,
包括水果公司的的 Swift 的語言設計者也很滿意這種風格(Swift中的分號和花括弧和Go基本相同).
如果只談 個人主觀感受, 我也可以說 Rust 的 fn 縮寫也很蛋疼!
兩年之後,2011年底,Go語言發布1.0的計劃被提上日程,相關的報道又多起來,我再次關注它,重新評估之後決定深入參與Go語言。我訂閱了其users、nuts、dev、commits等官方郵件組,堅持每天閱讀其中的電子郵件,以及開發者提交的每一次源代碼更新,給Go提交了許多改進意見,甚至包括修改Go語言編譯器源代碼直接參与開發任務。如此持續了數月時間。
這個到是事實, 在 golang-china 有不少吵架的帖子, 感興趣的可以去挖下, 我就不展開說了.
到2012年初,Go 1.0發布,語言和標準庫都已經基本定型,不可能再有大幅改進,我對Go語言未能在1.0定型之前更上一個台階、實現自我突破,甚至帶著諸多明顯缺陷走向1.0,感到非常失望,因而逐漸疏遠了它(所以Go 1.0之後的事情我很少關心)。後來看到即將發布的Go 1.1的Release Note,發現語言層面沒有太大改變,只是在庫和工具層面有所修補和改進,感到它尚在幼年就失去成長的動力,越發失望。外加Go語言社區里的某些人,其中也包括Google公司負責開發Go語言的某些人,其態度、言行,讓我極度厭惡,促使我決絕地離棄Go語言。
真的不清楚樓主說的可以在 Go1.0 之前短時間內能實現的 重大改進和諸多明顯缺陷 是什麼.
如果是樓主說前面的 其語法中的分號和花括弧不滿 之類的重大改進, 我只能說這只是你的 個人主觀感受 而已,
你的很多想法只能說服你自己, 沒辦法說服其他絕大部分人(不要以為像C++或Rust那樣什麼特性都有就NB了, 各種NB特性加到一起只能是 要你命3000, 而絕對不會是什麼 銀彈).
Go 1.1的Release Note,發現語言層面沒有太大改變. 語言層沒有改變是是因為 Go1 作出的向後兼容的承諾. 對於工業級的語言來說, Go1 這個只能是優點. 如果連語言層在每個版本都會出現諸多大幅改進, 那誰還敢用Go語言來做生產開發呢(我承認Rust的改動很大膽, 但也說明了Rust還處於比較幼稚和任性的階段)?
說 Go語言社區里的某些人固執 的觀點我是同意的. 但是這些 固執 的人是可以講道理的, 但是他們對很多東西的要求很高(特別是關於Go的設計哲學部分).
只要你給的建議有依據(語言的設計哲學是另外一回事情), 他們絕對不會盲目的拒絕(只是討論的周期會比較長).
關於樓主提交的給Go文件添加BOM的文章, 需要補充說明下.
在Go1.0發布的時候, Go語言的源文件(.go)明確要求必須是UTF8編碼的, 而且是無BOM的UTF8編碼的.
注意: 這個 無BOM的UTF8編碼 的限制僅僅是 針對 Go語言的源文件(.go).
這個限制並不是說不允許用戶處理帶BOM的UTF8的txt文件!
我覺得對於寫Go程序來說, 這個限制是沒有任何問題的, 到目前為止, 我還從來沒有使用過帶BOM的.go文件.
不僅是因為帶BOM的.go文件沒有太多的意義, 而且有很多的缺陷.
BOM的原意是用來表示編碼是大端還是小端的, 主要用於UTF16和UTF32. 對於 UTF8 來說, BOM 沒有任何存在的意義(正是Go的2個作者發明了UTF8, 徹底解決了全球的編碼問題).
但是, 在現實中, 因為MS的txt記事本, 對於中文環境會將txt(甚至是C/C++源文件)當作GBK編碼(GBK是個爛編碼),
為了區別到底是GBK還是UTF8, MS的記事本在前面加了BOM這個垃圾(被GBK佔了茅坑), 這裡的bom已經不是表示位元組序本意了. 不知道有沒有人用ms的記事本寫網頁, 然後生成一個帶bom的utf8網頁肯定很有意思.
這是MS的記事本的BUG: 它不支持生成無BOM的UTF8編碼的文本文件!
這些是現實存在的帶BOM的UTF8編碼的文本文件, 但是它們肯定都不是Go語言源文件!
所以說, Go語言的源文件即使強制限制了無BOM的UTF8編碼要求, 也是沒有任何問題的(而且我還希望有這個限制).
雖然後來Go源文件接受帶BOM的UTF8了, 但是運行 go fmt 之後, 還是會刪除掉BOM的(因為BOM就是然並卵). 也就是說 帶 BOM 的 Go 源文件是不符合 Go語言的編碼風格的, go fmt 會強制刪除 BOM 頭.
前面說了BOM是MS帶來的垃圾, 但是BOM的UTF8除瞭然並卵之外還有很多問題, 因為BOM在string的開頭嵌入了垃圾,
導致正則表達式, string的鏈接運算等操作都被會被BOM這個垃圾所污染. 對於.go語言, 即使代碼完全一樣, 有BOM和無BOM會導致文件的MD5之類的校驗碼不同.
所以, 我覺得Go用戶不用糾結BOM這個無關緊要的東西.
在上一個10年,我(Liigo)在我所屬的公司里,深度參與了兩個編程語言項目的開發。我想,對於如何判斷某個編程語言的優劣,或者說至少對於如何判斷某個編程語言是否適合於我自己,我應該還是有一點發言權的。
第1節:我為什麼對Go語言不爽?
Go語言有很多讓我不爽之處,這裡列出我現在還能記起的其中一部分,排名基本上不分先後。讀者們耐心地看完之後,還能淡定地說一句「我不在乎」嗎?
1.1 不允許左花括弧另起一行
關於對花括弧的擺放,在C語言、C++、Java、C#等社區中,十餘年來存在持續爭議,從未形成一致意見。在我看來,這本來就是主觀傾向很重的抉擇,不違反原則不涉及是非的情況下,不應該搞一刀切,讓程序員或團隊自己選擇就足夠了。編程語言本身強行限制,把自己的喜好強加給別人,得不償失。無論傾向於其中任意一種,必然得罪與其對立的一群人。雖然我現在已經習慣了把左花括弧放在行尾,但一想到被禁止其他選擇,就感到十分不爽。Go語言這這個問題上,沒有做到「團結一切可以團結的力量」不說,還有意給自己樹敵,太失敗了。
我覺得Go最偉大的發明是 go fmt, 從此Go用戶不會再有花括弧的位置這種無聊爭論了(當然也少了不少灌水和上tiobe排名的機會).
是這優點, Swift 語言也使用和 Go 類似的風格(當然樓主也可能鄙視swift的作者).
1.2 編譯器莫名其妙地給行尾加上分號
對Go語言本身而言,行尾的分號是可以省略的。但是在其編譯器(gc)的實現中,為了方便編譯器開發者,卻在詞法分析階段強行添加了行尾的分號,反過來又影響到語言規範,對「怎樣添加分號」做出特殊規定。這種變態做法前無古人。在左花括弧被意外放到下一行行首的情況下,它自動在上一行行尾添加的分號,會導致莫名其妙的編譯錯誤(Go 1.0之前),連它自己都解釋不明白。如果實在處理不好分號,乾脆不要省略分號得了;或者,Scala和JavaScript的編譯器是開源的,跟它們學學怎麼處理省略行尾分號可以嗎?
又是樓主的 個人主觀感受, 不過我很喜歡這個特性. Swift 語言也是類似.
1.3 極度強調編譯速度,不惜放棄本應提供的功能
程序員是人不是神,編碼過程中免不了因為大意或疏忽犯一些錯。其中有一些,是大家集體性的很容易就中招的錯誤(Go語言里的例子我暫時想不起來,C++里的例子有「基類析構函數不是虛函數」)。這時候編譯器應該站出來,多做一些檢查、約束、核對性工作,盡量阻止常規錯誤的發生,盡量不讓有潛在錯誤的代碼編譯通過,必要時給出一些警告或提示,讓程序員留意。編譯器不就是機器么,不就是應該多做臟活累活雜活、減少人的心智負擔么?編譯器多做一項檢查,可能會避免數十萬程序員今後多年內無數次犯同樣的錯誤,節省的時間不計其數,這是功德無量的好事。但是Go編譯器的作者們可不這麼想,他們不願意自己多花幾個小時給編譯器增加新功能,覺得那是虧本,反而減慢了編譯速度。他們以影響編譯速度為由,拒絕了很多對編譯器改進的要求。典型的因噎廢食。強調編譯速度固然值得讚賞,但如果因此放棄應有的功能,我不贊成。
編譯速度是很重要的, 如果編譯速度夠慢, 語言再好也不會有人使用的.
比如C/C++的增量編譯/預編譯頭文件/並發編譯都是為了提高編譯速度.
Rust1.1 也號稱 比 1.0 的編譯時間減少了32% (注意: 不是運行速度).
當然, Go剛面世的時候, 編譯速度是其中的一個設計目標.
不過我想樓主, 可能想說的是因為編譯器自己添加分號而導致的編譯錯誤的問題.
我覺得Go中 { 不能另起一行是語言特性, 如果修復這個就是引入了新的錯誤.
其他的我真想不起來還有哪些 調編譯速度,不惜放棄本應提供的功能 (不要提泛型, 那是因為還沒有好的設計).
1.4 錯誤處理機制太原始
在Go語言中處理錯誤的基本模式是:函數通常返回多個值,其中最後一個值是error類型,用於表示錯誤類型極其描述;調用者每次調用完一個函數,都需要檢查這個error並進行相應的錯誤處理:if err != nil { /*這種代碼寫多了不想吐么*/ }。此模式跟C語言那種很原始的錯誤處理相比如出一轍,並無實質性改進。實際應用中很容易形成多層嵌套的if else語句,可以想一想這個編碼場景:先判斷文件是否存在,如果存在則打開文件,如果打開成功則讀取文件,如果讀取成功再寫入一段數據,最後關閉文件,別忘了還要處理每一步驟中出現錯誤的情況,這代碼寫出來得有多變態、多醜陋?實踐中普遍的做法是,判斷操作出錯後提前return,以避免多層花括弧嵌套,但這麼做的後果是,許多錯誤處理代碼被放在前面突出的位置,常規的處理邏輯反而被掩埋到後面去了,代碼可讀性極差。而且,error對象的標準介面只能返回一個錯誤文本,有時候調用者為了區分不同的錯誤類型,甚至需要解析該文本。除此之外,你只能手工強制轉換error類型到特定子類型(靜態類型的優勢沒了)。至於panic – recover機制,致命的缺陷是不能跨越庫的邊界使用,註定是一個半成品,最多只能在自己的pkg裡面玩一玩。Java的異常處理雖然也有自身的問題(比如Checked Exceptions),但總體上還是比Go的錯誤處理高明很多。
話說, 軟體開發都發展了半個世紀, 還是無實質性改進. 不要以為弄一個異常的語法糖就是革命了.
我只能說錯誤和異常是2個不同的東西, 將所有錯誤當作異常那是SB行為.
正因為有異常這個所謂的銀彈, 導致很多等著別人幫忙擦屁股的行為(注意 shit 函數拋出的絕對不會是一種類型的 shit, 而被其間接調用的各種 xxx_shit 也可能拋出各種類型的異常, 這就導致 catch 失控了):
int main() {
try {
shit();
} catch( /* 到底有幾千種 shit ? */) {
…
}
}
Go的建議是 panic – recover 不跨越邊界, 也就是要求正常的錯誤要由pkg的處理掉.
這是負責任的行為.
再說Go是面向並發的編程語言, 在海量的 goroutine 中使用 try/catch 是不是有一種不倫不類的感覺呢?
1.5 垃圾回收器(GC)不完善、有重大缺陷
在Go 1.0前夕,其垃圾回收器在32位環境下有內存泄漏,一直拖著不肯改進,這且不說。Go語言垃圾回收器真正致命的缺陷是,會導致整個進程不可預知的間歇性停頓。像某些大型後台服務程序,如遊戲伺服器、APP容器等,由於佔用內存巨大,其內存對象數量極多,GC完成一次回收周期,可能需要數秒甚至更長時間,這段時間內,整個服務進程是阻塞的、停頓的,在外界看來就是服務中斷、無響應,再牛逼的並發機制到了這裡統統失效。垃圾回收器定期啟動,每次啟動就導致短暫的服務中斷,這樣下去,還有人敢用嗎?這可是後台伺服器進程,是Go語言的重點應用領域。以上現象可不是我假設出來的,而是事實存在的現實問題,受其嚴重困擾的也不是一家兩家了(2013年底ECUG Con 2013,京東的劉奇提到了Go語言的GC、defer、標準庫實現是性能殺手,最大的痛苦是GC;美團的沈鋒也提到Go語言的GC導致後台服務間隔性停頓是最大的問題。更早的網路遊戲仙俠道開發團隊也曾受Go垃圾回收的沉重打擊)。在實踐中,你必須努力減少進程中的對象數量,以便把GC導致的間歇性停頓控制在可接受範圍內。除此之外你別無選擇(難道你還想自己更換GC演算法、甚至砍掉GC?那還是Go語言嗎?)。跳出圈外,我近期一直在思考,一定需要垃圾回收器嗎?沒有垃圾回收器就一定是歷史的倒退嗎?(可能會新寫一篇博客文章專題探討。)
這是說的是32位系統, 這絕對不是Go語言的重點應用領域!! 我可以說Go出生就是面向64位系統和多核心CPU環境設計的. (再說 Rust 目前好像還不支持 XP 吧, 這可不可以算是影響巨大?)
32位當時是有問題, 但是對實際生產影響並不大(請問樓主還是在用32位系統嗎, 還只安裝4GB的內存嗎). 如果是8位單片機環境, 建議就不要用Go語言了, 直接C語言好了.
而且這個問題早就不存在了(大家可以去看Go的發布日誌).
Go的出生也就5年時間, GC的完善和改進是一個持續的工作, 2015年8月將發布的 Go1.5將採用並行GC.
關於GC的被人詬病的地方是會導致卡頓, 但是我以為這個主要是因為GC的實現還不夠完美而導致的.
如果是完美的並發和增量的GC, 那應該不會出現大的卡頓問題的.
當然, 如果非要實時性, 那用C好了(實時並不表示性能高, 只是響應時間可控).
對於Rust之類沒有GC的語言來說, 想很方便的開發並發的後台程序那幾乎是不可能的.
不要總是吹Rust能代替底層/中層/上層的開發, 我們要看有誰用Rust真的做了什麼.
1.6 禁止未使用變數和多餘import
Go編譯器不允許存在被未被使用的變數和多餘的import,如果存在,必然導致編譯錯誤。但是現實情況是,在代碼編寫、重構、調試過程中,例如,臨時性的注釋掉一行代碼,很容易就會導致同時出現未使用的變數和多餘的import,直接編譯錯誤了,你必須相應的把變數定義注釋掉,再翻頁回到文件首部把多餘的import也注釋掉,……等事情辦完了,想把剛才注釋的代碼找回來,又要好幾個麻煩的步驟。還有一個讓人蛋疼的問題,編寫資料庫相關的代碼時,如果你import某資料庫驅動的pkg,它編譯給你報錯,說不需要import這個未被使用的pkg;但如果你聽信編譯器的話刪掉該import,編譯是通過了,運行時必然報錯,說找不到資料庫驅動;你看看程序員被折騰的兩邊不是人,最後不得不請出大神:import _。對待這種問題,一個比較好的解決方案是,視其為編譯警告而非編譯錯誤。但是Go語言開發者很固執,不容許這種折中方案。
這個問題我只能說樓主的吐槽真的是沒水平.
為何不使用的是錯誤而不是警告? 這是為了將低級的bug消滅在編譯階段(大家可以想下C/C++的那麼多警告有什麼卵用).
而且, import 即使沒有使用的話, 也是用副作用的, 因為 import 會導致 init 和全局變數的初始化.
如果某些代碼沒有使用, 為何要執行 init 這些初始化呢?
如果是因為調試而添加的變數, 那麼調試完刪除不是很正常的要求嗎?
如果是因為調試而要導入fmt或log之類的包, 刪除調試代碼後又導致 import 錯誤的花,
樓主難道不知道在一個獨立的文件包裝下類似的輔助調試的函數嗎?
import (
“fmt”
“log”
)
func logf(format string, a …interface{}) {
file, line := callerFileLine()
fmt.Fprintf(os.Stderr, “%s:%d: “, file, line)
fmt.Fprintf(os.Stderr, format, a…)
}
func fatalf(format string, a …interface{}) {
file, line := callerFileLine()
fmt.Fprintf(os.Stderr, “%s:%d: “, file, line)
fmt.Fprintf(os.Stderr, format, a…)
os.Exit(1)
}
import _ 是有明確行為的用法, 就是為了執行包中的 init 等函數(可以做某些註冊操作).
將警告當作錯誤是Go的一個哲學, 當然在樓主看來這是白痴做法.
1.7 創建對象的方式太多令人糾結
創建對象的方式,調用new函數、調用make函數、調用New方法、使用花括弧語法直接初始化結構體,你選哪一種?不好選擇,因為沒有一個固定的模式。從實踐中看,如果要創建一個語言內置類型(如channel、map)的對象,通常用make函數創建;如果要創建標準庫或第三方庫定義的類型的對象,首先要去文檔里找一下有沒有New方法,如果有就最好調用New方法創建對象,如果沒有New方法,則退而求其次,用初始化結構體的方式創建其對象。這個過程頗為周折,不像C++、Java、C#那樣直接new就行了。
C++的new是狗屎. new導致的問題是構造函數和普通函數的行為不一致, 這個補丁特性真的沒啥優越的.
我還是喜歡C語言的 fopen 和 malloc 之類構造函數, 構造函數就是普通函數, Go語言中也是這樣.
C++中, 除了構造不兼容普通函數, 析構函數也是不兼容普通函數. 這個而引入的坑有很多吧.
1.8 對象沒有構造函數和析構函數
沒有構造函數還好說,畢竟還有自定義的New方法,大致也算是構造函數了。沒有析構函數就比較難受了,沒法實現RAII。額外的人工處理資源清理工作,無疑加重了程序員的心智負擔。沒人性啊,還嫌我們程序員加班還少嗎?C++里有析構函數,Java里雖然沒有析構函數但是有人家finally語句啊,Go呢,什麼都沒有。沒錯,你有個defer,可是那個defer問題更大,詳見下文吧。
defer 可以覆蓋析構函數的行為, 當然 defer 還有其他的任務. Swift2.0 也引入了一個簡化版的 defer 特性.
1.9 defer語句的語義設定不甚合理
Go語言設計defer語句的出發點是好的,把釋放資源的「代碼」放在靠近創建資源的地方,但把釋放資源的「動作」推遲(defer)到函數返回前執行。遺憾的是其執行時機的設置似乎有些不甚合理。設想有一個需要長期運行的函數,其中有無限循環語句,在循環體內不斷的創建資源(或分配內存),並用defer語句確保釋放。由於函數一直運行沒有返回,所有defer語句都得不到執行,循環過程中創建的大量短暫性資源一直積累著,得不到回收。而且,系統為了存儲defer列表還要額外佔用資源,也是持續增加的。這樣下去,過不了多久,整個系統就要因為資源耗盡而崩潰。像這類長期運行的函數,http.ListenAndServe()就是典型的例子。在Go語言重點應用領域,可以說幾乎每一個後台服務程序都必然有這麼一類函數,往往還都是程序的核心部分。如果程序員不小心在這些函數中使用了defer語句,可以說後患無窮。如果語言設計者把defer的語義設定為在所屬代碼塊結束時(而非函數返回時)執行,是不是更好一點呢?可是Go 1.0早已發布定型,為了保持向後兼容性,已經不可能改變了。小心使用defer語句!一不小心就中招。
前面說到 defer 還有其他的任務, 也就是 defer 中執行的 recover 可以捕獲 panic 拋出的異常.
還有 defer 可以在 return 之後修改命名的返回值.
上面2個工作要求 defer 只能在函數退出時來執行.
樓主說的 defer 是類似 Swift2.0 中 defer 的行為, 但是 Swift2.0 中 defer 是沒有前面2個特性的.
Go中的defer是以函數作用域作為觸發的條件的, 是會導致樓主說的在 for 中執行的錯誤用法(哪個語言沒有坑呢?).
不過 for 中 局部 defer 也是有辦法的 (Go中的defer是以函數作用域):
for {
func(){
f, err := os.Open(…)
defer f.Close()
}()
}
在 for 中做一個閉包函數就可以了. 自己不會用不要怪別人沒告訴你.
1.10 許多語言內置設施不支持用戶定義的類型
for in、make、range、channel、map等都僅支持語言內置類型,不支持用戶定義的類型(?)。用戶定義的類型沒法支持for in循環,用戶不能編寫像make、range那樣「參數類型和個數」甚至「返回值類型和個數」都可變的函數,不能編寫像channel、map那樣類似泛型的數據類型。語言內置的那些東西,處處充斥著斧鑿的痕迹。這體現了語言設計的局限性、封閉性、不完善,可擴展性差,像是新手作品——且不論其設計者和實現者如何權威。延伸閱讀:Go語言是30年前的陳舊設計思想,用戶定義的東西幾乎都是二等公民(Tikhon Jelvis)。
說到底, 這個是因為對泛型支持的不完備導致的.
Go語言是沒啥NB的特性, 但是Go的特性和工具組合在一起就是好用.
這就是Go語言NB的地方.
1.11 沒有泛型支持,常見數據類型介面醜陋
沒有泛型的話,List、Set、Tree這些常見的基礎性數據類型的介面就只能很醜陋:放進去的對象是一個具體的類型,取出來之後成了無類型的interface{}(可以視為所有類型的基礎類型),還得強制類型轉換之後才能繼續使用,令人無語。Go語言缺少min、max這類函數,求數值絕對值的函數abs只接收/返回雙精度小數類型,排序介面只能藉助sort.Interface無奈的迴避了被比較對象的類型,等等等等,都是沒有泛型導致的結果。沒有泛型,介面很難優雅起來。Go開發者沒有明確拒絕泛型,只是說還沒有找到很好的方法實現泛型(能不能學學已經開源的語言呀)。現實是,Go 1.0已經定型,泛型還沒有,那些醜陋的介面為了保持向後兼容必須長期存在著。
Go有自己的哲學, 如果能有和目前哲學不衝突的泛型實現, 他們是不會反對的.
如果只是簡單學學(或者叫抄襲)已經開源的語言的語法, 那是C++的設計風格(或者說C++從來都是這樣設計的, 有什麼特性就抄什麼), 導致了各種腦裂的編程風格.
編譯時泛型和運行時泛型可能是無法完全兼容的, 看這個例子:
type AdderT interface {
Add(a, b T) T
}
如何在”特殊”的網路環境下編譯 Docker
由於 Docker 編譯需要依賴於 Docker Daemon ,所以只能在 64 位的 Linux 環境下先安裝 Docker 程序,再從 Github 上克隆 Docker 的代碼進行編譯。
在 Docker 的目錄下執行 make 命令將默認執行 Makefile 中 make binary 指令進行編譯。
?
default: binary
all: build
$(DOCKER_RUN_DOCKER) hack/make.sh
binary: build
$(DOCKER_RUN_DOCKER) hack/make.sh binary
cross: build
$(DOCKER_RUN_DOCKER) hack/make.sh binary cross
從以上的 Makefile 可以看出,執行 make、make binary、make all 或 make cross 都可以得到可運行的 Docker 程序。
在 Mac OS 環境下使用 brew 的命令安裝 Docker ,只能得到一個 docker client 的二進位程序,如果以 daemon 的方式運行,會得到 『This is a client-only binary – running the Docker daemon is not supported.』 的錯誤提示信息。
方法 1.
使用 VirtualBox 或者 VMWare Workstation 安裝一個 Linux 的虛擬機。宿主機使用 VPN 等方案使網路「正常」訪問各種「服務」,虛擬機網卡使用 NAT 模式。在 Linux 虛擬機內使用 make 進行編譯 Docker 不會有任何網路問題。只是編譯速度受限於 VPN 等網路解決方案,有可能等待時間很長。
方法 2.
Docker 每次發布新版本,都會在 docker-dev 的鏡像倉庫發布一個新的標籤,這個鏡像倉庫包含了編譯 Docker 鏡像所依賴的所有環境,只需替換 Docker 代碼目錄下的 Dockerfile 即可實現編譯 Docker 。
?
FROM docker.cn/docker/docker-dev:v1.2.0
VOLUME /var/lib/docker
WORKDIR /go/src/github.com/docker/docker
ENV DOCKER_BUILDTAGS apparmor selinux
ENTRYPOINT [「hack/dind」]
COPY . /go/src/github.com/docker/docker
Dockerfile 中只保留必要的步驟就可以實現編譯了。
方法 3.
對 Docker 代碼中的 Docker 進行徹底的改造,用國內的各種鏡像替換其中不能在「正常」網路條件下訪問的鏡像,使得代碼能夠快速編譯通過。
?
FROM docker.cn/docker/ubuntu:14.04.1
MAINTAINER Meaglith Ma genedna@gmail.com (@genedna)
RUN echo “deb trusty main universe” /etc/apt/sources.list echo “deb-src trusty main restricted” /etc/apt/sources.list echo “deb trusty-updates main restricted” /etc/apt/sources.list echo “deb-src trusty-updates main restricted” /etc/apt/sources.list echo “deb trusty universe” /etc/apt/sources.list echo “deb-src trusty universe” /etc/apt/sources.list echo “deb trusty-updates universe” /etc/apt/sources.list echo “deb-src trusty-updates universe” /etc/apt/sources.list echo “deb trusty-security main restricted” /etc/apt/sources.list echo “deb-src trusty-security main restricted” /etc/apt/sources.list echo “deb trusty-security universe” /etc/apt/sources.list echo “deb-src trusty-security universe” /etc/apt/sources.list
RUN apt-get update apt-get install -y \
aufs-tools \
automake \
btrfs-tools \
build-essential \
curl \
dpkg-sig \
git \
iptables \
libapparmor-dev \
libcap-dev \
libsqlite3-dev \
lxc=1.0* \
mercurial \
parallel \
reprepro \
ruby1.9.1 \
ruby1.9.1-dev \
s3cmd=1.1.0* \
unzip \
–no-install-recommends
RUN git clone –no-checkout /usr/local/lvm2 cd /usr/local/lvm2 git checkout -q v2_02_103
RUN cd /usr/local/lvm2 ./configure –enable-static_link make device-mapper make install_device-mapper
RUN curl -sSL | tar -v -C /usr/local -xz
ENV PATH /usr/local/go/bin:$PATH
ENV GOPATH /go:/go/src/github.com/docker/docker/vendor
ENV PATH /go/bin:$PATH
RUN cd /usr/local/go/src ./make.bash –no-clean 21
ENV DOCKER_CROSSPLATFORMS \
linux/386 linux/arm \
darwin/amd64 darwin/386 \
freebsd/amd64 freebsd/386 freebsd/arm
ENV GOARM 5
RUN cd /usr/local/go/src bash -xc ‘for platform in $DOCKER_CROSSPLATFORMS; do GOOS=${platform%/*} GOARCH=${platform##*/} ./make.bash –no-clean 21; done’
RUN mkdir -p /go/src/github.com/gpmgo \
cd /go/src/github.com/gpmgo \
curl -o gopm.zip \revision=dev –location \
unzip gopm.zip \
mv $(ls | grep “gopm-“) gopm \
rm gopm.zip \
cd gopm \
go install
RUN gopm bin -v code.google.com/p/go.tools/cmd/cover
RUN gem sources –remove \
gem sources -a \
gem install –no-rdoc –no-ri fpm –version 1.0.2
RUN gopm bin -v -d /go/bin github.com/cpuguy83/go-md2man@tag:v1
RUN git clone -b buildroot-2014.02 /docker-busybox
RUN /bin/echo -e ‘[default]\naccess_key=$AWS_ACCESS_KEY\nsecret_key=$AWS_SECRET_KEY’ /.s3cfg
RUN git config –global user.email ‘docker-dummy@example.com’
RUN groupadd -r docker
RUN useradd –create-home –gid docker unprivilegeduser
VOLUME /var/lib/docker
WORKDIR /go/src/github.com/docker/docker
ENV DOCKER_BUILDTAGS apparmor selinux
ENTRYPOINT [“hack/dind”]
COPY . /go/src/github.com/docker/docker
以上的命令把 Ubuntu 鏡像中的源替換為國內速度較快的阿里源;把 lvm2 鏡像到國內的 Git 託管服務 coding.net;從 七牛雲存儲 保存的 Golang 源碼進行獲取和編譯;使用 gopm 下載編譯所需要的 Library ;最後把其中 gem 源切換到淘寶。至此,可以在「特殊」的網路條件下快速編譯 Docker 。
我為什麼放棄Go語言
有好幾次,當我想起來的時候,總是會問自己:我為什麼要放棄Go語言?這個決定是正確的嗎?是明智和理性的嗎?其實我一直在認真思考這個問題。
開門見山地說,我當初放棄Go語言(golang),就是因為兩個「不爽」:第一,對Go語言本身不爽;第二,對Go語言社區里的某些人不爽。毫無疑問,這是非常主觀的結論。轉載
1.1 不允許左花括弧另起一行
1.2 編譯器莫名其妙地給行尾加上分號
1.3 極度強調編譯速度,不惜放棄本應提供的功能
1.4 錯誤處理機制太原始
1.5 垃圾回收器(GC)不完善、有重大缺陷
1.6 禁止未使用變數和多餘import
1.7 創建對象的方式太多令人糾結
1.8 對象沒有構造函數和析構函數
1.9 defer語句的語義設定不甚合理
1.10 許多語言內置設施不支持用戶定義的類型
1.11 沒有泛型支持,常見數據類型介面醜陋
1.12 實現介面不需要明確聲明
1.13 省掉小括弧卻省不掉花括弧
1.14 編譯生成的可執行文件尺寸非常大
1.15 不支持動態載入類庫
Golang 並發讀寫map安全問題詳解
下面先寫一段測試程序,然後看下運行結果:
運行結果:
發生了錯誤,提示:fatal error: concurrent map read and map write, map 發生了同時讀和寫了; 但是這個錯誤並不是每次運行都會出現,就是有的時候會出現,有的時候並不會出現,根據筆者多次運行結果(其他例子,讀者可以自己嘗試下)來看還會有另外一種報錯就是:fatal error: concurrent map writes,就是map發生了同時寫,但是只是讀是不會有問題的。關於不同的運行結果小夥伴們可以自己寫幾個例子去測試下。下面就這兩個錯誤的發生,筆者給出如下解釋:
(1) fatal error: concurrent map read and map write
就是當一個goroutine在寫數據,而同時另外一個goroutine要讀數據就會報錯,不過這個報錯也很好理解:還沒寫完就讀,讀的數據會有問題,或者反過來還沒讀完就開始寫了,同樣會導致讀取的數據有問題;
(2) fatal error: concurrent map writes
兩個goroutine 同時寫一個內存地址,這種操作也是不允許的,會導致一些比較奇怪的問題;
總體來看其實就是寫map的操作和其他的讀或者寫同時發生了,導致的報錯,做過幾年開發的人可能會想到使用鎖來解決,比如寫map某個key的時候,通過鎖來保證其他goroutine不能再對其寫或者讀了。
實現思路:
(1) 當寫map的某個key時,通過鎖來保證其他goroutine不能再對其寫或者讀了。
(2) 當讀map的某個key時,通過鎖來保證其他的goroutine不能再對其寫,但是可以讀。
於是我們馬上想到golang 的讀寫鎖貌似符合需求,下面來實現下:
再來看下運行結果:
發現沒有報錯了,並且多次運行的結果都不會報錯,說明這個方法是有用的,不過在go1.9版本後就有sync.Map了,不過這個適用場景是讀多寫少的場景,如果寫很多的話效率比較差,具體的原因在這裡筆者就不介紹了,後面會寫篇文章詳細介紹下。
今天的文章就到這裡了,如果有不對的地方歡迎小夥伴給我留言,看到會即時回復的。
原創文章,作者:小藍,如若轉載,請註明出處:https://www.506064.com/zh-tw/n/193579.html