zoukankan      html  css  js  c++  java
  • Golang 1.16新特性-embed包及其使用

    embed 是什么

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

    为什么需要 embed 包

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

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

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

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

    第二种情况的话,Go以前是不支持的,大家就会借助各种花式的开源库,例如:go-bindata/go-bindata来实现。

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

    它有以下优点

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

    embed 的常用场景

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

    embed 的基本用法

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

    Go embed的使用非常简单,首先导入embed包,再通过//go:embed 文件名 将对应的文件或目录结构导入到对应的变量上。

    特别注意: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/

    embed可以嵌入的静态资源文件支持三种数据类型:字符串、字节数组、embed.FS文件类型

    数据类型 说明
    []byte 表示数据存储为二进制格式,如果只使用[]byte和string需要以import (_ "embed")的形式引入embed标准库
    string 表示数据被编码成utf8编码的字符串,因此不要用这个格式嵌入二进制文件比如图片,引入embed的规则同[]byte
    embed.FS 表示存储多个文件和目录的结构,[]byte和string只能存储单个文件

    embed例子

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

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

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

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

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

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

    如代码第8行所示:

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

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

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

    软链接&硬链接

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

    ln -s version.txt v
    
    package main
    
    import (
        _ "embed"
        "fmt"
    )
    //go:embed v
    var version string
    func main() {
        fmt.Printf("version %q
    ", 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
    ", 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
    ", 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
    ", string(versionByte))
    }
    

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

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

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

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

    读取单个文件

    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目录下建立多个文件,如下:

    |-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
    ", template.Name())
        }
    }
    

    嵌入多个目录

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

    |-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
    ", template.Name())
        }
        
        cppFiles, _ := fs.ReadDir(files, “cpp”)
        for _, cppFile := range cppFiles {
            fmt.Printf("%q
    ", cppFile.Name())
        }
    }
    

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

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

    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
    ", template.Name())
        }
    }
    

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

    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
    ", template.Name())    
         }
     }
    

    在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.FS这个函数,把embed.FS类型的static转换为http.FileServer函数可以识别的http.FileSystem类型。

    在模板中的应用

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

    template包提供了ParseFS函数,可以直接从一个embed.FS中加载模板,然后用于HTTP Web中。模板文件夹的结构如下所示:

    templates
    └── index.tmpl
    

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

    Gin中使用embed作为静态文件,也是用过http.FS函数转化的。

    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.ParseFS函数先加载embed中的模板,然后通过GinSetHTMLTemplate设置后就可以使用了。

    http.FS函数是一个可以把embed.FS转为http.FileSystem的工具函数

    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 .

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

    当然,运行go run . livego 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

    embed使用中注意事项

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

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

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

    //go:embed指令只能用在包一级的变量中,不能用在函数或方法级别,像以下程序将会报错,因为第10行的变量作用于属于函数级别:

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

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

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

  • 相关阅读:
    时间复杂度和空间复杂度
    七、vue计算属性
    六、vue侦听属性
    四、vue派发更新
    五、vue nextTick
    三、vue依赖收集
    二、vue响应式对象
    递归
    链表
    TypeScript类型定义文件(*.d.ts)生成工具
  • 原文地址:https://www.cnblogs.com/niuben/p/14461973.html
Copyright © 2011-2022 走看看