4

Go embed 简明教程

 3 years ago
source link: https://colobu.com/2021/01/17/go-embed-tutorial/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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!

package main
import (
_ "embed"
"fmt"
//go:embed hello.txt
var s string
func main() {
fmt.Println(s)

嵌入为byte slice

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

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

嵌入为fs.FS

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

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指令的):

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是支持的,相当于一个:

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

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

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的文件。

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的变量。

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

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嵌入同一个文件,但是它们的值在编译的时候会使用初始化字段中的不同的值:

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可以并发使用。

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

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:

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

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源文件所在的文件夹。

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

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/*

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

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源文件所在的路径,使用*:

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:

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功能,遍历一个文件下的文件和文件夹信息:

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接口,所以可以返回它的子文件夹作为新的文件系统:

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

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

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

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

type FileSystem
func FS(fsys fs.FS) FileSystem
type Handler
func FileServer(root FileSystem) Handler

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

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

text/template和html/template.

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

func ParseFS(fsys fs.FS, patterns ...string) (*Template, error)
func (t *Template) ParseFS(fsys fs.FS, patterns ...string) (*Template, error)
2 comments
@RitterHou
RitterHoucommented3 days ago

哇!真棒👍!go真是越来越好用了!

@yqchilde
yqchildecommentedabout 20 hours ago

充满活力的Go!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK