Go embed 簡明教程
Go 編譯的程序非常適合部署,如果沒有通過 CGO 引用其它的庫的話,我們一般編譯出來的可執行二進制文件都是單個的文件,非常適合複製和部署。在實際使用中,除了二進制文件,可能還需要一些配置文件,或者靜態文件,比如 html 模板、靜態的圖片、CSS、javascript 等文件,如何這些文件也能打進到二進制文件中,那就太美妙,我們只需複製、按照單個的可執行文件即可。
一些開源的項目很久以前就開始做這方面的工作,比如 gobuffalo/packr、markbates/pkger、rakyll/statik、knadh/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 的各個功能。
嵌入
- 對於單個的文件,支持嵌入爲字符串和 byte slice
- 對於多個文件和文件夾,支持嵌入爲新的文件系統 FS
- 比如導入 "embed" 包,即使無顯式的使用
go:embed
指令用來嵌入,必須緊跟着嵌入後的變量名- 只支持嵌入爲 string, byte slice 和 embed.FS 三種類型,這三種類型的別名 (alias) 和命名類型 (如 type S string) 都不可以
嵌入爲字符串
比如當前文件下有個 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/