Go Embed 简明教程


title: "Go Embed 简明教程"
date: 2021-03-07T14:53:56+08:00
draft: true
tags: ['go']
author: "dadigang"
author_cn: "大地缸"
personal: "http://www.real007.cn"


关于作者

http://www.real007.cn/about

Go embed 简明教程

Go编译的程序非常适合部署,如果没有通过CGO引用其它的库的话,我们一般编译出来的可执行二进制文件都是单个的文件,非常适合复制和部署。在实际使用中,除了二进制文件,可能还需要一些配置文件,或者静态文件,比如html模板、静态的图片、CSS、javascript等文件,如何这些文件也能打进到二进制文件中,那就太美妙,我们只需复制、按照单个的可执行文件即可。

一些开源的项目很久以前就开始做这方面的工作,比如 gobuffalo/packrmarkbates/pkgerrakyll/statikknadh/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!

12345678910111213
package main
import (    _ "embed"   "fmt")
//go:embed hello.txt
var s stringfunc main() {
    fmt.Println(s)
}

嵌入为byte slice

你还可以把单个文件的内容嵌入为slice of byte,也就是一个字节数组。

12345678910111213
package main
import (
        _ "embed"
        "fmt")
//go:embed hello.txt
var b []byte
func main() {
    fmt.Println(b)
}

嵌入为fs.FS

甚至你可以嵌入为一个文件系统,这在嵌入多个文件的时候非常有用。

比如嵌入一个文件:

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))
}

还可以支持模式匹配的方式嵌入,下面的章节专门介绍。

同一个文件嵌入为多个变量

比如下面的例子,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一级的的变量,即使是函数内的局部变量,也都支持嵌入:

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的值在编译时就已经嵌入了,而且虽然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的值。

只读

嵌入的内容是只读的。也就是在编译期嵌入文件的内容是什么,那么在运行时的内容也就是什么。

FS文件系统值提供了打开和读取的方法,并没有write的方法,也就是说FS实例是线程安全的,多个goroutine可以并发使用。

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系统也采用这个模式。

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指令中可以只写文件夹名,此文件夹中除了._开头的文件和文件夹都会被嵌入,并且子文件夹也会被递归的嵌入,形成一个此文件夹的文件系统。

如果想嵌入._开头的文件和文件夹, 比如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))}

文件系统

embed.FS实现了 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())  }}

因为它实现了io/fs.FS接口,所以可以返回它的子文件夹作为新的文件系统:

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

先前,我们提供一个静态文件的服务时,使用:

1
http.Handle("/", http.FileServer(http.Dir("/tmp")))

现在,io/fs.FS文件系统也可以转换成http.FileServer的参数了:

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也可以从嵌入的文件系统中解析模板:

12
func ParseFS(fsys fs.FS, patterns ...string) (*Template, error)func (t *Template) ParseFS(fsys fs.FS, patterns ...string) (*Template, error)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容

  • 概要 64学时 3.5学分 章节安排 电子商务网站概况 HTML5+CSS3 JavaScript Node 电子...
    阿啊阿吖丁阅读 9,096评论 0 3
  • 为什么选择GIT? Git自从2005年问世以来,已经逐步成为本地和分布环境下版本控制的事实标准。Git最早由Li...
    技匠阅读 10,762评论 12 235
  • 目的这篇教程从用户的角度出发,全面地介绍了Hadoop Map/Reduce框架的各个方面。先决条件请先确认Had...
    SeanC52111阅读 1,708评论 0 1
  • 以下内容大多来自 传送门,并根据其它资料以及自己的实际进行了修改整理,感谢原作者无私分享。 桌面环境配置 安装完成...
    FiveStrong阅读 13,545评论 2 20
  • awk简介 awk是一种编程语言,用于在linux/unix下对文本和数据进行处理。数据可以来自标准输入、一个或多...
    yeahuh阅读 3,942评论 0 7