首页 > 其他分享 >Golang 内嵌静态资源-转

Golang 内嵌静态资源-转

时间:2023-08-14 18:13:27浏览次数:47  
标签:内嵌 嵌入 文件 静态 Golang images go embed resources

转载:https://www.mousemin.com/archives/go-embed-resource/

把静态资源嵌入在程序里,原因无外乎以下几点:

  • 布署程序更简单。传统部署要么需要把静态资源和编译好的程序一起打包上传,要么使用docker和dockerfile自动化.
  • 保证程序完整性。运行中发生静态资源损坏或丢失往往会影响程序的正常运行.
  • 可以自主控制程序需要的静态资源.

最常见的,比如一个混编网址的后端程序,本来需要把程序与它所需要的静态资源(html模版、css、js、图片)一起上传至生产服务器,同时还需要正确配置静态资源在服务器中的路径让程序能正常访问.现在我们将这些资源全部嵌入到程序中,部署的时候只需要部署一个二进制文件,配置也只针对这个程序本身,部署的流程大大简化.

go 1.16前如何内嵌静态资源

go 1.16之前, 我们需要借助第三方工具来实现. 这些工具都是借助代码生成来完成资源的嵌入. 我们拿 go-bindata 举例.

首先我们创建一个项目:

1mkdir embed-demo && cd embed-demo
2go mod init embed/demo
3# 安装打包工具
4go get -u github.com/go-bindata/go-bindata/... 

然后我们复制一个png图片进images文件夹,整个项目看起来如下:

image-20211011151413667

然后是我们的代码

 1package main
 2
 3import "log"
 4
 5//go:generate go-bindata -fs -nomemcopy -pkg=main -ignore="\\.DS_Store|less" -prefix=./images -debug=false -o=images_gen.go ./images/...
 6
 7func main() {
 8	fp, err := Asset("2233.png")
 9	if err != nil {
10		log.Fatal(err)
11		return
12	}
13	log.Print(len(fp))
14}

想要完成资源嵌入,我们需要运行 go generate 命令, 之后直接运行 go build 即可, 顺利运行后项目如下:

image-20211011152308163

go-bindata 的思路就是将资源文件编码成合法的golang源文件,然后利用golang把这些代码化的资源编译进程序里。这是比较主流的嵌入资源实现方案。

从上面的例子我们可以看出这类方法有不少缺点:

  • 需要安装额外工具
  • 会生成超大体积的生产代码(是静态文件的3倍多, 因为需要对二进制文件进行一定的编码才能正常存储在go源文件中)
  • 编译完成的程序体积也锁资源文件的两倍多

golang1.16的官方内置版静态资源

想要嵌入静态资源,首先我们得利用embed这个新的标准库。在声明静态资源的文件里我们需要引入这个库。

对于我们想要嵌入进程序的资源,需要使用//go:embed指令进行声明,注意//之后不能有空格。具体格式如下

1//go:embed pattern
2// pattern是path.Match所支持的路径通配符

具体的通配符如下,

通配符释义
? 代表任意一个字符(不包括半角中括号)
* 代表0至多个任意字符组成的字符串(不包括半角中括号)
[…]和[!…] 代表任意一个匹配方括号里字符的字符,!表示任意不匹配方括号中字符的字符
[a-z]、[0-9] 代表匹配a-z任意一个字符的字符或是0-9中的任意一个数字
** 部分系统支持,不能跨目录匹配,**可以,不过目前个golang中和是同义词

我们可以在 embed 的 pattern 里自由组合这些通配符。

golang的embed默认的根目录从module的目录开始,路径开头不可以带/,不管windows还是其他系统路径分割副一律使用/。如果匹配到的是目录,那么目录下的所有文件都会被嵌入(有部分文件夹和文件会被排除,后面详细介绍),如果其中包含有子目录,则对子目录进行递归嵌入。

