默认情况下,将项目打包成二进制的时候是不会加入静态资源文件的,因此在部署的时候还需要捎带上这些文件,比如,一些配置文件,图片,样式表等。

很多时候,这些静态文件是不需要变的,如果能一并加入到二进制文件,就能减少部署时的依赖。

于是有了很多第三方解决方案,将静态资源文件“嵌入”最终的 Go 二进制文件中。最知名的应该是 go-bindata,此外还有很多其他的:

github.com/alecthomas/gobundle
github.com/GeertJohan/go.rice
github.com/go-playground/statics
github.com/gobuffalo/packr
github.com/knadh/stuffbin
github.com/mjibson/esc
github.com/omeid/go-resources
github.com/phogolabs/parcello
github.com/pyros2097/go-embed
github.com/rakyll/statik
github.com/shurcooL/vfsgen
github.com/UnnoTed/fileb0x
github.com/wlbr/templify
perkeep.org/pkg/fileembed

从这个列表足以看出需求的广泛性。于是官方决定提供实现,在 go 命令中实现该功能。因为在 Go 命令中添加对嵌入基本功能的直接支持将消除对某些工具的需求,至少可以简化其他工具的实现。

这个功能会在 Go1.16版本中被加入。

01 试用 go embed

通过几个示例快速了解 go embed 的用法。
注意,代码中的 //go embed 是指令,而不是注释。

例 1:内嵌文件 — Web 应用

基于 Echo 框架:

package mainimport (_ "embed""net/http""github.com/labstack/echo"
)//go:embed static/logo.png
var 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.png
var 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.png
var 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.txt
var 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 static
var 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 static
var 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 static
var 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.txt
var message mystring // 编译不通过:go:embed cannot apply to var of type mystring

4)允许有多个 //go:embed 指令。多个文件或目录可以通过空格分隔,也可以写多个指令。比如:

//go:embed image template
//go:embed html/index.html
var content embed.FS

5)文件或目录使用的是相对路径,相对于指令所在 Go 源文件所在的目录,路径分隔符永远使用 /;当文件或目录名包含空格时,可以使用双引号或反引号括起来。
6)对于目录,会以该目录为根,递归的方式嵌入所有文件和子目录;
7)变量的声明可以是导出或非导出的;可以是全局也可以在函数内部;但只能是声明,不能给初始化值;

//go:embed message.txt
var message string = "" // 编译不通过:go:embed cannot apply to var with initializer

8)只能内嵌模块内的文件,比如 .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 link's 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/gc's initEmbed.//// The files list is sorted by name but not by simple string comparison.// Instead, each file's 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 static
var StaticAsset embed.FS//go:embed template
var 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 = tple.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 中就可以访问到样式文件了:

可以将编译后的二进制文件移到任何地方,然后运行,访问 http://localhost:2020 看到如下界面表示成功了。

05 总结

本文通过几个例子快速了解官方内嵌静态资源的用法,然后讲解一些关键的标准库,最后是一个实际项目中使用的例子。
纵观官方的实现,使用起来很方便,通过一个 go:embed 指令,让很多细节都不需要关注。当然,如果你对它的实现感兴趣,可以阅读设计提案和源码。

转载地址:https://mp.weixin.qq.com/s/SiCTV7R2wA_I2nCQkC3GGQ

golang将静态资源文件打包进二进制文件相关推荐

  1. gin embed打包静态资源文件

    问题 在gin项目中如果单纯的只是实现api接口,那打包出来的是一个可执行文件.但如果项目中如果包含一些页面,则必定会引入一些css,jss,html文件.这样会使打包出来后会挂着对应的静态资源文件夹 ...

  2. golang打包HTML为Android,使用Go开发Web服务,并打包html/js/css等静态资源文件

    Go的高性能,使它天生适合开发io方面的服务,Web服务当然不再话下.同时,Go编译后生成的单文件不是字节码,而是对应平台的机器码,因此它效率更高.资源占用更低. 为了更好的进程程序管理,移动程序时更 ...

  3. Golang实践录:静态资源文件整合:初步使用

    趁着五一放假,趁着有时间,把欠的一些技术集中研究研究,写写文章,好给自己一个交待. 本文介绍如何在 Golang 中整合静态资源文件,将静态资源文件编译到二进制可执行文件中,这与其它程序的打包可能是一 ...

  4. iOS架构-静态库.framework之资源文件打包bundle(6)

    iOS架构-静态库.framework手动打包及脚本化打包(5)中介绍了.framework的脚本化打包,虽然在iOS架构-静态库.a打包之资源文件打包成bundle(4)中已经演示过,其实是一样的. ...

  5. Golang实践录:静态资源文件整合:web服务

    趁着五一放假,趁着有时间,把欠的一些技术集中研究研究,写写文章,好给自己一个交待. 本文研究静态资源文件的在 web 服务器的整合. 基础 Golang 中的 web 服务框架有很多种,本文选取 gi ...

  6. 5.15 vs2019 静态编译_Go Web 开发如何优雅的包含静态资源文件?

    点击上方蓝色"Go语言中文网"关注我们,领全套Go资料,每天学习 Go 语言 静态文件,也有人叫资产或资源,是一些被程序使用.没有代码的文件.在 Go 中,这类文件就是非 .go ...

  7. [转]Asp.net 将js文件打包进dll 方法

    Asp.net 将js文件打包进dll 方法 注:本文基于.NET 2.0 和 VS2005 我们在编写 Server Control 的时候难免要用到一些客户端脚本(javascript),如何把脚 ...

  8. vue-cli项目布署问题解决:空白页、静态资源文件404错误、refrenceError:promise未定义(部分浏览器不支持ES6语法)

    (前言:文章记录vue-cli项目打包使用IIS布署遇到的几个错误及解决方式) 首先简单理解webpack打包: 个人理解:项目开发中我们构建 "低耦合高内聚" 的组件/模块来代码 ...

  9. 将xml文件打包进dll(或exe)

    经常,为了安全,我们需要将xml文件加密或者打包进dll文件中,那么如何将xml文件打包进dll中呢?在.net中,为我们提供了很好的方式: 选中该xml文件,将其属性中的"生成操作&quo ...

最新文章

  1. MYSQL出错代码列表
  2. 包子和饺子之扫地机器人_智能家居篇之扫地机器人
  3. /etc/passwd /etc/shadow 详解
  4. tushare pro接口_利用tushare获取新闻联播文字稿并制作词云
  5. HTML之媒体元素[音频 视频]
  6. 深入浅出 Java 中的包装类
  7. java爬虫jsoup_Java爬虫之利用Jsoup自制简单的搜索引擎
  8. 【转】1.7异步编程:基于事件的异步编程模式(EAP)
  9. input复选框改变样式
  10. c++ vector常用用法总结
  11. AndroidStudio_android通过服务,检测本程序是否已经终止运行_终止运行后发送通知给Http服务器---Android原生开发工作笔记246
  12. 为何说要多用组合少用继承?
  13. PowerDesigner一些小技巧
  14. 17.nginx 的 rewrite 功能
  15. 两个简洁的页面:404和Loading
  16. 步进电机选型的计算方法
  17. csdn怎么查看自己写的文章
  18. axios 最详细封装
  19. 手机电视应走免费之路
  20. FPGA通过JTAG固化到EPCS芯片时无效的一些解决

热门文章

  1. 一文弄懂期权的内在价值和时间价值
  2. 电脑怎么批量把flac转换为mp3
  3. 分享一个易企CMS在线客服
  4. 计算机哪个是易失性存储器,易失性存储器有哪些
  5. 3DMAX里面的烘焙规范(一)
  6. 标题太长用省略号代替
  7. 化妆品电商网站响应式模板
  8. Java Controller自动urlDecode的坑
  9. Linux下结束进程的几种命令,linux下进程命令
  10. 更新中——citavi使用【合集】