在 Go 里用 CGO?這 7 個(gè)問題你要關(guān)注!
大家好,我是煎魚。
今天給大家分享的是 Go 諺語中的 Cgo is not Go[1],原文章同名,略有修改,原文作者是 @Dave Cheney。以下的 “我” 均指代原作者。
借用 JWZ 的一句話:有些人在面對(duì)一個(gè)問題時(shí),認(rèn)為 "我知道,我會(huì)使用 cgo(來解決這個(gè)問題)"。
類似的引言
在使用 cgo 后,他們就會(huì)遇到兩個(gè)新問題。
Cgo 是什么
Cgo 是一項(xiàng)了不起的技術(shù),它允許 Go 程序與 C 語言庫相互操作,這是一個(gè)非常有用的功能。
沒有它,Go 就不會(huì)有今天的地位。cgo 是在 Android 和 iOS 上運(yùn)行 Go 程序的關(guān)鍵。
注:甚至許多內(nèi)部用到其他底層語言的同學(xué),會(huì)使用它來做膠水。
被過度使用
我個(gè)人認(rèn)為 cgo 在 Go 項(xiàng)目中被過度使用了,當(dāng)面臨在 Go 中重新實(shí)現(xiàn)一大段 C 語言代碼時(shí),程序員會(huì)選擇使用 cgo 來包裝庫,認(rèn)為這是個(gè)更容易解決的問題。但我認(rèn)為這是一種錯(cuò)誤的選擇行為。
顯然,在某些情況下,cgo 是不可避免的,最明顯的是你必須與圖形驅(qū)動(dòng)或窗口系統(tǒng)進(jìn)行互操作,而后者只能以二進(jìn)制 blob 的形式提供。在這些場(chǎng)景下,cgo 的使用證明了它的權(quán)衡是合理的,比許多人準(zhǔn)備承認(rèn)的要少得多。
以下是一份不完整的權(quán)衡清單,當(dāng)你把 Go 項(xiàng)目建立在 cgo 庫上時(shí),你可能沒有意識(shí)到這些權(quán)衡。
你需要對(duì)此進(jìn)行思考。
構(gòu)建時(shí)間變長(zhǎng)
當(dāng)你在 Go 包中導(dǎo)入 "C" 時(shí),go build 需要做更多的工作來構(gòu)建你的代碼。
構(gòu)建你的包不再是簡(jiǎn)單地將范圍內(nèi)的所有 .go 文件的列表傳遞給 go 工具編譯的一次調(diào)用,而是包含以下工作項(xiàng):
- 需要調(diào)用 cgo 工具來生成 C 到 Go 和 Go 到 C 的相關(guān)代碼。
- 系統(tǒng)中的 C 編譯器會(huì)為軟件包中的每個(gè) C 文件進(jìn)行調(diào)用處理。
- 各個(gè)編譯單元被合并到一個(gè) .o 文件中。
- 生成的 .o 文件會(huì)通過系統(tǒng)的鏈接器,對(duì)其引用的共享對(duì)象進(jìn)行修正。
所有這些工作在你每次編譯或測(cè)試你的軟件包時(shí)都會(huì)發(fā)生,如果你在該軟件包中積極工作的話,這種情況是經(jīng)常發(fā)生的。
Go 工具會(huì)在可能的情況下將這些工作并行化(包括對(duì)所有的 C 代碼進(jìn)行全面重建),軟件包的編譯時(shí)間將會(huì)增加,并會(huì)隨之增大而增大。
你還需要在各大平臺(tái)上調(diào)試你的 C 語言代碼,以避免由于兼容性導(dǎo)致的編譯失敗。
復(fù)雜的構(gòu)建
Go 的目標(biāo)之一是產(chǎn)生一種語言,它的構(gòu)建過程是自我描述的;你的程序的源代碼包含了足夠的信息,可以讓一個(gè)工具來構(gòu)建這個(gè)項(xiàng)目。這并不是說使用 Makefile 來自動(dòng)構(gòu)建工作流程是不好的,但是在 cgo 被引入項(xiàng)目之前,除了 go 工具之外,你可能不需要任何東西來構(gòu)建和測(cè)試。
在引入了 cgo 之后,你需要設(shè)置所有的環(huán)境變量,跟蹤可能安裝在奇怪地方的共享對(duì)象和頭文件。
另外需要注意,Go 支持許多的平臺(tái),而 cgo 并不是。所以你必須花一些時(shí)間來為你的 Windows 用戶想出一個(gè)解決方案。
現(xiàn)在你的用戶必須安裝 C 編譯器,而不僅僅是 Go 編譯器。他們還必須安裝你的項(xiàng)目所依賴的 C 語言庫,你也要承擔(dān)這個(gè)技術(shù)支持的成本。
交叉匯編被拋在窗外
Go 對(duì)交叉編譯的支持是同類中最好的。從 Go 1.5 開始,你可以通過 Go 項(xiàng)目網(wǎng)站上的官方安裝程序支持從任何平臺(tái)交叉編譯到任何其他平臺(tái)。
在默認(rèn)情況下,交叉編譯時(shí) cgo 被禁用。通常情況下,如果你的項(xiàng)目是純粹的 Go,這不是一個(gè)問題。
當(dāng)你混入對(duì) C 庫的依賴時(shí),你要么放棄交叉編譯你的因那個(gè)也,要么你必須投入時(shí)間為所有目標(biāo)尋找和維護(hù)交叉編譯的 C 工具鏈,才能實(shí)現(xiàn)交叉編譯。
Go 支持的平臺(tái)數(shù)量在不斷增加。Go 1.5 增加了對(duì) 64 位 ARM 和 PowerPC 的支持。Go 1.6 增加了對(duì) 64 位 MIPS 的支持,而 IBM 的 s390 架構(gòu)被吹捧為 Go 1.7。RISC-V 正在開發(fā)中。
如果你的產(chǎn)品依賴于 C 語言庫,你不僅有上述交叉編譯的所有問題,你還必須確保你所依賴的 C 語言代碼在 Go 支持的新平臺(tái)上可靠地工作 -- 而且你必須在 C/Go 混合語言為你提供的有限調(diào)試能力的情況下做到這一點(diǎn)。
你失去了對(duì)所有工具的訪問權(quán)
Go 有很好的工具;我們有 race detector、用于分析代碼的 pprof、覆蓋率、模糊測(cè)試和源代碼分析工具。但這些工具都不能在 cgo 中起到作用(也就是沒法排查)。
相反,像 valgrind 這樣優(yōu)秀的工具并不了解 Go 的調(diào)用約定或堆棧布局。在這一點(diǎn)上,Ian Lance Taylor 的工作是整合 clang 的內(nèi)存凈化器來調(diào)試 C 端的懸空指針,這對(duì) Go 1.6 中的 cgo 用戶有好處。
將 Go 代碼和 C 代碼結(jié)合起來的結(jié)果是兩個(gè)世界的交叉點(diǎn),而不是結(jié)合點(diǎn);C 的內(nèi)存安全和 Go 程序的調(diào)試性。但失去了許多核心工具的使用空間。
性能將始終是一個(gè)問題
C 代碼和 Go 代碼生活在兩個(gè)不同的世界里,cgo 穿越了它們之間的邊界,這種轉(zhuǎn)換不是免費(fèi)的。而且取決于它在你的代碼中存在的位置,其成本可能是無關(guān)緊要的,也可能是巨大的。
?C 對(duì) Go 的調(diào)用慣例或可增長(zhǎng)的堆棧一無所知,所以對(duì) C 代碼的調(diào)用必須記錄 goroutine 堆棧的所有細(xì)節(jié),切換到 C 堆棧,并運(yùn)行 C 代碼,而 C 代碼對(duì)它是如何被調(diào)用的,或負(fù)責(zé)程序的更大的 Go 運(yùn)行時(shí)一無所知。
公平地說,Go 對(duì) C 的世界也一無所知。這就是為什么隨著時(shí)間的推移,兩者之間的數(shù)據(jù)傳遞規(guī)則變得越來越繁瑣,因?yàn)榫幾g器越來越善于發(fā)現(xiàn)不再被認(rèn)為是有效的堆棧數(shù)據(jù),而垃圾回收器也越來越善于對(duì)堆進(jìn)行同樣的處理。
如果在 C 語言世界中出現(xiàn)故障,Go 代碼必須恢復(fù)足夠的狀態(tài),至少要打印出堆棧跟蹤并干凈地退出程序,而不是把核心文件的信息都暴露出來。
管理這種跨調(diào)用堆棧的過渡,尤其是涉及到信號(hào)、線程和回調(diào)的情況下,是不容易的(Ian Lance Taylor 在 Go 1.6 中也做了大量的工作來改善信號(hào)處理與 C 的互操作性)。
歸根結(jié)底,C 語言和 Go 語言之間的轉(zhuǎn)換是不容易的,互相對(duì)對(duì)方都一戶無知,會(huì)有明顯的性能開銷。
C 語言發(fā)號(hào)施令,而不是你的代碼
你用哪種語言編寫綁定或包裝 C 代碼并不重要;Python、使用 JNI 的 Java、使用 libFFI 的一些語言,或者通過 cgo 的 Go;這是 C 的世界,你只是生活在其中。
Go 代碼和 C 代碼必須就如何共享地址空間、信號(hào)處理程序和線程 TLS 槽等資源達(dá)成一致 -- 我說的一致,實(shí)際上是指 Go 必須圍繞 C 代碼的假設(shè)開展工作。C 代碼可以假設(shè)它總是在一個(gè)線程上運(yùn)行,或者根本沒有準(zhǔn)備好在多線程環(huán)境下工作。
你不是在寫一個(gè)使用 C 庫的邏輯的 Go 程序,是在寫一個(gè)必須與互不可控的 C 代碼共存的 Go 程序,這個(gè) C 代碼很難被取代,在談判中占上風(fēng),而且不關(guān)心你的問題。
部署變得更加復(fù)雜
任何對(duì)普通觀眾的 Go 演講都會(huì)包含至少一張帶有這些文字的幻燈片:Single, static binary(單一的、靜態(tài)的二進(jìn)制)。
這是 Go 的一張王牌,使其成為遠(yuǎn)離虛擬機(jī)和運(yùn)行時(shí)管理的典型代表。使用 cgo,你就放棄了這一點(diǎn),放棄了 Go 的優(yōu)勢(shì)區(qū)域。
根據(jù)你的環(huán)境,你可能會(huì)把你的 Go 項(xiàng)目編譯成 deb 或 rpm,并且假設(shè)你的其他依賴項(xiàng)也被打包了,把它們作為安裝依賴項(xiàng)加入,把問題推給操作系統(tǒng)的軟件包管理器。但這對(duì)以前像 go build && scp 那樣直接的構(gòu)建和部署過程來說,是有幾個(gè)重大的變化。
完全靜態(tài)地編譯 Go 程序是可能的,但這絕不是簡(jiǎn)單的,這表明在項(xiàng)目中加入 cgo 的影響會(huì)波及整個(gè)構(gòu)建和部署的生命周期。
明智的選擇
說白了,我并不是說你不應(yīng)該使用 cgo。但是在你做這個(gè)設(shè)計(jì)前,請(qǐng)仔細(xì)考慮你將會(huì)放棄的 Go 的許多品質(zhì)。
需要考慮清楚得失,再思考是否值得你這么去做。
參考資料
[1]Cgo is not Go: https://dave.cheney.net/2016/01/18/cgo-is-not-go???