Go1.16引入新的//go:embed指令,可以在编译时嵌入文件和目录,并对其进行访问。通过它,真正做到部署时只有一个二进制文件。

背景:2021-02-16,Go Team正式发布了Go1.16。该版本包含下面的一些重要变化:

  • embed 包和 //go:embed 指令
  • 增加对 macOS ARM64 的支持
  • 默认启用 Module
  • io/fs 包
  • 弃用io/ioutil

最后,还有许多其他改进和错误修复,包括构建速度提高了 20-25%,linux/amd64上内存使用量减少了 5-15%。有关更改的完整列表以及有关上述改进的更多信息,请参阅 Go 1.16 发行说明

01 基本使用

基本思路是,在代码中添加特殊注释,Go将知道其包含的一个或多个文件。

Go源文件在引入“embed”包后,可以使用//go:embed指令在编译时,从包目录或者子目录的文件读取内容来初始化

string,[]byte,FS类型的变量。如,可以用下面的三种方式嵌入名为hello.txt文件,然后在运行时打印其内容。

目录结构:

1
2
3
.
├── main.go
└── hello.txt
  • 将文件内容嵌入到一个字符串变量
1
2
3
4
5
6
7
8
import (
  _ "embed"
  "fmt"
)

//go:embed hello.txt
var s string
fmt.Print(s)
  • 将文件内容嵌入到[]byte
1
2
3
4
5
6
7
8
import (
  _ "embed"
  "fmt"
)

//go:embed hello.txt
var b []byte
fmt.Print(string(b))
  • 将一个或多个文件嵌入到文件系统中
1
2
3
4
5
6
7
8
9
import (
  _ "embed"
  "fmt"
)

//go:embed hello.txt
var f embed.FS
data, _ := f.ReadFile("hello.txt")
fmt.Print(string(data))

02 可能使用方案

版本信息

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

对于更复杂的示例,我们甚至可以根据是否将某个构建标记传递给go工具来有条件地包含版本信息。

1
2
3
4
5
6
// version_dev.go
// +build !prod

package main

var version string = "dev"
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// version_prod.go
// +build prod

package main

import (
    _ "embed"
)

//go:embed version.txt
var version string
1
2
3
4
5
$ go run .
Version "dev"

$ go run -tags prod .
Version "0.0.1"

Quine

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import (
    _ "embed"
    "fmt"
)

//go:embed quine.go
var src string

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

当运行时,就可以将自己打印出来。

Web资源文件

可以网站所需要的所有静态文件或者模板包含在一个可执行文件中,甚至可以通过命令行参数,在读取磁盘文件和读取嵌入文件之间进行切换。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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)
}

03 注意事项

关于嵌入有些地方需要注意,首先必须要将包导入到任何使用embed命令的文件中。如没有导入包时:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import (
    "fmt"
)

//go:embed file.txt
var s string

func main() {
    fmt.Print(s)
}
1
2
$ go run mian.go 
main.go:7:3: //go:embed only allowed in Go files that import "embed"

其次,embed命令只能在包级别变量使用,不能用在函数或方法中。

更多:使用规则,详见文档:https://golang.org/pkg/embed/

相关代码在https://github.com/bytedaring/embed

04 参考资源

  1. Go 1.16 Release Notes
  2. embed Doc
  3. How to Use //go:embed
  4. Managing Go installations
  5. 来了来了!Go1.16 重磅发布