Golang1.16 使用embed載入靜態檔案

一根毛毛闖天下發表於2021-08-17

embed是什麼

embed是在Go 1.16中新加入的包。它通過//go:embed指令,可以在編譯階段將靜態資原始檔打包進編譯好的程式中,並提供訪問這些檔案的能力。

為什麼需要embed

在以前,很多從其他語言轉過來Go語言的小夥伴會問到,或者踩到一個坑:就是以為Go語言所打包的二進位制檔案中會包含配置檔案的聯同編譯和打包。

結果往往一把二進位制檔案挪來挪去,就無法把應用程式執行起來了,因為無法讀取到靜態檔案的資源。

無法將靜態資源編譯打包二進位制檔案的話,通常會有兩種解決方法:

  • 第一種是識別這類靜態資源,是否需要跟著程式走。
  • 第二種就是將其打包進二進位制檔案中。

第二種情況的話,Go以前是不支援的,大家就會藉助各種花式的開源庫,例如:go-bindata/go-bindata來實現。

但是在Go1.16起,Go語言自身正式支援了該項特性。

它有以下優點

  • 能夠將靜態資源打包到二進位制包中,部署過程更簡單。傳統部署要麼需要將靜態資源與已編譯程式打包在一起上傳,或者使用dockerdockerfile自動化前者,這是很麻煩的。
  • 確保程式的完整性。在執行過程中損壞或丟失靜態資源通常會影響程式的正常執行。
  • 靜態資源訪問沒有io操作,速度會非常快

embed基礎用法

通過 官方文件 我們知道embed嵌入的三種方式:string、bytes 和 FS(File Systems)。其中string[]byte型別都只能匹配一個檔案,如果要匹配多個檔案或者一個目錄,就要使用embed.FS型別。

特別注意:embed這個包一定要匯入,如果匯入不使用的話,使用 _ 匯入即可

一、嵌入為字串


比如當前檔案下有個hello.txt的檔案,檔案內容為hello,world!。通過go:embed指令,在編譯後下面程式中的s變數的值就變為了hello,world!

package main
import (
    _ "embed"
    "fmt"
)
//go:embed hello.txt
var s string
func main() {
    fmt.Println(s)
}

二、嵌入為byte slice


你還可以把單個檔案的內容嵌入為slice of byte,也就是一個位元組陣列。

package main
import (
    _ "embed"
    "fmt"
)
//go:embed hello.txt
var b []byte
func main() {
    fmt.Println(b)
}

三、嵌入為fs.FS


甚至你可以嵌入為一個檔案系統,這在嵌入多個檔案的時候非常有用。

比如嵌入一個檔案:

package main
import (
    "embed"
    "fmt"
)
//go:embed hello.txt
var f embed.FS
func main() {
    data, _ := f.ReadFile("hello.txt")
    fmt.Println(string(data))
}

嵌入本地的另外一個檔案hello2.txt, 支援同一個變數上多個go:embed指令(嵌入為string或者byte slice是不能有多個go:embed指令的):

package main
import (
    "embed"
    "fmt"
)
//go:embed hello.txt
//go:embed hello2.txt
var f embed.FS
func main() {
    data, _ := f.ReadFile("hello.txt")
    fmt.Println(string(data))
    data, _ = f.ReadFile("hello2.txt")
    fmt.Println(string(data))
}

當前重複的go:embed指令嵌入為embed.FS是支援的,相當於一個:

package main
import (
    "embed"
    "fmt"
)
//go:embed hello.txt
//go:embed hello.txt
var f embed.FS
func main() {
    data, _ := f.ReadFile("hello.txt")
    fmt.Println(string(data))
}

還可以嵌入子資料夾下的檔案:

package main
import (
    "embed"
    "fmt"
)
//go:embed p/hello.txt
//go:embed p/hello2.txt
var f embed.FS
func main() {
    data, _ := f.ReadFile("p/hello.txt")
    fmt.Println(string(data))
    data, _ = f.ReadFile("p/hello2.txt")
    fmt.Println(string(data))
}

embed進階用法

Go1.16 為了對 embed 的支援也新增了一個新包 io/fs。兩者結合起來可以像之前操作普通檔案一樣。

一、只讀

嵌入的內容是隻讀的。也就是在編譯期嵌入檔案的內容是什麼,那麼在執行時的內容也就是什麼。

FS檔案系統值提供了開啟和讀取的方法,並沒有write的方法,也就是說FS例項是執行緒安全的,多個goroutine可以併發使用。

embed.FS結構主要有3個對外方法,如下:

// Open 開啟要讀取的檔案,並返回檔案的fs.File結構.
func (f FS) Open(name string) (fs.File, error)

// ReadDir 讀取並返回整個命名目錄
func (f FS) ReadDir(name string) ([]fs.DirEntry, error)

// ReadFile 讀取並返回name檔案的內容.
func (f FS) ReadFile(name string) ([]byte, error)

二、嵌入多個檔案


package main

import (
    "embed"
    "fmt"
)

//go:embed hello.txt hello2.txt
var f embed.FS

func main() {
    data, _ := f.ReadFile("hello.txt")
    fmt.Println(string(data))

    data, _ = f.ReadFile("hello2.txt")
    fmt.Println(string(data))
}

當然你也可以像前面的例子一樣寫成多行go:embed:

package main
import (
    "embed"
    "fmt"
)
//go:embed hello.txt
//go:embed hello2.txt
var f embed.FS
func main() {
    data, _ := f.ReadFile("hello.txt")
    fmt.Println(string(data))
    data, _ = f.ReadFile("hello2.txt")
    fmt.Println(string(data))
}

三、支援資料夾


資料夾分隔符采用正斜槓/,即使是windows系統也採用這個模式。

package main
import (
    "embed"
    "fmt"
)
//go:embed p
var f embed.FS
func main() {
    data, _ := f.ReadFile("p/hello.txt")
    fmt.Println(string(data))
    data, _ = f.ReadFile("p/hello2.txt")
    fmt.Println(string(data))
}

應用

在我們的專案中,是將應用的常用的一些配置寫在了.env的一個檔案上,所以我們在這裡就可以使用go:embed指令。

main.go 檔案:

//go:embed ".env" "v1d0/.env"
var FS embed.FS

func main(){
    setting.InitSetting(FS)
    manager.InitManager()
    cron.InitCron()
    routersInit := routers.InitRouter()
    readTimeout := setting.ServerSetting.ReadTimeout
    writeTimeout := setting.ServerSetting.WriteTimeout
    endPoint := fmt.Sprintf(":%d", setting.ServerSetting.HttpPort)
    maxHeaderBytes := 1 << 20
    server := &http.Server{
        Addr:           endPoint,
        Handler:        routersInit,
        ReadTimeout:    readTimeout,
        WriteTimeout:   writeTimeout,
        MaxHeaderBytes: maxHeaderBytes,
    }
    server.ListenAndServe()
}

setting.go檔案:

func InitSetting(FS embed.FS) {
    // 總配置處理
    var err error
    data, err := FS.ReadFile(".env")
    if err != nil {
        log.Fatalf("Fail to parse '.env': %v", err)
    }
    cfg, err = ini.Load(data)
    if err != nil {
        log.Fatal(err)
    }
    mapTo("server", ServerSetting)
    ServerSetting.ReadTimeout  = ServerSetting.ReadTimeout * time.Second
    ServerSetting.WriteTimeout = ServerSetting.WriteTimeout * time.Second
    // 分版本配置引入
    v1d0Setting.InitSetting(FS)
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章