我们好,我是值班员 polarisxu。
讨厌 Go 词汇有许多理据,当中有一点“即使校对为两个十进制文档,间接运转,没其它倚赖,使布署不光难。”我想是许多人讨厌的。
不过两个工程项目,很可能会包涵许多静态天然资源文档,如此一来,两个 Go 十进制文档就无法化解了,须要将动态天然资源文档一同所带。只好有了许多服务器端软件系统,将动态天然资源文档“嵌入”最后的 Go 十进制文档中。最著名的如果是 go-bindata,除此之外除了许多其它的:
github.com/alecthomas/gobundlegithub.com/GeertJohan/go.ricegithub.com/go-playground/staticsgithub.com/gobuffalo/packrgithub.com/knadh/stuffbingithub.com/mjibson/escgithub.com/omeid/go-resourcesgithub.com/phogolabs/parcellogithub.com/pyros2097/go-embedgithub.com/rakyll/statikgithub.com/shurcooL/vfsgengithub.com/UnnoTed/fileb0xgithub.com/wlbr/templifyperkeep.org/pkg/fileembed从那个条目不足以窥见市场需求的广泛性。只好非官方下定决心提供更多同时实现,在 go 指示中同时实现该机能。即使在 Go 指示中加进对嵌入基本机能的间接全力支持将消解对这类辅助工具的市场需求,最少能精简其它辅助工具的同时实现。
2020 年 10 月 30 日,Russ Cox 提交了最后的同时实现:[cmd/go: add //go:embed support](cmd/go: add //go:embed support),意味着你在 tip 版本能试玩该机能了。Go1.16 版本会包涵该机能。欢迎我们试玩,反馈建议。
01 试玩 go embed
通过几个示例快速了解 go embed 的用法。
例 1:嵌入文档 — Web 应用
基于 Echo 框架:
package mainimport ( _ “embed” “net/http” “github.com/labstack/echo”)//go:embed static/logo.pngvar content []bytefunc main() { e := echo.New() e.GET(“/”, func(c echo.Context) error { return c.Blob(http.StatusOK, “image/png”, content) }) e.Logger.Fatal(e.Start(“:8989”))}目录结构如下:
.├── main.go└── static └── logo.png校对运转后,能将十进制文档移到任何地方运转,浏览器访问 http://localhhost:8989,能够正确显示 logo 图片表示成功了。
基于 Gin 框架,代码类似:
package mainimport ( _ “embed” “net/http” “github.com/gin-gonic/gin”)//go:embed static/logo.pngvar content []bytefunc main() { router := gin.Default() router.GET(“/”, func(ctx *gin.Context) { ctx.Data(http.StatusOK, “image/png”, content) }) router.Run(“:8989”)}间接使用 net/http 库,代码如下:
package mainimport ( _ “embed” “log” “net/http” “fmt”)//go:embed static/logo.pngvar content []bytefunc main() { http.HandleFunc(“/”, func(w http.ResponseWriter, r *http.Request) { w.Header().Add(“Content-Type”, “image/png”) w.WriteHeader(http.StatusOK) fmt.Fprintf(w, “%s”, content) }) log.Fatal(http.ListenAndServe(“:8989”, nil))}例 2:嵌入文档 — 指示行应用
简单的 Hello World:
package mainimport ( _ “embed” “fmt”)//go:embed message.txtvar message stringfunc main() { fmt.Println(message)}当中 messaeg.txt 中的内容是 Hello World。目录结构如下:
.├── main.go└── message.txt校对后,能将十进制移到任何地方,运转输出 Hello World(即 messaeg.txt 中的内容)。
例 3:嵌入目录 – 指示行应用
以下程序将 static 目录嵌入到十进制程序中,然后在当前目录创建 static 目录中的所有文档。
package mainimport ( “embed” “io” “log” “os” “path”)//go:embed staticvar local embed.FSfunc main() { fis, err := local.ReadDir(“static”) if err != nil { log.Fatal(err) } for _, fi := range fis { in, err := local.Open(path.Join(“static”, fi.Name())) if err != nil { log.Fatal(err) } out, err := os.Create(“embed-” + path.Base(fi.Name())) if err != nil { log.Fatal(err) } io.Copy(out, in) out.Close() in.Close() log.Println(“exported”, “embed-“+path.Base(fi.Name())) }}该示例的目录结构和例 1 一样。校对后,能将十进制文档移到任何地方,运转后,会在当前目录输出以 embed- 开头的文档。
例 4:嵌入目录 — Web 应用
基于 Echo 框架:
package mainimport ( “embed” “net/http” “github.com/labstack/echo/v4”)//go:embed staticvar local embed.FSfunc main() { e := echo.New() e.GET(“/*”, echo.WrapHandler(http.FileServer(http.FS(local)))) e.Logger.Fatal(e.Start(“:8989”))}同样,目录结构和 example1 一致。校对后运转,访问 http://localhost:8989,看到如下界面:
注意上面使用的是 /*,如果间接使用 /,点击链接会是 404。
换成 Gin,代码如下:
package mainimport ( “embed” “net/http” “github.com/gin-gonic/gin”)//go:embed static/*var local embed.FSfunc main() { router := gin.Default() router.GET(“/*filepath”, gin.WrapH(http.FileServer(http.FS(local)))) router.Run(“:8989”)}结果和 Echo 框架一样。同样要注意是 /*filepath,无法是 /。
换成标准库 net/http 试试?
package mainimport ( “embed” “log” “net/http”)//go:embed staticvar local embed.FSfunc main() { http.Handle(“/”, http.FileServer(http.FS(local))) log.Fatal(http.ListenAndServe(“:8989”, nil))}标准库中 / 会自动处理所有的请求。
02 //go:embed 指令
之前服务器端的现实,基本是基于 go generate,将动态天然资源文档生成 go 源文档,最后校对进十进制文档中。非官方的同时实现,通过 //go:embed 指令,在校对时将动态天然资源嵌入十进制文档中。然后,Go 通过标准库,让用户能够访问这些嵌入的天然资源。因此,先介绍下 //go:embed 指令的用法。
相关规则
在变量声明上方,通过 //go:embed 指令指定两个或多个符合 path.Match 模式的要嵌入的文档或目录。相关规则或使用注意如下:
1)跟其它指令一样,// 和 go:embed 之间无法有空格。(不会报错,但该指令会被校对器忽略)
2)指令和变量声明之间能有空行或普通注释,无法有其它语句;
//go:embed message.txtvar message string以上代码是允许的,不过建议紧挨着,而且建议变量声明和指令之间也别加注释,注释如果放在指令上方。
3)变量的类型只能是 string、[]byte 或 embed.FS,即使是这三个类型的别名也不行;
type mystring = string//go:embed hello.txtvar message mystring // 校对不通过:go:embed cannot apply to var of type mystring4)允许有多个 //go:embed 指令。多个文档或目录能通过空格分隔,也能写多个指令。比如:
//go:embed image template//go:embed html/index.htmlvar content embed.FS5)文档或目录使用的是相对路径,相对于指令所在 Go 源文档所在的目录,路径分隔符永远使用 /;当文档或目录名包涵空格时,能使用双引号或反引号括起来。
6)对于目录,会以该目录为根,递归的方式嵌入所有文档和子目录;
7)变量的声明能是导出或非导出的;能是全局也能在函数内部;但只能是声明,无法给初始化值;
//go:embed message.txtvar message string = “” // 校对不通过:go:embed cannot apply to var with initializer8)只能嵌入模块内的文档,比如 .git/* 或软链接文件无法匹配;空目录会被忽略;
9)模式无法包涵 . 或 ..,也无法以 / 开始,如果要匹配当前目录所有文档,如果使用 * 而不是 .;
03 标准库
和 embed 相关的标准库有 5 个,当中 2 个是新增的:embed 和 io/fs;net/http,text/template 和 html/template 包倚赖 io/fs 包,而 embed.FS 类型同时实现了 io/fs 包的 FS 接口,因此这 3 个包能使用 embed.FS。(Go1.16 正式发布时可能还会增加其它包或修改许多包的内容)
io/fs 包
该包定义了文档系统的基本接口。文档系统既能由主机操作系统提供更多,也能由其它包提供。本文我们主要介绍和 embed 密切相关的内容。
先看 FS 接口:
type FS interface { // Open opens the named file. // // When Open returns an error, it should be of type *PathError // with the Op field set to “open”, the Path field set to name, // and the Err field describing the problem. // // Open should reject attempts to open names that do not satisfy // ValidPath(name), returning a *PathError with Err set to // ErrInvalid or ErrNotExist. Open(name string) (File, error)}FS 提供更多对分层文档系统的访问。像操作系统使用的文档系统就是一种分层文档系统。
FS 接口是文档系统所需的最小同时实现。文档系统能同时实现其它接口,比如 fs.ReadFileFS,以提供更多其它或优化的机能。
File 接口:
type File interface { Stat() (FileInfo, error) Read([]byte) (int, error) Close() error}该接口定义对单个文档的访问。这是文档的最小同时实现要求。文档能同时实现其它接口,例如 fs.ReadDirFile,io.ReaderAt 或 io.Seeker,以提供更多其它或优化的机能。
即使有了 FS、File 等的接口抽象,之前在 os 包中的许多内容移到了 io/fs 包中,比如 fs.FileInfo 接口、fs.FileMode 类型,os 中原有的定义改成了它们的别名。
DirEntry 接口:
type DirEntry interface { // Name returns the name of the file (or subdirectory) described by the entry. // This name is only the final element of the path (the base name), not the entire path. // For example, Name would return “hello.go” not “/home/gopher/hello.go”. Name() string // IsDir reports whether the entry describes a directory. IsDir() bool // Type returns the type bits for the entry. // The type bits are a subset of the usual FileMode bits, those returned by the FileMode.Type method. Type() FileMode // Info returns the FileInfo for the file or subdirectory described by the entry. // The returned FileInfo may be from the time of the original directory read // or from the time of the call to Info. If the file has been removed or renamed // since the directory read, Info may return an error satisfying errors.Is(err, ErrNotExist). // If the entry denotes a symbolic link, Info reports the information about the link itself, // not the links target. Info() (FileInfo, error)}DirEntry 是从目录读取的条目(使用 ReadDir 函数或 ReadDirFile 的 ReadDir方法)。比如下面 embed 包中的 embed.FS 有两个方法 ReadDir 就返回了 DirEntry 类型的切片。这样能遍历 embed.FS 那个文档系统。
embed 包
天然资源文档嵌入 Go 十进制程序后,我们通过 embed 包能访问它们。
string 和 []byte
当指令用于 string 或 []byte 时,只能有两个模式,匹配两个文档,字符串或 []byte 的内容是该文档的内容。这时虽然不须要使用 embed 包,但必须导入,因此采用空导入:
import _ “embed” FS(File System)
一般嵌入单个文档,采用 string 或 []byte 是最好的选择;但嵌入许多文件或目录树,如果使用 embed.FS 类型,这也是该包目前唯一的类型。
type FS struct { // The compiler knows the layout of this struct. // See cmd/compile/internal/gcs initEmbed. // // The files list is sorted by name but not by simple string comparison. // Instead, each files name takes the form “dir/elem” or “dir/elem/”. // The optional trailing slash indicates that the file is itself a directory. // The files list is sorted first by dir (if dir is missing, it is taken to be “.”) // and then by base, so this list of files: // // p // q/ // q/r // q/s/ // q/s/t // q/s/u // q/v // w // // is actually sorted as: // // p # dir=. elem=p // q/ # dir=. elem=q // w/ # dir=. elem=w // q/r # dir=q elem=r // q/s/ # dir=q elem=s // q/v # dir=q elem=v // q/s/t # dir=q/s elem=t // q/s/u # dir=q/s elem=u // // This order brings directory contents together in contiguous sections // of the list, allowing a directory read to use binary search to find // the relevant sequence of entries. files *[]file}FS 是文档的只读集合,通常使用 //go:embed 指令进行初始化。如果不使用 //go:embed 指令声明 FS,则它是两个空文档系统。
FS 是只读值,因此能安全地同时使用多个 goroutine,也能将 FS 类型的值相互赋值。
FS 同时实现了 fs.FS,因此它能与任何使用文档系统接口(fs.FS)的包一同使用,包括 net/http,text/template 和 html/template。
除此之外,FS 还是同时实现了 fs.ReadDirFS 和 fs.ReadFileFS 这两个接口。
所以,FS 同时实现了 3 个接口,一共 3 个方法:
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)关于它们的用法,在上文例子中有所涉及。
04 实际工程项目使用
本节模拟两个实际工程项目,看怎么使用 embed,主要两个方面:嵌入动态天然资源;嵌入模板文档。本节示例代码地址:https://github.com/polaris1119/embed-example,采用 Echo 框架。
即使是演示 embed 的实际用法,因此工程项目做了尽可能精简,目录结构如下:
.├── LICENSE├── README.md├── cmd│ └── blog│ └── main.go├── embed.go├── go.mod├── go.sum├── static│ └── css│ └── style.min.css└── template └── index.html做个说明:
即使 go:embed 指令只能从相对源码所在目录的位置引用天然资源,这里特意采用了 main.go 放在 cmd/blog 中这种方式,看这样如何处理资源嵌入;static 和 template 目录是须要嵌入的目录;即使 main.go 和 static/template 不在同两个目录,因此 main.go 中没法间接使用 go:embed 指令。我们在 static 的同级目录下创建两个文档:embed.go,专门用来写该指令。代码如下:
package embedexampleimport ( “embed”)//go:embed staticvar StaticAsset embed.FS//go:embed templatevar TemplateFS embed.FS这样,工程项目中所有其它的地方都能通过引用该包来使用嵌入的天然资源。
接着看 main.go 的代码如何使用它的。
package mainimport ( “html/template” “io” “net/http” “github.com/labstack/echo/v4” “github.com/labstack/echo/v4/middleware” “github.com/polaris1119/embedexample”)func main() { e := echo.New() e.Use(middleware.Recover()) e.Use(middleware.Logger()) tpl := &Template{ templates: template.Must(template.New(“index”).ParseFS(embedexample.TemplateFS, “template/*.html”)), } e.Renderer = tpl e.GET(“/static/*”, echo.WrapHandler(http.FileServer(http.FS(embedexample.StaticAsset)))) e.GET(“/”, func(ctx echo.Context) error { return ctx.Render(http.StatusOK, “index.html”, nil) }) e.Logger.Fatal(e.Start(“:2020”))}type Template struct { templates *template.Template}func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { return t.templates.ExecuteTemplate(w, name, data)}模板的引用:
tpl := &Template{ templates: template.Must(template.New(“index”).ParseFS(embedexample.TemplateFS, “template/*.html”)),}通过 ParseFS 方法来同时实现,全力支持 path.Match 格式。
而动态天然资源这样引用:
e.GET(“/static/*”, echo.WrapHandler(http.FileServer(http.FS(embedexample.StaticAsset))))这样,在模板文档 index.html 中就能访问到样式文档了:
<link rel=”stylesheet” href=”/static/css/style.min.css”>能将校对后的十进制文档移到任何地方,然后运转,访问 http://localhost:2020 看到如下界面表示成功了。
05 总结
本文通过几个例子快速了解非官方嵌入动态天然资源的用法,然后讲解许多关键的标准库,最后是两个实际工程项目中使用的例子。
纵观非官方的同时实现,使用起来很方
06 参考资料
Embed 设计提案:https://github.com/golang/proposal/blob/master/design/draft-embed.md示例参考:https://github.com/mattn/go-embed-exampletip 相关文档:https://tip.golang.org