查看原文
其他

Golang 1.16 新特性:embed使用教程

字符串拼接工程师 Go开发大全 2021-07-19

(给Go开发大全加星标)


【导读】golang 1.16版本已经发布,关于新版本的新功能embed如何使用,本文是一篇go官方推荐教程翻译。

Go1.16版本引入了一个新的//go:embed命令,这个命令可以在go应用程序里引入二进制文件和目录。我写了一些例子放在代码仓库里(https://github.com/carlmjohnson/exembed/),验证了//go:embed的功能,本文的例子代码都在这个仓库里。

嵌入的这个基本概念是通过在代码里添加一个特殊的注释实现的,Go会根据这个注释知道要引入哪个或哪几个文件。注释的格式是:

//go:embed FILENAME(S)

FILENAME可以是string类型也可以是[]byte类型,取决于你引入的是单个文件、还是embed.FS类型的一组文件。go:embed命令可以识别Go的文件格式,比如files/*.html这种文件格式也可以识别到(但要注意不要写成**/*.html这种递归的匹配规则)。

文件格式https://pkg.go.dev/path#Match

可以看下官方文档的说明。https://golang.org/pkg/embed/

接下来我们看一些例子了解//go:embed

版本信息

//go:embed让我们很容易就能记录一个version.txt文件的版本信息:

package main

import (
    _ "embed"
    "fmt"
    "strings"
)

var (
    Version string = strings.TrimSpace(version)
    //go:embed version.txt
    version string
)

func main() {
    fmt.Printf("Version %q\n", Version)
}

接下来是一个更复杂的例子。我们还能在代码里按照条件保留版本信息,比如编译时是否传递了某个tag来记录版本:

// version_dev.go
// +build !prod

package main

var version string = "dev"
// version_prod.go
// +build prod

package main

import (
    _ "embed"
)

//go:embed version.txt
var version string

执行命令运行、验证结果:

$ go run .
Version "dev"

$ go run -tags prod .
Version "0.0.1"

Quine

quine是一个程序,它负责打印源代码,我们看个例子:

package main

import (
    _ "embed"
    "fmt"
)

//go:embed quine.go
var src string

func main() {
    fmt.Print(src)
}

运行程序的时候自动就打印了版本信息。

嵌入复杂结构体

如果有一些需要预计算的复杂信息,可以把这些预计算操作保存到go项目里。需要做的就是把数据存储成go可以读取的格式、并在程序启动时加载这些序列化的数据即可:

package main

import (
    "bytes"
    _ "embed"
    "encoding/gob"
    "fmt"
)

var (
    // File value.gob contains some complicated data
    // which we have precomputed and saved.
    //go:embed value.gob
    b []byte
    s = func() (s struct {
        Number   float64
        Weather  string
        Alphabet []string
    })
 {
        dec := gob.NewDecoder(bytes.NewReader(b))
        if err := dec.Decode(&s); err != nil {
            panic(err)
        }
        return
    }()
)

func main() {
    fmt.Printf("s: %#v\n", s)
}

网页文件

这部分是//go:embed主要应用场景。有了embed就可以在一个可执行文件里包含所有静态文件和模板了。还可根据命令行参数让程序去读磁盘上的文件还是去读打包好的、嵌入的文件:

package main

import (
    "embed"
    "io/fs"
    "log"
    "net/http"
    "os"
)

func main() {
    useOS := len(os.Args) > 1 && os.Args[1] == "live"
    http.Handle("/", http.FileServer(getFileSystem(useOS)))
    http.ListenAndServe(":8888"nil)
}

//go:embed static
var embededFiles embed.FS

func getFileSystem(useOS bool) http.FileSystem {
    if useOS {
        log.Print("using live mode")
        return http.FS(os.DirFS("static"))
    }

    log.Print("using embed mode")
    fsys, err := fs.Sub(embededFiles, "static")
    if err != nil {
        panic(err)
    }

    return http.FS(fsys)
}

需要注意,embed.FS的路径前缀需要用fs.Sub去掉,这样才能让os.DirFS成功匹配到。

下面是另一个例子。具体展示了嵌入模板的用法:

package main

import (
    "embed"
    "os"
    "text/template"
)

//go:embed *.tmpl
var tpls embed.FS

func main() {
    name := "en.tmpl"
    if len(os.Args) > 1 {
        name = os.Args[1] + ".tmpl"
    }
    arg := "World"
    if len(os.Args) > 2 {
        arg = os.Args[2]
    }

    t, err := template.ParseFS(tpls, "*")
    if err != nil {
        panic(err)
    }
    if err = t.ExecuteTemplate(os.Stdout, name, arg); err != nil {
        panic(err)
    }
}

英文模板en.tmpl的内容是Hello {{ . }}, how are you today?、日文模板jp.tmpl内容是こんにちは{{ . }}。お元気ですか。,运行程序:

默认(英文):

$ go run ./main.go
Hello World, how are you today?

指定用jp模板:

$ go run ./main.go jp ワールド
こんにちはワールド。お元気ですか。

一些坑

//go:embed需要注意避开一些坑。首先,使用embed 就要在文件里引入embed包,否则会报错,下面这段代码就不能运行:

package main

import (
    "fmt"
)

//go:embed file.txt
var s string

func main() {
    fmt.Print(s)
}

具体报错如下:

$ go run missing-embed.go
# command-line-arguments
./missing-embed.go:8:3: //go:embed only allowed in Go files that import "embed"

通常情况下go编程中是不建议在代码里引入不调用的包的,但是如果引入embed但是又不会在代码里有显示调用,因为前面也讲了这只是个在注释里起作用的东西,这时候就必须用import _ "embed"这样的写法才能正常引入、但是看起来就像是embed没有被调用一样,所以更要注意。

另一个坑是你只能在包的那层用//go:embed注释,不能写进函数、方法里面,下面是一个错误写法的例子:

package main

import (
    _ "embed"
    "fmt"
)

func main() {
    //go:embed file.txt
    var s string
    fmt.Print(s)
}

运行程序报错:

$ go run bad-level.go
# command-line-arguments
./bad-level.go:9:4: go:embed cannot apply to var inside func

第三个坑是引入一个目录时,会忽略掉文件名里._开头的文件。但是如果写了通配符、正则匹配,比如用dir/*这种写法,就会包含._开头的文件了。一定要注意mac os系统的.DS_Store文件可能会被误引用,这对于web服务来说可能会有些安全问题。出于安全考虑,go也不允许用embed功能时引用连接符和查找路径。


embed适用于任何需要的应用,比如你想要让程序命令行应用可以读取README和licence信息,或是把查询数据库的.sql文件都作为嵌入文件存储到应用里,甚至还可以写一个覆盖式FS,以将内置的embed.FS与用户提供的覆盖文件结合起来等等。前面提到的这些都是一些浅显的大概想法,我认为随着时间推移会有更多人提出更好的实践方法的。

go 1.16已经于2021年2月16日发布了,还没升级版本的可以试试升级了。


 - EOF -

推荐阅读(点击标题可打开)

1、对象池 sync.pool 源码解读

2、Golang 闭包到底包了什么?

3、Kubernetes组件介绍


Go 开发大全

参与维护一个非常全面的Go开源技术资源库。日常分享 Go, 云原生、k8s、Docker和微服务方面的技术文章和行业动态。

关注后获取

回复 Go 获取6万star的Go资源库



分享、点赞和在看

支持我们分享更多好文章,谢谢!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存