這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
原文地址 https://scene-si.org/2017/08/22/embedding-data-in-go-executables/
假如你已經關注了我一段時間,應該知道我正在開發Pendulum編輯器作為每天至少編碼一小時的#100DaysOfCode挑戰。Pendulum是一個非常適合編輯簡單文本和markdown檔案的基於web的編輯器。
實際上這篇文章就是用它寫的。它由go後端和VueJS前端組成。我希望它便於使用並提供包含一切的單個可執行檔,因此使用者不需要下載安裝器並解壓檔案。我需要找個能把所有東西打包到一塊的方案。我決定用go-bindata以代碼產生的方式來把所有資料通過 go build 添加到可執行檔中。
代碼產生?
當然這很簡單。例如 go-bindata 工具可以幫我們從public_html
目錄產生對應的 .go 檔案。這對我的應用程式情境來說是極好的。不過為什麼要用bash指令碼或者makefile來產生它呢?因為這樣我們在執行go build
之前就只需要通過執行一下go generate
利用go的代碼產生工具了如果還不熟悉代碼產生,你只需要在你代碼的某處加上簡單的注釋,用main.go
舉個例子:
package main//go:generate echo "Hello world"func main() {}
執行go generate
時,可以看到輸出了 “Hello world” 。這不是你用 go generate 產生代碼的實際需求。你在//go:generate
後面寫的一切都會執行。如果你想的話,甚至可以執行go build
。
package main//go:generate echo "Hello world"//go:generate go run main.gofunc main() { println("Hello world from Go")}
運行這個會有預期的輸出:
%go generateHello worldHello world from Go
旗開得勝!go generate 很有意思。Node程式通過babel
來使Node ES5運行時相容ES6/ES7的文法。人們正嘗試用類似的途徑為go提供超出語言目前功能的特性。
例如,genny主要針對強型別代碼的產生,因此不再需要手動複製粘貼。不過Have這樣的項目更接近Babel對Node的處理–提供轉換到go的語言。目前我還不清楚這方面更有吸引力的其他嘗試。不過關於Go2及泛型的討論似乎比較有趣。
這對我們的應用情境來說略顯枯燥,我們只是要把一些資料打包到程式中。那麼閑話休提,書歸正傳:
//go:generate go-bindata -prefix front/src -o assets/bindata.go -pkg assets -nomemcopy front/src/dist/...
這一行略長,就把它拆分來看:
//go:generate
- 為go generate
作提示
go-bindata
- 要執行的主命令
-prefix front/src
- 排除“front/src”包
-o assets/bindata.go
- 指定輸出檔案
-pkg assets
- 要產生的包名
-nomemcopy
- 對記憶體佔用的最佳化
front/src/dist/...
- 要打包的地方
這會在應用目錄下建立一個可以簡單的用app/assets
匯入的assets
包,其中app
對應的是應用目錄。
通過HTTP提供內嵌檔案服務
這稍微有點複雜。不過看一下文檔之後就簡單了。如果要基於本地檔案提供服務,你大致需要下面這幾行類似的代碼:
folder := http.Dir("/")server := http.FileServer(folder)http.Handle("/", server)
實際上,go-bindata-assetfs包已經提供了一個http.FileServer實現。這個用起來就夠簡單了:
import "github.com/elazarl/go-bindata-assetfs"import "app/assets"// ...func main() { // ... files := assetfs.AssetFS{ Asset: assets.Asset, AssetDir: assets.AssetDir, AssetInfo: assets.AssetInfo, Prefix: "dist", } server := http.FileServer(&files) // ...}
還有一個小問題。我用的是啟用了pushHistory的VueJS應用。這就意味著,使用者使用時會看到沒有釋伴符(雜湊,#)的類似/blog/about.md
的普通連結。這些需要被應用處理的連結內容在asset中並不存在。
這個問題也不難解決。assetfs.AssetFS
結構體有一個AssetsInfo
方法(相當於os.Stat
)和一個Asset
方法(有點像ioutil.ReadFile
)。這使檢查一個檔案是否存在於asset,若不存在則輸出另一個檔案成為可能:
// Serves index.html in case the requested file isn't found// (or some other os.Stat error)func serveIndex(serve http.Handler, fs assetfs.AssetFS) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { _, err := fs.AssetInfo(path.Join(fs.Prefix, r.URL.Path)) if err != nil { contents, err := fs.Asset(path.Join(fs.Prefix, "index.html")) if err != nil { http.Error(w, err.Error(), http.StatusNotFound) return } w.Header().Set("Content-Type", "text/html") w.Write(contents) return } serve.ServeHTTP(w, r) }}
如果找到了檔案,就用預置的ServeHTTP方法取代我自己的實現。採用這種方法只需要對我們之前定義的handler稍作調整:
http.HandleFunc("/", serveIndex(server, assets))
serveIndex
函數返回一個http.HandlerFunc
,這行是相應的修改。這就提供了你用 go generate 和 go-bindata 添加到應用中的資料服務的完整實現。如果你想跳過//go:generate
環節把這些放到CI指令碼中也是可以的。
鑒於此我實現了Pendulum的單個可執行發布版本。可以從GitHub發布頁擷取並嘗試。
編輯:改進serveIndex樣本 感謝@Rdihipone
當你看到了這裡…
要是你能買本我的書定是極好的:
- API Foundations in Go
- 12 Factor Apps with Docker and Go
- The SaaS Handbook (work in progress)
I promise you’ll learn a lot more if you buy one. Buying a copy supports me writing more about similar topics. Say thank you and buy my books.Feel free to send me an email if you want to book my time for consultancy/freelance services. I’m great at APIs, Go, Docker, VueJS and scaling services, among many other things.