[golang]Go內嵌靜態資源go-bindata的安裝及使用

  • 2019 年 10 月 7 日
  • 筆記

使用 Go 開發應用的時候,有時會遇到需要讀取靜態資源的情況。比如開發 Web 應用,程式需要載入模板文件生成輸出的 HTML。在程式部署的時候,除了發布應用可執行文件外,還需要發布依賴的靜態資源文件。這給發布過程添加了一些麻煩。既然發布單獨一個可執行文件是非常簡單的操作,就有人會想辦法把靜態資源文件打包進 Go 的程式文件中。下面就來看一些解決方案:

go-bindata

go-bindata 是目前我的程式 pugo 在用的嵌入靜態資源的工具。它可以把靜態文件嵌入到一個 go 文件中,並提供一些操作方法。

安裝 go-bindata: go get -u github.com/jteeuwen/go-bindata/…

注意 go get 地址最後的三個點 ...。這樣會分析所有子目錄並下載依賴編譯子目錄內容。go-bindata 的命令工具在子目錄中。(還要記得把 $GOPATH/bin 加入系統 PATH)。

使用命令工具 go-bindata ( pugo 的例子):

go-bindata -o=app/asset/asset.go -pkg=asset source/… theme/… doc/source/… doc/theme/…

-o 輸出文件到 app/asset/asset.go,包名 -pkg=asset,然後是需要打包的目錄,三個點包括所有子目錄。這樣就可以把所有相關文件打包到 asset.go 且開頭是 package asset 保持和目錄一致。

pugo 里釋放靜態文件的程式碼:

dirs := []string{"source", "theme", "doc"} // 設置需要釋放的目錄    for _, dir := range dirs {      // 解壓dir目錄到當前目錄      if err := asset.RestoreAssets("./", dir); err != nil {          isSuccess = false          break      }  }  if !isSuccess {      for _, dir := range dirs {          os.RemoveAll(filepath.Join("./", dir))      }  }

asset.go 內的靜態內容還是根據實際的目錄位置索引。所以我們可以直接通過目錄或者文件地址去操作。

-debug 開發模式

go-bindata 支援開發模式,即不嵌入靜態內容,只生成操作方法到輸出的 go 程式碼中,如:

go-bindata -debug -o=app/asset/asset.go -pkg=asset source/… theme/… doc/source/… doc/theme/…

-debug 參數開啟開發模式。生成的程式碼會直接去讀取靜態文件到記憶體,而不是編碼到程式碼中。程式碼文件更小,你更快速的編寫業務邏輯。

// -pkg=asset, 打包的包名是 asset  bytes, err := asset.Asset("theme/default/post.html")    // 根據地址獲取對應內容  if err != nil {      fmt.Println(err)      return  }  t, err := template.New("tpl").Parse(string(bytes))      // 比如用於模板處理  fmt.Println(t, err)
http.FileSystem

http.FileSystem 是定義 HTTP 靜態文件服務的介面。go-bindata 的第三方包 go-bindata-assetfs 實現了這個介面,支援 HTTP 訪問靜態文件目錄的行為。以我們上面編譯好的 asset.go 為例:

