Go embed 简明教程

2021/03/22 17:48
阅读数 76

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!

 
1
2
3
4
5
6
7
8
9
10
package main
import (
     _ "embed"
     "fmt"
)
//go:embed hello.txt
var s string
func main ( ) {
     fmt . Println ( s )
}

嵌入为byte slice

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

 
1
2
3
4
5
6
7
8
9
10
package main
import (
     _ "embed"
     "fmt"
)
//go:embed hello.txt
var b [ ] byte
func main ( ) {
     fmt . Println ( b )
}

嵌入为fs.FS

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

 
1
2
3
4
5
6
7
8
9
10
11
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指令的):

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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是支持的,相当于一个:

 
1
2
3
4
5
6
7
8
9
10
11
12
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 ) )
}

还可以嵌入子文件夹下的文件:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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的文件。

 
1
2
3
4
5
6
7
8
9
10
11
12
13
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的变量。

 
1
2
3
4
5
6
7
8
9
10
11
12
13
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一级的的变量,即使是函数内的局部变量,也都支持嵌入:

 
1
2
3
4
5
6
7
8
9
10
11
12
package main
import (
     _ "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嵌入同一个文件,但是它们的值在编译的时候会使用初始化字段中的不同的值:

 
1
2
3
4
5
6
7
8
9
10
11
12
0x0021 00033 ( / Users / . . . . . . / main . go : 10 )          MOVQ      "" . embed . 1 ( SB ) , AX
0x0028 00040 ( / Users / . . . . . . / main . go : 10 )          MOVQ      "" . embed . 1 + 8 ( SB ) , CX
0x002f 00047 ( / Users / . . . . . . / main . go : 13 )          MOVQ      "" . embed . 2 ( SB ) , DX
0x0036 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可以并发使用。

 
1
2
3
4
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指令支持嵌入多个文件

 
1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import (
     "embed"
     "fmt"
)
//go:embed hello.txt 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:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 ) )
}

支持文件夹

文件夹分隔符采用正斜杠/,即使是windows系统也采用这个模式。

 
1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import (
     "embed"
     "fmt"
)
//go:embed p
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 ) )
}

使用的是相对路径

相对路径的根路径是go源文件所在的文件夹。

支持使用双引号"或者反引号的方式应用到嵌入的文件名或者文件夹名或者模式名上,这对名称中带空格或者特殊字符的文件文件夹有用。

 
1
2
3
4
5
6
7
8
9
10
11
package main
import (
     "embed"
     "fmt"
)
//go:embed "he llo.txt" `hello-2.txt`
var f embed . FS
func main ( ) {
     data , _ : = f . ReadFile ( "he llo.txt" )
     fmt . Println ( string ( data ) )
}

匹配模式

go:embed指令中可以只写文件夹名,此文件夹中除了._开头的文件和文件夹都会被嵌入,并且子文件夹也会被递归的嵌入,形成一个此文件夹的文件系统。

如果想嵌入._开头的文件和文件夹, 比如p文件夹下的.hello.txt文件,那么就需要使用*,比如go:embed p/*

*不具有递归性,所以子文件夹下的._不会被嵌入,除非你在专门使用子文件夹的*进行嵌入:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import (
     "embed"
     "fmt"
)
//go:embed p/*
var f embed . FS
func 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源文件所在的路径,使用*:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import (
     "embed"
     "fmt"
)
//go:embed *
var f embed . FS
func 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:

 
1
2
3
4
5
6
7
8
9
10
11
12
package main
import (
     "embed"
     "fmt"
)
//go:embed *
var f embed . FS
func main ( ) {
     helloFile , _ : = f . Open ( "hello.txt" )
     stat , _ : = helloFile . Stat ( )
     fmt . Println ( stat . Name ( ) , stat . Size ( ) )
}

它还提供了ReadFileh和ReadDir功能,遍历一个文件下的文件和文件夹信息:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import (
     "embed"
     "fmt"
)
//go:embed *
var f embed . FS
func main ( ) {
     dirEntries , _ : = f . ReadDir ( "p" )
     for _ , de : = range dirEntries {
         fmt . Println ( de . Name ( ) , de . IsDir ( ) )
     }
}

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

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import (
     "embed"
     "fmt"
     "io/fs"
     "io/ioutil"
)
//go:embed *
var f embed . FS
func 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的参数了:

 
1
2
3
4
type FileSystem
     func FS ( fsys fs . FS ) FileSystem
type Handler
     func FileServer ( root FileSystem ) Handler

所以,嵌入文件可以使用下面的方式:

 
1
http . Handle ( "/" , http . FileServer ( http . FS ( fsys ) ) )

text/template和html/template.

同样的,template也可以从嵌入的文件系统中解析模板:

 
1
2
func ParseFS ( fsys fs . FS , patterns . . . string ) ( * Template , error )
func ( t * Template ) ParseFS ( fsys fs . FS , patterns . . . string ) ( * Template , error )
展开阅读全文
加载中

作者的其它热门文章

打赏
0
0 收藏
分享
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部