Go 1.16新特性-embed包及其使用

2023-01-31 15:12:11 浏览数 (1)

Go 1.16中增加了embed包,首先来看下提纲。

  • embed是什么
  • 为什么需要embed包
  • embed包的常用场景
  • embed的基本使用
  • embed的使用实例-一个简单的静态web服务
  • embed使用中的注意事项

01

embed是什么

embed是在Go 1.16中新加包。它通过//go:embed指令,可以在编译阶段将静态资源文件打包进编译好的程序中,并提供访问这些文件的能力。

02

为什么需要embed包

以下是我们使用embed包的一些原因:

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

最常见的方法(例如静态网站的后端程序)要求将程序连同其所依赖的html模板,css,js和图片以及静态资源的路径一起上传到生产服务器。必须正确配置Web服务器,以便用户访问它。

现在,我们将所有这些资源都嵌入到程序中。我们只需要部署一个二进制文件并为程序本身配置它们即可。部署过程已大大简化。

03

embed的常用场景

以下列举一些静态资源文件需要被嵌入到程序中的常用场景:

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

04

embed的基本使用

终于,进到embed包的具体使用环节了。embed包是golang 1.16中的新特性,所以,请确保你的golang环境已经升级到了1.16版本。

embed的基本语法

基本语法非常简单,首先导入embed包,然后使用指令//go:embed 文件名 将对应的文件或目录结构导入到对应的变量上。

例如:在当前目录下新建文件 version.txt,并在文件中输入内容:0.0.1

代码语言:javascript复制
package main

import (
    _ "embed"
    "fmt"
)

//go:embed version.txt
var version string

func main() {
    fmt.Printf("version: %qn", version)
}

embed可嵌入的三种数据类型及使用

在embed中,可以将静态资源文件嵌入到三种类型的变量,分别为:字符串、字节数组、embed.FS文件类型。

  • 将文件内容嵌入到字符串变量中,如下代码第7、8行:
代码语言:javascript复制
package main

import (
    _ "embed"
    "fmt"
)
//go:embed version.txt
var version string

func main() {
    fmt.Printf("version %qn", version)
}

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

代码语言:javascript复制
package mainimport (    _ "embed"    "fmt")//go:embed “version info".txtvar versionByte []bytefunc main() {    fmt.Printf("version %qn", string(versionByte))}

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

如代码第8行所示:

代码语言:javascript复制
package main

import (
    _ "embed"
    "fmt"
)

//go:embed version.txt info.txt
var version string

func main() {
    fmt.Printf("version %qn", version)
}

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

代码语言:javascript复制
sh-3.2# go run .
# demo
./main.go:8:5: invalid go:embed: multiple files for type string
  • 软链接&硬链接

嵌入指令是否支持嵌入文件的软链接呢 ?如下:在当前目录下创建一个指向version.txt的软链接 v

代码语言:javascript复制
ln -s version.txt v
代码语言:javascript复制
package main

import (
    _ "embed"
    "fmt"
)
//go:embed v
var version string
func main() {
    fmt.Printf("version %qn", version)
}

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

代码语言:javascript复制
sh-3.2# go run .# demomain.go:8:12: pattern v: cannot embed irregular file vsh-3.2#

结论://go:embed指令不支持文件的软链接

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

代码语言:javascript复制
sh-3.2# rm v
sh-3.2# ln version.txt h
代码语言:javascript复制
import (
    _ "embed"
    "fmt"
)
//go:embed v
var version string

func main() {
    fmt.Printf("version %qn", version)
}

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

代码语言:javascript复制
sh-3.2# go run .version 0.0.1

结论://go:embed指令支持文件的硬链接。因为硬链接本质上是源文件的一个拷贝。

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

代码语言:javascript复制
package main

import (
    _ "embed"
    "fmt"
)

//go:embed v
var version string = ""

func main() {
    fmt.Printf("version %qn", version)
}

运行程序,得到error结果:

