title: "Go Embed 簡明教程"
date: 2021-03-07T14:53:56+08:00
draft: true
tags: ['go']
author: "dadigang"
author_cn: "大地缸"
personal: "http://www.real007.cn"
關于作者
Go embed 簡明教程
Go編譯的程序非常適合部署泽示,如果沒有通過CGO引用其它的庫的話验残,我們一般編譯出來的可執(zhí)行二進制文件都是單個的文件节预,非常適合復制和部署。在實際使用中听诸,除了二進制文件,可能還需要一些配置文件彼妻,或者靜態(tài)文件作儿,比如html模板、靜態(tài)的圖片舱沧、CSS妹沙、javascript等文件,如何這些文件也能打進到二進制文件中熟吏,那就太美妙距糖,我們只需復制、按照單個的可執(zhí)行文件即可牵寺。
一些開源的項目很久以前就開始做這方面的工作肾筐,比如 gobuffalo/packr、 markbates/pkger缸剪、 rakyll/statik吗铐、 knadh/stuffbin 等等,但是不管怎么說這些都是第三方提供的功能杏节,如果Go官方能內(nèi)建支持就好了唬渗。2019末一個提案被提出 issue#35950,期望Go官方編譯器支持嵌入靜態(tài)文件。后來Russ Cox專門寫了一個設計文檔 Go command support for embedded static assets, 并最終實現(xiàn)了它奋渔。
Go 1.16中包含了go embed的功能镊逝,而且Go1.16基本在一個月左右的時間就會發(fā)布了,到時候你可以嘗試使用它嫉鲸,如果你等不及了撑蒜,你也可以下載Go 1.16beta1嘗鮮。
本文將通過例子玄渗,詳細介紹go embed的各個功能座菠。
嵌入
- 對于單個的文件,支持嵌入為字符串和 byte slice
- 對于多個文件和文件夾藤树,支持嵌入為新的文件系統(tǒng)FS
- 比如導入 "embed"包浴滴,即使無顯式的使用
-
go:embed
指令用來嵌入,必須緊跟著嵌入后的變量名 - 只支持嵌入為string, byte slice和embed.FS三種類型岁钓,這三種類型的別名(alias)和命名類型(如type S string)都不可以
嵌入為字符串
比如當前文件下有個hello.txt的文件升略,文件內(nèi)容為hello,world!
。通過go:embed
指令屡限,在編譯后下面程序中的s變量的值就變?yōu)榱?code>hello,world!品嚣。
12345678910111213
package main
import ( _ "embed" "fmt")
//go:embed hello.txt
var s stringfunc main() {
fmt.Println(s)
}
嵌入為byte slice
你還可以把單個文件的內(nèi)容嵌入為slice of byte,也就是一個字節(jié)數(shù)組钧大。
12345678910111213
package main
import (
_ "embed"
"fmt")
//go:embed hello.txt
var b []byte
func main() {
fmt.Println(b)
}
嵌入為fs.FS
甚至你可以嵌入為一個文件系統(tǒng)翰撑,這在嵌入多個文件的時候非常有用。
比如嵌入一個文件:
1234567891011121314
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
指令的):
1234567891011121314151617
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是支持的拓型,相當于一個:
123456789101112131415
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))
}
還可以嵌入子文件夾下的文件:
1234567891011121314151617
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))
}
還可以支持模式匹配的方式嵌入额嘿,下面的章節(jié)專門介紹瘸恼。
同一個文件嵌入為多個變量
比如下面的例子,s和s2變量都嵌入hello.txt的文件册养。
123456789101112131415161718
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的變量。
123456789101112131415161718
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一級的的變量球拦,即使是函數(shù)內(nèi)的局部變量靠闭,也都支持嵌入:
12345678910111213141516
package mainimport ( _ "embed" "fmt")func main() { //go:embed hello.txt var s string //go:embed hello.txt var s2 string fmt.Println(s, s2)}
局部變量s的值在編譯時就已經(jīng)嵌入了,而且雖然s和s2嵌入同一個文件坎炼,但是它們的值在編譯的時候會使用初始化字段中的不同的值:
1234567891011121314
0x0021 00033 (/Users/....../main.go:10) MOVQ "".embed.1(SB), AX0x0028 00040 (/Users/....../main.go:10) MOVQ "".embed.1+8(SB), CX0x002f 00047 (/Users/....../main.go:13) MOVQ "".embed.2(SB), DX0x0036 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的值谣光。
只讀
嵌入的內(nèi)容是只讀的檩淋。也就是在編譯期嵌入文件的內(nèi)容是什么,那么在運行時的內(nèi)容也就是什么萄金。
FS文件系統(tǒng)值提供了打開和讀取的方法蟀悦,并沒有write的方法,也就是說FS實例是線程安全的氧敢,多個goroutine可以并發(fā)使用日戈。
1234
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指令支持嵌入多個文件
1234567891011121314151617
package mainimport ( "embed" "fmt")//go:embed hello.txt hello2.txtvar f embed.FSfunc main() { data, _ := f.ReadFile("hello.txt") fmt.Println(string(data)) data, _ = f.ReadFile("hello2.txt") fmt.Println(string(data))}
當然你也可以像前面的例子一樣寫成多行go:embed
:
123456789101112131415161718
package mainimport ( "embed" "fmt")//go:embed hello.txt//go:embed hello2.txtvar f embed.FSfunc main() { data, _ := f.ReadFile("hello.txt") fmt.Println(string(data)) data, _ = f.ReadFile("hello2.txt") fmt.Println(string(data))}
支持文件夾
文件夾分隔符采用正斜杠/
,即使是windows系統(tǒng)也采用這個模式。
1234567891011121314151617
package mainimport ( "embed" "fmt")//go:embed pvar f embed.FSfunc main() { data, _ := f.ReadFile("p/hello.txt") fmt.Println(string(data)) data, _ = f.ReadFile("p/hello2.txt") fmt.Println(string(data))}
使用的是相對路徑
相對路徑的根路徑是go源文件所在的文件夾孙乖。
支持使用雙引號"
或者反引號的方式應用到嵌入的文件名或者文件夾名或者模式名上浙炼,這對名稱中帶空格或者特殊字符的文件文件夾有用。
1234567891011121314
package mainimport ( "embed" "fmt")//go:embed "he llo.txt" `hello-2.txt`var f embed.FSfunc main() { data, _ := f.ReadFile("he llo.txt") fmt.Println(string(data))}
匹配模式
go:embed
指令中可以只寫文件夾名唯袄,此文件夾中除了.
和_
開頭的文件和文件夾都會被嵌入弯屈,并且子文件夾也會被遞歸的嵌入,形成一個此文件夾的文件系統(tǒng)越妈。
如果想嵌入.
和_
開頭的文件和文件夾季俩, 比如p文件夾下的.hello.txt文件,那么就需要使用*
梅掠,比如go:embed p/*
。
*
不具有遞歸性店归,所以子文件夾下的.
和_
不會被嵌入阎抒,除非你在專門使用子文件夾的*
進行嵌入:
1234567891011121314151617
package main
import ( "embed" "fmt")//go:embed p/*var f embed.FSfunc main() { data, _ := f.ReadFile("p/.hello.txt") fmt.Println(string(data)) data, _ = f.ReadFile("p/q/.hi.txt") // 沒有嵌入 p/q/.hi.txt fmt.Println(string(data))}
嵌入和嵌入模式不支持絕對路徑、不支持路徑中包含.
和..
,如果想嵌入go源文件所在的路徑消痛,使用*
:
1234567891011121314151617
package mainimport ( "embed" "fmt")//go:embed *var f embed.FSfunc main() { data, _ := f.ReadFile("hello.txt") fmt.Println(string(data)) data, _ = f.ReadFile(".hello.txt") fmt.Println(string(data))}
文件系統(tǒng)
embed.FS
實現(xiàn)了 io/fs.FS
接口且叁,它可以打開一個文件,返回fs.File
:
123456789101112131415
package mainimport ( "embed" "fmt")//go:embed *var f embed.FSfunc main() { helloFile, _ := f.Open("hello.txt") stat, _ := helloFile.Stat() fmt.Println(stat.Name(), stat.Size())}
它還提供了ReadFileh和ReadDir功能秩伞,遍歷一個文件下的文件和文件夾信息:
12345678910111213141516
package mainimport ( "embed" "fmt")//go:embed *var f embed.FSfunc main() { dirEntries, _ := f.ReadDir("p") for _, de := range dirEntries { fmt.Println(de.Name(), de.IsDir()) }}
因為它實現(xiàn)了io/fs.FS
接口逞带,所以可以返回它的子文件夾作為新的文件系統(tǒng):
123456789101112131415161718
package main
import ( "embed" "fmt" "io/fs" "io/ioutil")//go:embed *var f embed.FSfunc main() { ps, _ := fs.Sub(f, "p") hi, _ := ps.Open("q/hi.txt") data, _ := ioutil.ReadAll(hi) fmt.Println(string(data))}
應用
net/http
先前欺矫,我們提供一個靜態(tài)文件的服務時,使用:
1
http.Handle("/", http.FileServer(http.Dir("/tmp")))
現(xiàn)在展氓,io/fs.FS
文件系統(tǒng)也可以轉(zhuǎn)換成http.FileServer的參數(shù)了:
12345
type FileSystem func FS(fsys fs.FS) FileSystemtype Handler func FileServer(root FileSystem) Handler
所以穆趴,嵌入文件可以使用下面的方式:
1
http.Handle("/", http.FileServer(http.FS(fsys)))
text/template和html/template.
同樣的,template也可以從嵌入的文件系統(tǒng)中解析模板:
12
func ParseFS(fsys fs.FS, patterns ...string) (*Template, error)func (t *Template) ParseFS(fsys fs.FS, patterns ...string) (*Template, error)