下面举一些例子,假设我们的项目在/data/project

 1//go:embed resources
 2这是匹配所有位于/data/project/resources及其子目录中的文件
 3
 4//go:embed resources/images/2233.png
 5匹配/data/project/resources/images/2233.png这一个文件
 6
 7//go:embed a.txt
 8匹配/data/project/a.txt
 9
10//go:embed resources/js/*.min.js
11匹配/data/project/resources/js/下所有 `.min.js` 文件
12
13//go:embed /data/project/resources/images/a?.jpg
14匹配/data/project/resources/images/下的 `a1.jpg` `a2.jpg` `ab.jpg`等
15
16//go:embed resources/images/*.*
17/data/project/resources/images/*.*的文件夹里的所有有后缀名的文件,例如2233.png jpg/a.jpeg
18
19//go:embed *
20直接匹配整个/data/project
21
22//go:embed a.txt
23//go:embed *.png *.jpg
24//go:embed aa.jpg
25可以指定多个//go:embed指令行,之间不能有空行,也可以用空格在一行里写上对个模式匹配,表示匹配所有这些文件,相当于并集操作
26可以包含重复的文件或是模式串,golang对于相同的文件只会嵌入一次,很智能

另外,通配符的默认目录和源文件所在的目录是同一目录,所以我们只能匹配同目录下的文件或目录,不能匹配到父目录。举个例子:

image-20211011161931857

考虑如上的目录结构。 code/main.go可见资源只有 code 目录及其子目录里的文件, 而resources里的文件是无法匹配的.

如何使用嵌入的静态资源

对于一个完整的嵌入资源,代码中的申明是这样的

1//go:embed resources/images/**
2var images embed.FS
3
4//go:embed resources/css/bootstrap.css
5var css []byte
6
7//go:embed resources/texts/zh.txt
8var txt string

一共有三种数据格式可选:

数据类型说明
[]byte 表示数据存储为二进制格式,如果只使用[]byte和string需要以import (_ "embed")的形式引入embed标准库
string 表示数据被编码成utf8编码的字符串,因此不要用这个格式嵌入二进制文件比如图片,引入embed的规则同[]byte
embed.FS 表示存储多个文件和目录的结构,[]byte和string只能存储单个文件

实际上接受嵌入文件数据的变量也可以是string和[]byte的类型别名或基于他们定义的新类型,例如下面的代码那样:

1type StringAlias = string
2
3//go:embed resources/texts/zh.txt
4var txt StringAlias
5
6type NewBytes []byte
7
8//go:embed resources/css/bootstrap.css
9var css NewBytes

这一变化是issue 43602中提出的,并在commit ec94701中实现。

下面我们看个更具体例子,目录结构如下:

image-20211011161931857

目录包含了一些静态图片、javascriptcss文件,一个国际化的文件。当然还有我们的测试代码。

单个文件

我们先来看用[]bytestring嵌入单个文件的例子:

 1package main
 2
 3import (
 4	_ "embed"
 5	"log"
 6)
 7
 8//go:embed resources/css/bootstrap.css
 9var css []byte
10
11//go:embed resources/texts/zh.txt
12var txt string
13
14func main() {
15	log.Println(len(css)) // bootstrap.css文件的总字符数
16	log.Panicln(txt)
17}

如你所见,声明嵌入内容的变量一定要求使用var声明。

我们直接用go run main.gogo build main.go && ./main即可完成编译运行,过程中不会生成任何中间代码。另外变量是否是公开的(首字母是否大小写)并不会对资源的嵌入产生影响。

issue 43216中,基于如下的矛盾golang取消了对本地作用域变量的嵌入资源声明的支持:

  • 如果嵌入资源只初始化一次,那么每次函数调用都将共享这些资源,考虑到任何函数都可以作为goroutine运行,这会带来严重的潜在风险;
  • 如果每次函数调用时都重新初始化,这样做会产生昂贵的性能开销。

因此最后golang官方在commit 54198b0中关闭了本地作用域的静态资源嵌入功能。现在你的代码应该这样写:

1//go:embed resources/texts/zh.txt
2var txt string
3
4func Print() {
5	// //go:embed resources/texts/zh.txt
6	// var txt string
7}

再来看看二进制文件的例子,main.go如下所示:

 1package main
 2
 3import (
 4	_ "embed"
 5	"log"
 6)
 7
 8//go:embed resources/images/2233.png
 9var image []byte
10
11func main() {
12	log.Println(len(image))
13}

如果编译运行这个程序,你会发现二进制文件的大小是4.5M(不同系统会有差异),比我们之前使用go-bindata创建的要小了许多。

多个文件和目录

如果你 go doc embed 的话会发现整个标准库里只有一个 FS 类型(之前按提案被命名为Files,后来考虑到用目录结构组织多个资源更类似新的io/fs.FS接口,故改名),而我们对静态资源的操作也全都依赖这个FS。下面接着用例子说明:

 1package main
 2
 3import (
 4	"embed"
 5	"log"
 6)
 7
 8//go:embed resources/texts/**
 9var txts embed.FS
10
11func main() {
12	zh, err := txts.ReadFile("resources/texts/zh.txt")
13	if err != nil {
14		log.Fatal("read zh.txt error:", err)
15	} else {
16		log.Println("zh.txt: ", string(zh))
17	}
18}

运行结果:

12021/10/11 17:26:26 zh.txt:  other = "分类"

我们想读取单个文件需要用 ReadFile 方法,它接受一个path字符串做参数,从中查找对应的文件然后返回 ([]byte, error)

要注意的是文件路径必须要明确写出自己的父级目录,否则会报错,因为嵌入资源是按它存储路径相同的结构存储的,和通配符怎么指定无关

Open是和ReadFile类似的方法,只不过返回了一个fs.File类型的io.Reader,因此这里就不再赘述,需要使用Open还是ReadFile可以由开发者根据自身需求决定。

embed.FS自身是只读的,所以我们不能在运行时添加或删除嵌入的文件,fs.File也是只读的,所以我们不能修改嵌入资源的内容。

如果只是提供了一个查找读取资源的能力,那未免小看了embed。在golang1.16里任意实现了io/fs.FS接口的类型都可以表现的像是真实存在于文件系统中的目录一样,哪怕它其实是在内存里的类map数据结构。因此我们也可以像遍历目录一样去处理embed.FS:

 1package main
 2
 3import (
 4	"embed"
 5	"log"
 6)
 7
 8//go:embed resources/images/**
 9var images embed.FS
10
11func main() {
12	dirs, err := images.ReadDir("resources/images")
13	if err != nil {
14		log.Fatal(err)
15	}
16	for _, dir := range dirs {
17		info, _ := dir.Info()
18		log.Println("filename: ", info.Name(), "\t isDir: ", info.IsDir(), "\tsize: ", info.Size())
19	}
20}

运行结果:

12021/10/11 17:23:21 filename:  026b232abe59d6e2bce9513cefedc5f70c4db615.jpg      isDir:  false  size:  5391201
22021/10/11 17:23:21 filename:  2233.png          isDir:  false  size:  2663844
32021/10/11 17:23:21 filename:  ae8b83d9598534fa43f791a5ac688fecf0253009.jpg      isDir:  false  size:  3467751
42021/10/11 17:23:21 filename:  gkypn.jpg         isDir:  false  size:  78569

唯一和真实的目录不一样的地方是目录文件的大小,在ext4等文件系统上目录会存储子项目的元信息,所以大小通常不为0。

如果想要内嵌整个module,则在引用的时候需要使用"."这个名字,但除了单独使用之外路径里不可以包含..或者.,换而言之,embed.FS不支持相对路径,把上面的代码稍加修改:

 1package main
 2
 3import (
 4	"embed"
 5	"log"
 6)
 7
 8//go:embed *
 9var files embed.FS
10
11func main() {
12	dirs, err := files.ReadDir(".")
13	if err != nil {
14		log.Fatal(err)
15	}
16	for _, dir := range dirs {
17		info, _ := dir.Info()
18		log.Println("filename: ", info.Name(), "\t isDir: ", info.IsDir(), "\tsize: ", info.Size())
19	}
20}

程序输出

12021/10/11 17:28:59 filename:  .DS_Store         isDir:  false  size:  6148
22021/10/11 17:28:59 filename:  code      isDir:  true   size:  0
32021/10/11 17:28:59 filename:  go.mod    isDir:  false  size:  27
42021/10/11 17:28:59 filename:  main      isDir:  false  size:  4707136
52021/10/11 17:28:59 filename:  main.go   isDir:  false  size:  310
62021/10/11 17:28:59 filename:  resources         isDir:  true   size:  0

因为使用了错误的文件名或路径会在运行时panic,所以要格外小心。(当然 //go:embed 是在编译时检查的,而且同样不支持相对路径,同时也不支持超出了module目录的任何路径,比如go module/data/project,我们指定了/data/project2

一些陷阱

方便的功能背后往往也会有陷阱相随,golang的内置静态资源嵌入也不例外。

隐藏文件的处理

根据2020年11月21日的issue,现在golang在对目录进行递归嵌入的时候会忽略名字以下划线(_)和点(.)开头的文件或目录。这些文件名在部分文件系统中为隐藏文件,issue的提出者认为默认不应该包含这些文件,隐藏文件通常包含对程序来说没有意义的元数据,或是用户的隐私配置,除非明确声明,否则嵌入资源中包含隐藏文件是不妥的。

举个例子,假设我们有个images文件夹,底下有a.jpg.b.jpg两个常规文件,以及_imgsimgs两个子目录,根据commit,以下的嵌入资源指令的效果如注释中的解释:

1//go:embed images
2var images embed.FS // 不包含.b.jpg和_imgs目录
3
4//go:embed images/*
5var images embed.FS // 注意!!! 这里包含.b.jpg和_imgs目录
6
7//go:embed images/.b.jpg
8var bJPG []byte // 明确给出文件名也不会被忽略

注意第二条。使用*相当于明确给出了目录下所有文件的名字,因此点和下划线开头的文件和目录也会被包含。

当然,隐藏文件不止文件名特殊这么简单,在部分文件系统上拥有正常文件名的文件通过增加某些flag或者attribute也可以变为隐藏,目前怎么处理此类情况还没有定论。官方暂且按照社区的习惯使用文件名进行区分。

另外对于*是否应该包含隐藏文件的争论也没有停止,官方暂且认为应该包含隐藏文件,这点要多加注意。

资源是否应该被压缩

静态资源嵌入的提案被接受后争论最多的就是是否应该对资源采取压缩,压缩后的资源更紧凑,不会浪费太多存储空间,特别是一些大文本文件。同时更大的程序运行加载时间越长,cpu缓存利用率可能会变低。

而反对意见认为压缩和运行时的解压一个浪费编译的时间一个浪费运行时的效率,在用户没有明确指定的情况下用户需要为自己不需要的功能花费代价。

目前官方采用的实现是不压缩嵌入资源,并预计在后续版本加入控制是否启用压缩的选项。

会被忽略的目录

前面说过,embed会递归处理目录,出来以下的几个:

  • .bzr
  • .hg
  • .git
  • .svn

这些都是版本控制工具的目录,资源里理应不包含他们,因此是被忽略的。会被忽略的目录列在src/cmd/go/internal/load/pkg.goisBadEmbedName函数里(line: 2094)

注意: .idea不在此列

另外不像隐藏文件可以明确指定嵌入,这些目录你是无法用任何正常手段嵌入的,golang都会忽略他们。

参考资料

 

标签:内嵌,嵌入,文件,静态,Golang,images,go,embed,resources
From: https://www.cnblogs.com/yaoshi641/p/17629374.html

相关文章

  • Golang: 使用embed内嵌资源文件-转
    转载:https://blog.kakkk.net/archives/71/embed介绍首先,embed是 go1.16才有的新特性,使用方法非常简单,通过 //go:embed指令,在打包时将文件内嵌到程序中。官方文档:https://pkg.go.dev/embed快速开始文件结构.├──go.mod├──main.go└──resources└──hello......
  • ubuntu设置静态ip地址
    环境ubuntu版本:22.04jammy【查看指令lsb_release-a】ubuntu17.xx前后,网络配置文件地址不一样查看网卡名称有多种指令可以查看到网卡①ifconfig②ipaddrshow.......无论何种方式,只要带有ip地址的一行,行名称即为网卡名称操作默认转换为root用户操作访问......
  • 医疗设备软件静态和动态分析的 5 个技巧(下)
    上一篇文章医疗设备软件静态和动态分析的5个技巧(上)中我们简单介绍了医疗设备软件关于风险方面的相关背景和两个技巧。这篇文我们将继续介绍剩下的三个技巧,以及如何管理风险。4.动态分析静态分析将源代码解析为文本,并在不执行单个指令的情况下根据解析器输出得出所有结果,而动态应......
  • Golang: 如何交叉编译
    0.golang可以交叉编译出不同操作系统运行的程序1.在macm2架构下,golang程序mian文件所在的主目录下,即可生成#在命令行进入项目根目录,并执行以下命令CGO_ENABLED=0GOOS=xxxGOARCH=xxxgobuild参数说明:CGO_ENABLED:是否使用 C语言 版本的 GO 编译器。0 表示不......
  • golang简单实现CLHLock,不可重入的clh自旋锁
    如果不想自旋,可以把Lock、waitIsFinish和noticeIsFinish代码中的方式2注释掉,改用方式1。不过实际测试在低并发的情况下,自旋的执行效率更高,要根据实际业务场景选择使用哪种方式。源代码如下:import("runtime""sync/atomic")const(Gosched_Spin_Count=10000......
  • 装饰器模式和静态代理模式的区别
    核心点是要增强的对象,代理模式是代理类内部new出来的,装饰器模式是外部传进来的。https://www.bilibili.com/video/BV1Np4y1z7BU?p=74&vd_source=46d50b5d646b50dcb2a208d3946b1598......
  • Nginx配置不记录静态文件、过期时间
    用户访问web网站,通常日志文件会记录很多web站点上的一些静态文件信息,如果长期不处理,日志文件会越来越大,占用的系统资源也越大,此时就需要我们配置不记录静态文件和过期时间,减少日志文件记录过多不必要的内容信息和系统资源占用。1配置1.1编写conf文件[root@host~]#vim/usr/local/......
  • Golang之旅——内存管理
    转载放在最前一文带你了解,虚拟内存、内存分页、分段、段页式内存管理[Golang三关-典藏版]一站式Golang内存洗髓经|Go技术论坛刘丹冰Aceld感谢以上文章作者,收获满满......
  • 静态成员(static)VS extern
    static与extern区别静态成员(static)静态全局变量:限制作用域,只在定义该变量的源文件内有效作用于全局变量时:叫静态全局变量。表示该变量是私有的,只能在该文件使用。不能通过extern关键字对其引用。静态局部变量:改变局部变量的生存周期,使得该变量存在于定义直到程序......
  • RTMP流媒体服务器LntonMedia(免费版)视频直播点播平台采用Golang指针问题导致平台重复推
    我们的团队在研发视频流媒体平台时,广泛应用了Go语言。之前我们也与大家交流过关于Go语言指针的问题和应用。如果你对视频流媒体平台编译中如何运用Go语言指针感兴趣,可以了解一下我们的讨论。在对LntonMedia的编译中,我们发现Golang指针问题会导致系统内的重复推流。Golang遍历切片代......