Go embed 簡明教程

Go 編譯的程序非常適合部署,如果沒有通過 CGO 引用其它的庫的話,我們一般編譯出來的可執行二進制文件都是單個的文件,非常適合複製和部署。在實際使用中,除了二進制文件,可能還需要一些配置文件,或者靜態文件,比如 html 模板、靜態的圖片、CSS、javascript 等文件,如何這些文件也能打進到二進制文件中,那就太美妙,我們只需複製、按照單個的可執行文件即可。

一些開源的項目很久以前就開始做這方面的工作,比如 gobuffalo/packrmarkbates/pkgerrakyll/statikknadh/stuffbin 等等,但是不管怎麼說這些都是第三方提供的功能,如果 Go 官方能內建支持就好了。2019 末一個提案被提出 issue#35950, 期望 Go 官方編譯器支持嵌入靜態文件。後來 Russ Cox 專門寫了一個設計文檔 Go command support for embedded static assets, 並最終實現了它。

Go 1.16 中包含了 go embed 的功能,而且 Go1.16 基本在一個月左右的時間就會發布了,到時候你可以嘗試使用它,如果你等不及了,你也可以下載 Go 1.16beta1 嚐鮮。

本文將通過例子,詳細介紹 go 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))
}

還可以支持模式匹配的方式嵌入,下面的章節專門介紹。

同一個文件嵌入爲多個變量

比如下面的例子,s 和 s2 變量都嵌入 hello.txt 的文件。

package main

import (
	_ "embed"
	"fmt"
)

//go:embed hello.txt
var s string

//go:embed hello.txt
var s2 string

func main() {
	fmt.Println(s)
	fmt.Println(s2)
}

exported/unexported 的變量都支持

Go 可以將文件可以嵌入爲 exported 的變量,也可以嵌入爲 unexported 的變量。

package main

import (
	_ "embed"
	"fmt"
)

//go:embed hello.txt
var s string
//go:embed hello2.txt
var S string

func main() {
	fmt.Println(s)
	fmt.Println(S)
}

package 級別的變量和局部變量都支持

前面的例子都是 package 一級的的變量,即使是函數內的局部變量,也都支持嵌入:

package main

import (
	_ "embed"
	"fmt"
)

func main() {
	//go:embed hello.txt
	var s string
	//go:embed hello.txt
	var s2 string
	fmt.Println(s, s2)
}

局部變量 s 的值在編譯時就已經嵌入了,而且雖然 s 和 s2 嵌入同一個文件,但是它們的值在編譯的時候會使用初始化字段中的不同的值:

0x0021 00033 (/Users/....../main.go:10)        MOVQ    "".embed.1(SB), AX

0x0028 00040 (/Users/....../main.go:10)        MOVQ    "".embed.1+8(SB), CX

0x002f 00047 (/Users/....../main.go:13)        MOVQ    "".embed.2(SB), DX

0x0036 00054 (/Users/....../main.go:13)        MOVQ    DX, "".s2.ptr+72(SP)

0x003b 00059 (/Users/....../main.go:13)        MOVQ    "".embed.2+8(SB), BX

......

"".embed.1 SDATA size=16

0x0000 00 00 00 00 00 00 00 00 0d 00 00 00 00 00 00 00  ................

       rel 0+8 t=1 go.string."hello, world!"+0

"".embed.2 SDATA size=16

0x0000 00 00 00 00 00 00 00 00 0d 00 00 00 00 00 00 00  ................

       rel 0+8 t=1 go.string."hello, world!"+0

注意 s 和 s2 的變量的值是在編譯期就確定了,即使在運行時你更改了 hello.txt 的文件,甚至把 hello.txt 都刪除了也不會改變和影響 s 和 s2 的值。

只讀

嵌入的內容是隻讀的。也就是在編譯期嵌入文件的內容是什麼,那麼在運行時的內容也就是什麼。

FS 文件系統值提供了打開和讀取的方法,並沒有 write 的方法,也就是說 FS 實例是線程安全的,多個 goroutine 可以併發使用。

type FS
	func (f FS) Open(name string) (fs.File, error)
	func (f FS) ReadDir(name string) ([]fs.DirEntry, error)
	func (f FS) ReadFile(name string) ([]byte, error)

go:embed 指令

go:embed 指令支持嵌入多個文件

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))
}

使用的是相對路徑

相對路徑的根路徑是 go 源文件所在的文件夾。

支持使用雙引號"或者反引號的方式應用到嵌入的文件名或者文件夾名或者模式名上,這對名稱中帶空格或者特殊字符的文件文件夾有用。

package main

import (
	"embed"
	"fmt"
)

//go:embed "he llo.txt" `hello-2.txt`
var f embed.FS

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

匹配模式

go:embed指令中可以只寫文件夾名,此文件夾中除了._開頭的文件和文件夾都會被嵌入,並且子文件夾也會被遞歸的嵌入,形成一個此文件夾的文件系統。

如果想嵌入._開頭的文件和文件夾, 比如 p 文件夾下的. hello.txt 文件,那麼就需要使用*,比如go:embed p/*

*不具有遞歸性,所以子文件夾下的._不會被嵌入,除非你在專門使用子文件夾的*進行嵌入:

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/q/.hi.txt") 
	fmt.Println(string(data))

}

嵌入和嵌入模式不支持絕對路徑、不支持路徑中包含..., 如果想嵌入 go 源文件所在的路徑,使用*:

package main

import (
	"embed"
	"fmt"
)

//go:embed *
var f embed.FS

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

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

文件系統

embed.FS實現了 io/fs.FS接口,它可以打開一個文件,返回fs.File:

package main

import (
	"embed"
	"fmt"
)

//go:embed *
var f embed.FS

func main() {
	helloFile, _ := f.Open("hello.txt")
	stat, _ := helloFile.Stat()
	fmt.Println(stat.Name(), stat.Size())
}

它還提供了 ReadFileh 和 ReadDir 功能,遍歷一個文件下的文件和文件夾信息:

package main

import (
	"embed"
	"fmt"
)

//go:embed *
var f embed.FS

func main() {
	dirEntries, _ := f.ReadDir("p")
	for _, de := range dirEntries {
		fmt.Println(de.Name(), de.IsDir())
	}
}

因爲它實現了io/fs.FS接口,所以可以返回它的子文件夾作爲新的文件系統:

package main

import (
	"embed"
	"fmt"
	"io/fs"
	"io/ioutil"
)

//go:embed *
var f embed.FS

func main() {
	ps, _ := fs.Sub(f, "p")
	hi, _ := ps.Open("q/hi.txt")
	data, _ := ioutil.ReadAll(hi)
	fmt.Println(string(data))
}

應用

net/http

先前,我們提供一個靜態文件的服務時,使用:

http.Handle("/", http.FileServer(http.Dir("/tmp")))

現在,io/fs.FS文件系統也可以轉換成 http.FileServer 的參數了:

type FileSystem
	func FS(fsys fs.FS) FileSystem

type Handler
	func FileServer(root FileSystem) Handler

所以,嵌入文件可以使用下面的方式:

http.Handle("/", http.FileServer(http.FS(fsys)))

text/template 和 html/template.

同樣的, template 也可以從嵌入的文件系統中解析模板:

func ParseFS(fsys fs.FS, patterns ...string) (*Template, error)

func (t *Template) ParseFS(fsys fs.FS, patterns ...string) (*Template, error)
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://colobu.com/2021/01/17/go-embed-tutorial/