原文:《Choosing A Library to Embed Static Assets in Go》
作者:Tim Shannon
譯者按:文章介紹了 Go 嵌入靜態檔案的幾種方法,接著講自己在使用某一個庫期間遇到的麻煩,最後比較不同庫的特點。對於想要將靜態資源構建到可執行檔裡的開發人員來說,有一定參考意義。在徵得原文作者同意的前提下,我開始了翻譯工作。
背景
Go 中一個常被吹捧的特性是 Go 應用容易部署,原因是 Go 寫的程式是靜態編譯的。但在你運行一個 Web 應用程式時,如果需要管理一系列檔案的路徑和許可權的話,這個優勢就基本不複存在了。
解決方案是把必要的檔案編譯到應用二進位裡去。在 Go 中可以用位元組切片存放檔案中String literals 形式的位元組內容。
fileData := []byte("\x1f\x8b\ ... \x0f\x00\x00")
但這種方法最大的幾個壞處是:
- 更大的二進位檔案
- 對我當前的項目 Lex Library 而言,在沒嵌入靜態檔案之前,可執行檔大小為 20MB,嵌入後為 21MB。
- 更長的編譯時間
- 編譯時間更占記憶體
- 如果你在用小記憶體的裝置開發,這會影響到你。但對我個人來說,還無需關心。
你需要在開發效率和營運行政時間上作取捨。如果你的應用受眾是普羅福士(或者是那些有個人網上應用的極客使用者),就更值得權衡利弊了。
嵌入方式選擇
第一個處理 Go 嵌入靜態檔案的庫,或者說第一個真正知名的,應該是 jteeuwen 的 Go-BinData。這是個命令列應用,你輸入一個檔案路徑,它會產生包含靜態檔案的 .go
檔案。
然而,作者 jteeuwen 似乎離開了星球,並在走時候把 GitHub 上這個倉庫中所有東西刪了。幸運的是,他的代碼是開源的,並在社區被廣泛 fork。你在 GitHub 能找到好幾個維護得很好的 fork。我起初選的 fork 是 shuLhan 的,但後來又選了別的方案,原因下面會講。
更多有關 jteewen 項目的細節在這裡:https://github.com/jteeuwen/go-bindata/issues/5
備選方案
既然 jteewen 的庫有很多很多替代品。下面是我搜尋並整理的一個不完全列表:
- vfsgen - https://github.com/shurcooL/vfsgen
- go.rice - https://github.com/GeertJohan/go.rice
- statik - https://github.com/rakyll/statik
- esc - https://github.com/mjibson/esc
- go-embed - https://github.com/pyros2097/go-embed
- go-resources - https://github.com/omeid/go-resources
- packr - https://github.com/gobuffalo/packr
- statics - https://github.com/go-playground/statics
- templify - https://github.com/wlbr/templify
- gnoso/go-bindata - https://github.com/gnoso/go-bindata
- shuLhan/go-bindata - https://github.com/shuLhan/go-bindata
- fileb0x - https://github.com/UnnoTed/fileb0x
- gobundle - https://github.com/alecthomas/gobundle
- parcello - https://github.com/phogolabs/parcello
本文目的是幫你弄明白這些庫的差別,並幫你在選擇上面的某個庫時決定所需要考慮的特性。
去蕪存菁
有了這麼多選項,當決定要用哪一個才最適合需求時,就讓人頭大。你的程式和我的可能不一樣,但如果是 Web 應用程式的話,那會有很多可借鑒的地方。如果你需要做同樣的選擇,下文的比較將會很有用。
判斷標準
壓縮
上文提到的,嵌入靜態檔案一個缺點是增加了可執行檔大小。你可以在嵌入靜態檔案之前,用一個庫去壓縮,這樣能減少一些空間。同時也需要花點小功夫去解壓縮,但這通常是值得的,因為除了節省空間的外,構建時記憶體佔用量也會減少。此外,網頁靜態檔案壓縮率較高(相比圖片),這就是為什麼大多數瀏覽器支援 gzip 壓縮的原因。這就引入了下一個標準。
可選解壓縮
如果你的可執行檔中存有 gzip 壓縮的靜態檔案,並且你想把這些檔案按 gzip 格式提供給用戶端,那為何不直接發送這些已經壓縮的檔案呢?理想的庫應該支援一個選項,讓你在運行時去設定直接接受壓縮後的檔案,而不需要先解壓。
從本地檔案系統載入
在你開發 Web 應用程式時,在你修改代碼與看到改變之間,任何增加了時間或困難的事都應該被避免。如果在你每次修改 CSS 或 HTML 程式碼時,都必須重新構建 Go 程式來靜態嵌入檔案,你很快(就會放棄並)會選別的方案。
理想的庫應該允許我們輕鬆切換開發中構建的程式,從本地快速載入靜態檔案;在生產中,靜態檔案已經嵌入可執行程式並準備好發布了。
可重現的構建
這個準則讓我很意外,在我著手開發 Lex Library 時,我起初都沒有考慮過。前面提到的,我第一個選擇的庫是 shuLhan fork 的 go-bindata。我選擇它主要是因為我對原始的 go-bindata 庫熟悉,而且這個 fork 看起來維護得很好。
這個庫用起來很贊,不過出乎意料的事,我的持續整合(CI)構建開始失敗。像每一位碰到測試失敗的開發人員一樣,我想到的是改動了什麼。我立刻去檢查最新提交的改動,並試著弄明白為什麼這些改動導致模版處理失敗。在我困惑一陣後,並沒有找到原因,於是我重新跑了一次測試套件,這次測試是基於最後一次構建成功的代碼的,但也失敗了。這說明改動在於環境,而不是My Code。不過這引出了更多了問題。
我是在 Docker 容器中跑持續整合測試的。測試環境應該是自我包好的、嶄新的並且可複現的。但我的假設並不正確。在我看 Dockerfile
時,找到了問題所在:
RUN go get -u github.com/shuLhan/go-bindata/...
庫 go-bindata 有一次小的更新,這影響了我傳靜態檔案路徑的過程。突然間我內嵌檔案路徑就和我的預期不一樣了。這可能怪一些因素,比如 go get 總是擷取預設的分支。最後,歸結為這樣簡單的事實:程式中產生代碼的模組版本沒有被確定並讓我的 git 倉庫追蹤。程式依賴的外部代碼如果改動了,就會在不知不覺時,導致我的構建或測試失敗。
一種解決辦法是存個預先編譯的 go-bindata 可執行檔到我的 git 項目中,但是:
- 通常 git 倉庫中不宜存二進位檔案。
- 如果依賴的庫修複了漏洞,我每次都需要手動更新 go-bindata。
- 如果換一個平台,將會使構建變得困難。
此外,我找到了一個無需獨立二進位檔案的庫,完全依賴代碼版本,這可以用 vendor 來管理。意思是有一個支援 go-generate 的庫,但不同的是,不需要依賴外部程式運行。
附加的標準
上文講你可能有不同的需求,所以在我的下面的對比表中,還添加了一些額外的標準,這或許對你有用。
設定檔
如果你有大量不同的目錄和檔案需要管理,使用設定檔並放到你的源碼裡,這樣管理就輕鬆多了。
http.FileSystem 介面
實現 http.FileSystem 介面的庫能讓嵌入檔案更容易處理。
GitHub 超過 200 星
這有點武斷,星數不一定是衡量代碼品質的方法。不過項目星數能說明庫的活躍性,起碼能說明是不是一個在多處被引用的庫。這反過來也說明很多人都在測試它,和(或)反饋問題。我選的庫,僅僅超過了這個星數,注意一下。
對比
庫 |
壓縮 |
可選解壓 |
本地檔案系統 |
go generate |
無可執行檔 |
設定檔 |
http.FS |
多於 200 星 |
vfsgen |
✓ |
✓ |
✓* |
✓ |
✓ |
✗ |
✓ |
✓ |
go.rice |
✓ |
✗ |
✓ |
✗ |
✗ |
✗ |
✓ |
✓ |
statik |
✓ |
✗ |
✗ |
✗ |
✗ |
✗ |
✓ |
✓ |
esc |
✓ |
✗ |
✓ |
✓ |
✗ |
✗ |
✓ |
✓ |
go-embed |
✓ |
✗ |
✓* |
✗ |
✗ |
✗ |
✗ |
✗ |
go-resources |
✗ |
✗ |
✓* |
✗ |
✗ |
✗ |
✓ |
✗ |
packr |
✓ |
✗ |
✓ |
✗ |
✗ |
✗ |
✓ |
✓ |
statics |
✓ |
✗ |
✓ |
✓ |
✗ |
✗ |
✓ |
✗ |
templify |
✗ |
✗ |
✓ |
✓ |
✗ |
✗ |
✗ |
✗ |
gnoso/go-bindata |
✓ |
✗ |
✗ |
✗ |
✗ |
✗ |
✗ |
✓ |
shuLhan/go-bindata |
✓ |
✗ |
✓ |
✗ |
✗ |
✗ |
✗ |
✗ |
fileb0x |
✓ |
✓ |
✓ |
✓ |
✗ |
✓ |
✓ |
✓ |
gobundle |
✓ |
✗ |
✗ |
✗ |
✗ |
✗ |
✗ |
✗ |
parcello |
✓ |
✗ |
✓ |
✓ |
✗ |
✗ |
✓ |
✓ |
* 需要額外代碼
我使用這些庫的經驗是它們在寫程式和部署時各有不同,要去看對應項目的 README
和文檔。如果你看到上表中不準確之處,請在評論中讓我知道,我會更新。
我的選擇
通過上面的比較表,就很清楚為什麼我最終會用 vfsgen,並且我高度推薦它,尤其在你需要可複現的構建時。稍遜一點的是 fileb0x,它需要獨立的可執行檔才能使用 go generate
。