1. Golang 1.16 新特性-embed 包及其使用

1.1. embed 是什么

embed//go:embed

1.2. 为什么需要 embed 包

在以前, 很多从其他语言转过来 Go 语言的同学会问到, 或者踩到一个坑。就是以为 Go 语言所打包的二进制文件中会包含配置文件的联同编译和打包。

结果往往一把二进制文件挪来挪去, 就无法把应用程序运行起来了。因为无法读取到静态文件的资源。

无法将静态资源编译打包二进制文件的话, 通常会有两种解决方法:

  • 第一种是识别这类静态资源, 是否需要跟着程序走。
  • 第二种就是将其打包进二进制文件中。
go-bindata/go-bindata

但是在 Go1.16 起, Go 语言自身正式支持了该项特性。

它有以下优点

  • 能够将静态资源打包到二进制包中, 部署过程更简单。传统部署要么需要将静态资源与已编译程序打包在一起上传, 或者使用 docker 和 dockerfile 自动化前者, 这是很麻烦的。
  • 确保程序的完整性。在运行过程中损坏或丢失静态资源通常会影响程序的正常运行。
  • 静态资源访问没有 io 操作, 速度会非常快。

1.3. embed 的常用场景

  • Go 模版: 模版文件必须可用于二进制文件(模版文件需要对二进制文件可用)。对于 Web 服务器二进制文件或那些通过提供 init 命令的 CLI 应用程序, 这是一个相当常见的用例。在没有嵌入的情况下, 模版通常内联在代码中。
  • 静态 web 服务: 有时, 静态文件(如 index.html 或其他 HTML, JavaScript 和 CSS 文件之类的静态文件)需要使用 golang 服务器二进制文件进行传输, 以便用户可以运行服务器并访问这些文件。
  • 数据库迁移: 另一个使用场景是通过嵌入文件被用于数据库迁移脚本。

1.4. embed 的基本用法

embed 包是 golang 1.16 中的新特性, 所以, 请确保你的 golang 环境已经升级到了 1.16 版本。

embed//go:embed
embed_

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

//go:embed FILENAME(S)
[]byteembed.FSgo:embedfiles/*.html**/*.html

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

embedembed.FS
[]byte[]bytestringimport (_ "embed")embedstringembed[]byteembed.FS[]bytestring

1.5. embed 例子

version.txt0.0.1

将文件内容嵌入到字符串变量中

package main
 
import (
    _ "embed"
    "fmt"
)
 
//go:embed version.txt
var version string
 
func main() {
    fmt.Printf("version: %q\n", version)
}

当嵌入文件名的时候, 如果文件名包含空格, 则需要用引号将文件名括起来。如下, 假设文件名是 “version info.txt”, 如下代码第 8 行所示:

package main
 
import (
    _ "embed"
    "fmt"
)
 
//go:embed "version info.txt"
var version string
 
func main() {
    fmt.Printf("version: %q\n", version)
}

将文件内容嵌入到字符串或字节数组类型变量的时候, 只能嵌入 1 个文件, 不能嵌入多个文件, 并且文件名不支持正则模式, 否则运行代码会报错

如代码第 8 行所示:

package main
 
import (
    _ "embed"
    "fmt"
)
 
//go:embed version.txt info.txt
var version string
 
func main() {
    fmt.Printf("version %q\n", version)
}

运行代码, 得到错误提示:

sh-3.2# go run .
# demo
./main.go:8:5: invalid go:embed: multiple files for type string

1.6. 软链接&硬链接

version.txtv
ln -s version.txt v
package main
 
import (
    _ "embed"
    "fmt"
)
//go:embed v
var version string
func main() {
    fmt.Printf("version %q\n", version)
}

运行程序, 得到不能嵌入软链接文件的错误:

sh-3.2# go run .# demomain.go:8:12: pattern v: cannot embed irregular file vsh-3.2#
//go:embed

让我们再来看看文件的硬链接, 如下:

sh-3.2# rm v
sh-3.2# ln version.txt h
import (
    _ "embed"
    "fmt"
)
//go:embed v
var version string
 
func main() {
    fmt.Printf("version %q\n", version)
}

运行程序, 能够正常运行并输出, 如下:

sh-3.2# go run .version 0.0.1
//go:embed

我们能不能将嵌入指令用于 初始化的变量呢? 如下:

package main
 
import (
    _ "embed"
    "fmt"
)
 
//go:embed v
var version string = ""
 
func main() {
    fmt.Printf("version %q\n", version)
}

运行程序, 得到 error 结果:

sh-3.2# go run ../main.go:12:3: go:embed cannot apply to var with initializersh-3.2#

结论: 不能将嵌入指令用于已经初始化的变量上。

将文件内容嵌入到字节数组变量中

package main
 
import (
    _ "embed"
    "fmt"
)
//go:embed version.txt
var versionByte []byte
 
func main() {
    fmt.Printf("version %q\n", string(versionByte))
}

1.7. 将文件目录结构映射成 embed.FS 文件类型

embed.FSembed.FS
embed.FS
// Open 打开要读取的文件, 并返回文件的 fs.File 结构。
func (f FS) Open(name string) (fs.File, error)
 
// ReadDir 读取并返回整个命名目录
func (f FS) ReadDir(name string) ([]fs.DirEntry, error)
 
