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\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

軟鏈接&硬鏈接

嵌入指令是否支持嵌入文件的軟鏈接呢 ?如下:在當前目錄下創建一個指向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\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))
}

將文件目錄結構映射成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\n", 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\n", template.Name())
    }
    
    cppFiles, _ := fs.ReadDir(files, “cpp”)
    for _, cppFile := range cppFiles {
        fmt.Printf("%q\n", 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\n", 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\n", 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在嵌入時也不會包含符號鏈接或上一層目錄。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM