Aggregator
安全从业人员应该如何选择一家公司
安全从业人员应该如何选择一家公司
安全从业人员应该如何选择一家公司
IOS内核堆风水布局解读
IOS内核堆风水布局解读
IOS内核堆风水布局解读
网络知识复习 - Afant1
Threats, Vulnerabilities, Exploits and Their Relationship to Risk
Threats, Vulnerabilities, Exploits and Their Relationship to Risk
网商银行安全团队招聘(蚂蚁集团)
网商银行安全团队招聘(蚂蚁集团)
网商银行安全团队招聘(蚂蚁集团)
用户安全能力进化模型
用户安全能力进化模型
道理我都懂,但 go embed 究竟该怎么用?
就在前几天,Go 1.16 赶在二月的末尾发布了。
对于这个版本我期待了很久,因为官方终于从语言层面解决了静态文件嵌入的问题—— 加入了 go embed。从此,像 go-bindata、statik、togo 等库都将退出历史的舞台。 同时 Go 1.16 配套的加入了 io/fs 标准库,提供了实现文件系统的接口。同时对 http、embed、os 标准库都加入了对 fs 库的支持。 我记得之前用 togo 做静态资源嵌入时,togo 生成的 .go 文件中是它自己实现了 http/fs 中的 FileSystem 接口,以此实现了一个内部的文件系统。现在可以通过的 io/fs 实现一个基本的文件系统,再通过 http.FS 转换给 http 库使用。可以说 io/fs 库打通了其它标准库中对文件系统转换的需求。
我们常用读写文件的 io/ioutil 库也在 1.16 中做了改动,因为社区反映 ioutil 这个名字模棱两可,遂将 io/ioutil 中的包给废弃了。 具体变动如下:
Before After Discard io.Discard NopCloser io.NopCloser ReadAll io.ReadAll ReadDir os.ReadDir ReadFile os.ReadFile TempDir os.MkdirTemp TempFile os.CreateTemp WriteFile os.WriteFile需要指出的是,上文中我提到的“废弃”,且版本的英文说明用词是Deprecated,但并不意味着 io/ioutil 在未来的 Go 版本中将被移除。 我们仍然可以使用,但是 IDE 会加上横线并提示不推荐使用。Russ Cox 也发推明确说明 io/ioutil 库并不会被“移除”。想想也是,Go 是保证向后兼容的嘛。
以上就是对 Go 1.16 更新的大致介绍,可以看到大多改动都围绕着文件处理。今天想来重点聊聊其中的 go embed,网上关于 go embed 的文章有很多,但是鲜有文章提到 go embed 在我们的实际项目中究竟应该如何使用。
一看就会,一用就废我摸索了挺久才发现一个比较优雅的写法,并成功将 go embed 用到了我前阵子写的 Elaina 中。
在开始介绍之前,我们先来复习一下 go embed 的使用方法、三种数据格式以及对应的注意事项。 go embed 通过注释的形式进行使用。例如:
import ( _ "embed" ) //go:embed readme.md var intro string这样就将 readme.md 文件的内容嵌入到了 intro 变量中。Go 能够允许嵌入的变量类型有如下三种:
变量类型 说明 []byte 用于存储二进制形式的数据,比如图片、富媒体等。 string 用于存储 UTF-8 编码的字符串。 embed.FS 用于嵌入多个文件和目录的结构。如果变量类型有误,程序将在编译期间报错。
需要特别注意的是: `go embed` 仅能嵌入当前目录及其子目录,无法嵌入上层目录。同时也不支持软链接。
更绝的是,go emebd 禁止嵌入如 .git .svn 这些目录,官方认为这些目录不属于 package 的一部分,如果嵌入则会在编译时报错。可参见 Go 源码src/cmd/go/internal/load/pkg.go#L2091-2107
// isBadEmbedName reports whether name is the base name of a file that // can't or won't be included in modules and therefore shouldn't be treated // as existing for embedding. func isBadEmbedName(name string) bool { if err := module.CheckFilePath(name); err != nil { return true } switch name { // Empty string should be impossible but make it bad. case "": return true // Version control directories won't be present in module. case ".bzr", ".hg", ".git", ".svn": return true } return false }我原本还想着通过 go embed 在程序编译时读取 .git/config 配置敏感信息的…… 💔
三种嵌入文件的情况在 Elaina 项目中使用 go emebd 时,我遇到了三种不同的目录结构,这三种目录结构也大致囊括了我们在实际项目会遇到的场景。这里分享一下我的做法。
嵌入多个文件在一个 Web 应用项目中常会有 templates 目录,存放了 HTML 的模板文件,它们常以 .tmpl 或者 .html 作为后缀名。
. ├── sandbox.tmpl └── sandbox_404.tmpl要嵌入这些模板文件,我们可以在 templates 目录下创建一个 fs.go 文件:
package templates import ( "embed" ) //go:embed *.tmpl var FS embed.FS这样就将所有的 .tmpl 后缀的文件嵌入进了 FS 变量中。 后面在路由中使用 html/template 库来从文件系统中加载并解析模板。以下是在 Gin 框架中的示例:
tpl := template.Must(template.New("").ParseFS(templates.FS, "*")) r.SetHTMLTemplate(tpl) 嵌入多个目录一个 Web 应用项目下往往还会有个 public 目录,其用于存储所有的静态资源。目录下会有诸如 css js assets 这样的子目录。
. ├── css │ └── sandbox.css └── js └── sandbox.js这一次是嵌入多个目录,我们可以效仿上面的做法,在 public 目录下创建一个 fs.go 文件:
package public import ( "embed" ) //go:embed css js var FS embed.FS在注册路由时,Gin 的 StaticFS 需要一个实现了 http.fs 中 FileSystem 接口的变量。这里我们使用 http.FS 方法,将 fs.FS 转换成 FileSystem。二者其实都是只需实现 Open(name string) (File, error) 这个方法即可。
r.StaticFS("/static", http.FS(public.FS)) 嵌入子目录有时我们的项目是前后端分离的,需要将打包编译好的前端嵌入进来。编译好的前端往往会在 dist 目录下。
. ├── css │ ├── app.3ca5488f.css │ └── chunk-vendors.08a0794a.css ├── index.html ├── js │ ├── app.1bdd8cf2.js │ ├── app.1bdd8cf2.js.map │ ├── chunk-2d0ac239.c72b0c7d.js │ ├── chunk-2d0ac239.c72b0c7d.js.map ├── manifest.json ├── precache-manifest.a2e4eb7c729e7ecf28ada54a6ea672b4.js └── service-worker.js而我们并不能效仿前两种情况,创建一个 fs.go 文件在 dist 目录下。原因有两点:
- dist 目录往往是写在 .gitignore 中被忽略的。
- dist 中既有文件又有目录,若指定其嵌入 * 的话,fs.go 文件也会被嵌入进来。
因此这里我们将 fs.go 放置于 dist 的父目录中。文件内容还是类似的:
package frontend import ( "embed" ) //go:embed dist var FS embed.FS需要注意的是,若直接使用 `frontend.FS` 注册路由,所有的文件路径都会有 `dist/` 前缀。我们需要通过形如 `http://localhost:8080/dist/index.html` 的地址进行访问,这显然不是我们想要的。 因此,这里需要使用 fs.Sub() 方法,来进入 frontend.FS 的下层文件夹,并返回一个新的 FS。
fe, err := fs.Sub(frontend.FS, "dist") if err != nil { log.Fatal("Failed to sub path `dist`: %v", err) } r.StaticFS("/m", http.FS(fe))其实前两种情形都可以用这第三种 fs.Sub() 进入目录来解决(即分别进入 templates public 目录)。 但这将失去变量 templates.FS public.FS 这些清晰易懂的包名命名。
总结以上就是我摸索出的 go embed 在实际项目中的使用方式。可能不大准确,欢迎大家纠正以及提出你所认为的最佳实践。