// ReadFile 读取并返回 name 文件的内容。
func (f FS) ReadFile(name string) ([]byte, error)

1.8. 读取单个文件

package main
 
import (
    "embed"
    "fmt"
    "log"
)
 
//go:embed "version.txt"
var f embed.FS
 
func main() {
    data, err := f.ReadFile("version.txt")
    if err != nil {
        log.Fatal(err)
    }
 
    fmt.Println(string(data))
}

1.9. 读取多个文件

templatestemplates
|-templates
|-—— t1.html
|——— t2.html
|——— t3.html
package main
 
import (
    "embed"
    "fmt"
    "io/fs"
)
 
//go:embed templates/*
var files embed.FS
 
func main() {
    templates, _ := fs.ReadDir(files, "templates")
    
    //打印出文件名称
    for _, template := range templates {
        fmt.Printf("%q\n", template.Name())
    }
}

1.10. 嵌入多个目录

//go:embedcpp
|-cpp
|——— cpp1.cpp
|——— cpp2.cpp
|——— cpp3.cpp

如下代码, 第 9、10 行所示:

package main
 
import (
    "embed"
    "fmt"
    "io/fs"
)
 
//go:embed templates/*
//go:embed cpp/*
var files embed.FS
 
func main() {
    templates, _ := fs.ReadDir(files, "templates")
    
    //打印出文件名称
    for _, template := range templates {
        fmt.Printf("%q\n", template.Name())
    }
    
    cppFiles, _ := fs.ReadDir(files, "cpp")
    for _, cppFile := range cppFiles {
        fmt.Printf("%q\n", cppFile.Name())
    }
}

1.11. 按正则嵌入匹配目录或文件

templatestxt
package main
 
import (
    "embed"
    "fmt"
    "io/fs"
)
 
//go:embed templates/*.txt
var files embed.FS
 
func main() {
    templates, _ := fs.ReadDir(files, "templates")
    
    //打印出文件名称
    for _, template := range templates {
        fmt.Printf("%q\n", template.Name())
    }
}
templatest2.htmlt3.html
package main
 
import (
  "embed"    
  "fmt"    
  "io/fs"
 )
 
 //go:embed templates/t[2-3].txt
 var files embed.FS
 
 func main() {
     templates, _ := fs.ReadDir(files, "templates")
     //打印出文件名称    
     for _, template := range templates {
       fmt.Printf("%q\n", template.Name())    
     }
 }

1.12. 在 http web 中的使用

package main
 
import (
   "embed"
   "net/http"
)
 
//go:embed static
var static embed.FS
 
func main() {
   http.ListenAndServe(":8080", http.FileServer(http.FS(static)))
}
http.FSembed.FShttp.FileServerhttp.FileSystem

1.13. 在模板中的应用

package main
 
import (
   "embed"
   "html/template"
   "net/http"
)
 
//go:embed templates
var tmpl embed.FS
 
func main() {
   t, _ := template.ParseFS(tmpl, "templates/*.tmpl")
   http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
      t.ExecuteTemplate(rw,"index.tmpl",map[string]string{"title":"Golang Embed 测试"})
   })
   http.ListenAndServe(":8080",nil)
}
templateParseFSembed.FS
templates
└── index.tmpl

1.14. Gin 静态文件服务

package main
 
import (
   "embed"
   "github.com/gin-gonic/gin"
   "net/http"
)
 
//go:embed static
var static embed.FS
 
func main() {
   r:=gin.Default()
   r.StaticFS("/",http.FS(static))
   r.Run(":8080")
}
embedhttp.FS

1.15. Gin HTML 模板

package main
 
import (
   "embed"
   "github.com/gin-gonic/gin"
   "html/template"
)
 
//go:embed templates
var tmpl embed.FS
 
//go:embed static
var static embed.FS
 
func main() {
   r:=gin.Default()
   t, _ := template.ParseFS(tmpl, "templates/*.tmpl")
   r.SetHTMLTemplate(t)
   r.GET("/", func(ctx *gin.Context) {
      ctx.HTML(200,"index.tmpl",gin.H{"title":"Golang Embed 测试"})
   })
   r.Run(":8080")
template.ParseFSembedSetHTMLTemplate
http.FSembed.FShttp.FileSystem

1.16. embed 的使用实例-一个简单的静态 web 服务

以下搭建一个简单的静态文件 web 服务为例。在项目根目录下建立如下静态资源目录结构

|-static
|---js
|------util.js
|---img
|------logo.jpg
|---index.html
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)
 }
go run . livego run .
staticindex.html
go run . livego run .static

以下为验证步骤:

首先, 使用编译到二进制文件的方式。

embed
go run .index.htmlHello Chinahttp://localhost:8888Hello World

其次, 使用普通的文件方式。

若文件内容改变, 输出的内容也改变, 说明编译后依然依赖于原有静态文件。

go run . liveindex.htmldeletehttp://localhost:8888Hello China

1.17. embed 使用中注意事项

//go:embedembed
embed
package main
 
import (
    "fmt"
)
 
//go:embed file.txt
var s string
 
func main() {
    fmt.Print(s)
}
//go:embed
package main
 
import (
    _ "embed"    
    "fmt"
)
 
func main() {
    //go:embed file.txt    
    var s string    
    fmt.Print(s)
}

当包含目录时, 它不会包含以 “.” 或 “_” 开头的文件。

dir/*.DS_Store