embed 是什么
embed是在Go 1.16中新加入的包。它通過//go:embed
指令,可以在編譯階段將靜態資源文件打包進編譯好的程序中,並提供訪問這些文件的能力。
為什么需要 embed 包
在以前,很多從其他語言轉過來Go語言的同學會問到,或者踩到一個坑。就是以為Go語言所打包的二進制文件中會包含配置文件的聯同編譯和打包。
結果往往一把二進制文件挪來挪去,就無法把應用程序運行起來了。因為無法讀取到靜態文件的資源。
無法將靜態資源編譯打包二進制文件的話,通常會有兩種解決方法:
- 第一種是識別這類靜態資源,是否需要跟着程序走。
- 第二種就是將其打包進二進制文件中。
第二種情況的話,Go以前是不支持的,大家就會借助各種花式的開源庫,例如:go-bindata/go-bindata
來實現。
但是在Go1.16起,Go語言自身正式支持了該項特性。
它有以下優點
- 能夠將靜態資源打包到二進制包中,部署過程更簡單。傳統部署要么需要將靜態資源與已編譯程序打包在一起上傳,或者使用
docker
和dockerfile
自動化前者,這是很麻煩的。 - 確保程序的完整性。在運行過程中損壞或丟失靜態資源通常會影響程序的正常運行。
- 靜態資源訪問沒有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
中的模板,然后通過Gin
的SetHTMLTemplate
設置后就可以使用了。
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 . live
和 go run .
然后在瀏覽器中運行http://localhost:8888
默認顯示static目錄下的index.html文件內容。
當然,運行go run . live
和 go run .
的不同之處在於編譯后的二進制程序文件在運行過程中是否依賴static目錄中的靜態文件資源。
以下為驗證步驟:
首先,使用編譯到二進制文件的方式。
若文件內容改變,輸出依然是改變前的內容,說明embed嵌入的文件內容在編譯后不再依賴於原有靜態文件了。
- 運行
go run .
- 修改
index.html
文件內容為Hello China
- 瀏覽器輸入
http://localhost:8888
查看輸出。輸出內容為修改之前的Hello World
其次,使用普通的文件方式。
若文件內容改變,輸出的內容也改變,說明編譯后依然依賴於原有靜態文件。
go run . live
- 修改
index.html
文件內容為delete
- 瀏覽器輸入
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在嵌入時也不會包含符號鏈接或上一層目錄。