代码语言:javascript复制
sh-3.2# go run ../main.go:12:3: go:embed cannot apply to var with initializersh-3.2#

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

  • 将文件内容嵌入到字节数组变量中,如下代码中的第7、8行:
代码语言:javascript复制
package main

import (
    _ "embed"
    "fmt"
)
//go:embed version.txt
var versionByte []byte

func main() {
    fmt.Printf("version %qn", string(versionByte))
}
  • 将文件目录结构映射成embed.FS文件类型

使用embed.FS类型,可以读取一个嵌入到embed.FS类型变量中的目录和文件树,这个变量是只读的,所以是线程安全的。

embed.FS结构主要有3个对外方法,如下:

代码语言:javascript复制
// 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)
  • 读取单个文件
代码语言:javascript复制
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))
}
  • 读取多个文件

首先,在项目根目录下建立 templates目录,以及在templates目录下建立多个文件,如下:

代码语言:javascript复制
|-templates
|-—— t1.html
|——— t2.html
|——— t3.html
代码语言:javascript复制
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("%qn", template.Name())
    }
}
  • 嵌入多个目录

通过使用多个//go:embed指令,可以在同一个变量中嵌入多个目录。我们在项目根目录下再创建一个cpp目录,在该目录下添加几个示例文件名。如下:

代码语言:javascript复制
|-cpp
|——— cpp1.cpp
|——— cpp2.cpp
|——— cpp3.cpp

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

代码语言:javascript复制
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("%qn", template.Name())
    }
    
    cppFiles, _ := fs.ReadDir(files, “cpp”)
    for _, cppFile := range cppFiles {
        fmt.Printf(“%qn”, cppFile.Name())
    }
}
  • 按正则嵌入匹配目录或文件

只读取templates目录下的txt文件,如下代码第9行所示:

代码语言:javascript复制
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("%qn", template.Name())
    }
}

只读取templates目录下的t2.html和t3.html文件,如下代码第9行所示:

代码语言:javascript复制
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("%qn", template.Name())    
     }
 }

05

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

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

代码语言:javascript复制
|-static
|---js
|------util.js
|---img
|------logo.jpg
|---index.html
代码语言:javascript复制
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 . live 和 go run .

然后在浏览器中运行http://localhost:8888 默认显示static目录下的index.html文件内容。

当然,运行go run . live 和 go run . 的不同之处在于编译后的二进制程序文件在运行过程中是否依赖static目录中的静态文件资源。

以下为验证步骤:

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

若文件内容改变,输出依然是改变前的内容,说明embed嵌入的文件内容在编译后不再依赖于原有静态文件了。

1、运行go run .

2、修改index.html文件内容为 Hello China

3、浏览器输入 http://localhost:8888 查看输出。输出内容为修改之前的Hello World

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

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

1、go run . live

2、修改index.html文件内容为 delete

3、浏览器输入 http://localhost:8888 查看输出。输出修改后的内容:Hello China

06

embed使用中注意事项

  • 在使用//go:embed指令的文件都需要导入 embed包。

例如,以下例子 没有导入embed包,则不会正常运行 。

代码语言:javascript复制
package main

import (
    "fmt"
)

//go:embed file.txt
var s string

func main() {
    fmt.Print(s)
}
  • //go:embed指令只能用在包一级的变量中,不能用在函数或方法级别,像以下程序将会报错,因为第10行的变量作用于属于函数级别:
代码语言:javascript复制
package main

import (
    _ "embed"    
    "fmt"
)

func main() {
    //go:embed file.txt    
    var s string    
    fmt.Print(s)
}
  • 当包含目录时,它不会包含以“.”或“_“开头的文件。

但是如果使用通配符,比如dir/*,它将包含所有匹配的文件,即使它们以“."或"_"开头。请记住,在您希望在Web服务器中嵌入文件但不允许用户查看所有文件的列表的情况下,包含Mac OS的.DS_Store文件可能是一个安全问题。出于安全原因,Go在嵌入时也不会包含符号链接或上一层目录。

0 人点赞