import (      "net/http"        "github.com/elazarl/go-bindata-assetfs"      "github.com/go-xiaohei/pugo/app/asset" // 用 pugo 的asset.go進行測試  )    func main() {      fs := assetfs.AssetFS{          Asset:     asset.Asset,          AssetDir:  asset.AssetDir,          AssetInfo: asset.AssetInfo,      }      http.Handle("/", http.FileServer(&fs))      http.ListenAndServe(":12345", nil)  }

訪問 http://localhost:12345,就可以看到嵌入的 source,theme,doc 的目錄列表頁面,和 Nginx 查看靜態文件目錄一樣的。

go.rice

go.rice 也支援打包靜態文件到 go 文件中,但是行為和 go-bindata 很不相同。從使用角度,go.rice 其實是更便捷的靜態文件操作庫。打包靜態文件反而是順帶的功能。

安裝和 go-bindata 一樣,注意 三個點

go get github.com/GeertJohan/go.rice/…

go.rice 把一個目錄認為是一個 rice.Box 操作:

import (      "fmt"      "html/template"        "github.com/GeertJohan/go.rice"  )    func main() {      // 這裡寫相對於的執行文件的地址      box, err := rice.FindBox("theme/default")      if err != nil {          println(err.Error())          return      }      // 從目錄 Box 讀取文件      str, err := box.String("post.html")      if err != nil {          println(err.Error())          return      }      t, err := template.New("tpl").Parse(str)      fmt.Println(t, err)  }

rice 命令

go.rice 的打包命令是 rice。用起來非常直接:在有使用 go.rice 操作的 go 程式碼目錄,直接執行 rice embed-go:

rice embed-go rice -i "github.com/fuxiaohei/xyz" embed-go // -i 處理指定包里的 go.rice 操作

他就會生成當前包名下的、嵌入了文件的程式碼 rice-box.go。但是,它不遞歸處理 import。他會分析當前目錄下的 go 程式碼中 go.rice 的使用,找到對應需要嵌入的文件夾。但是子目錄下的和 import 的裡面的 go.rice 使用不會分析,需要你手動 cd 過去或者 -i 指定要處理的包執行命令。這點來說非常的不友好。

http.FileSystem

go.rice 是直接支援 http.FileSystem 介面:

func main() {      // MustFindBox 出錯直接 panic      http.Handle("/", http.FileServer(rice.MustFindBox("theme").HTTPBox()))      http.ListenAndServe(":12345", nil)  }

有點略繁瑣的是 rice.FindBox(dir) 只能載入一個目錄。因此需要多個目錄的場景,會有程式碼:

func main() {      http.Handle("/img", http.FileServer(rice.MustFindBox("static/img").HTTPBox()))      http.Handle("/css", http.FileServer(rice.MustFindBox("static/css").HTTPBox()))      http.Handle("/js", http.FileServer(rice.MustFindBox("static/js").HTTPBox()))      http.ListenAndServe(":12345", nil)  }

esc

esc 的作者在研究幾款嵌入靜態資源的工具後,發覺都不好用,就自己寫出了 esc。它的需求很簡單,就是嵌入靜態資源 和 支援 http.FileSystemesc 工具也這兩個主要功能。

安裝 esc:

go get github.com/mjibson/esc

使用方法和 go-bindata 類似:

// 注意 esc 不支援 source/… 三個點表示所有子目錄 go-bindata -o=asset/asset.go -pkg=asset source theme doc/source doc/theme

直接支援 http.FileSystem

import (      "net/http"      "asset" // esc 生成 asset/asset.go  )    func main() {      fmt.Println(asset.FSString(false, "/theme/default/post.html"))         // 讀取單個文件      http.ListenAndServe(":12345", http.FileServer(asset.FS(false)))     // 支援 http.FileSystem,但是沒有做展示目錄的支援  }

esc 有個較大的問題是只能一個一個文件操作,不能文件夾操作,沒有類似go-bindata 的 asset.RestoreDir() 方法。並且沒有方法可以列出嵌入的文件的列表,導致也無法一個一個文件操作,除非自己寫死。這是我不使用他的最大原因。

go generate

嵌入靜態資源的工具推薦配合 go generate 使用。例如 pugo 的入口文件就有:

package main    import (      "os"      "time"        "github.com/go-xiaohei/pugo/app/command"      "github.com/go-xiaohei/pugo/app/vars"      "github.com/urfave/cli"  )    //go:generate go-bindata -o=app/asset/asset.go -pkg=asset source/... theme/... doc/source/... doc/theme/...    // ......

在編譯的時候執行:

go generate && go build

這個是 go generate 的基本用法。更詳細的了解可以看 官方博文

總結

我在開發 pugo 的時候對這幾款嵌入靜態資源的程式進行了測試。go.rice 並不是我想要的模式,就沒有考慮。esc 提供的操作方法太少,無法滿足程式開發的需要。最後選擇 go-bindata。但是 go-bindata 和 go.rice 都是將純字元數據或 []byte 字元數據寫入 go 文件,內容非常大。esc 是寫入 gzip 壓縮流的 Base64 編碼。經過壓縮後 go 程式碼的大小明顯更少(我嵌入的都是模板等文本文件)。可見庫類都有各自的優缺點。倘若有 go-bindata 那樣豐富的 API,又有 esc 那樣嵌入壓縮過的字元數據,那該多好。