在 Go 中如何使用 go:embed 指令嵌入靜態文件
有時候,將配置文件、模板甚至整個前端應用直接嵌入到 Go 二進制文件中,是一種提高應用部署效率和簡化操作的有效方法。自從 Go 1.16 版本起,Go 語言官方引入了 //go:embed
指令,這使得嵌入靜態資源變得異常簡單而直接。本文將詳細介紹如何在你的 Go 應用中使用這一強大的特性。
什麼是 go:embed
//go:embed
在 Go 1.16 版本中被加入,這也是我接觸 Go 語言的第一個版本。
//go:embed
是一個編譯器指令,能夠在程序編譯時期在 Go 的二進制文件中嵌入任意文件和目錄(除了少數 Go 官方限制不允許嵌入的指定類型文件或目錄,後文會講)。
//go:embed
用法非常簡單,示例如下:
import "embed"
//go:embed hello.txt
var content string
//go:embed hello.txt
var contentBytes []byte
//go:embed hello.txt
var fileFS embed.FS
var data, _ = fileFS.ReadFile("hello.txt")
我們有且僅有 3 種方式可以將一個文件內容嵌入到 Go 變量中。
這 3 種方式各自適用場景如下:
-
對於第 1 種用法,將文件嵌入到
string
中,適合嵌入單個文件(如配置數據、模板文件或一段文本)。 -
對於第 2 種用法,將文件嵌入到
[]byte
中,適合嵌入單個文件(如二進制文件:圖片、字體或其他非文本數據)。 -
對於第 3 種用法,將文件嵌入到
embed.FS
中,適合嵌入多個文件或整個目錄(embed.FS
是一個只讀的虛擬文件系統)。
//go:embed
指令語法格式爲://go:embed patterns
,其中 patterns
可以是文件名、目錄名或 path.Match
所支持的路徑通配符。
值得注意的是://go:embed
指令僅接受相對於包含 Go 源文件的目錄的路徑,即當前程序源碼所在目錄,不能直接嵌入父目錄文件內容(後文會講解解決方案)。
並且,//go:embed
指令要緊挨着寫在被嵌入文件的變量上面,類似註釋,//go:embed
是固定寫法,字符中間不能含有任何空格。比如 // go:embed
這種寫法是不能被解析的。
此外,//go:embed
指令需要配合 embed
包一起使用。
快速開始
下面我們來通過一個示例程序,演示下 //go:embed
的使用。
準備如下項目目錄結構:
$ tree -a getting-started
getting-started
├── file
│ ├── hello1.txt
│ ├── hello2.txt
│ └── sub
│ └── sub.txt
├── go.mod
├── hello.txt
└── main.go
3 directories, 6 files
在 main.go
中編寫示例代碼如下:
package main
import (
"embed"
"fmt"
"io"
"io/fs"
)
//go:embed hello.txt
var content string
//go:embed hello.txt
var contentBytes []byte
//go:embed file
var fileFS embed.FS
//go:embed file/hello1.txt
//go:embed file/hello2.txt
var helloFS embed.FS
func main() {
fmt.Printf("hello.txt content: %s\n", content)
fmt.Printf("hello.txt content: %s\n", contentBytes)
// NOTE: embed.FS 提供了 ReadFile 功能,可以直接讀取文件內容,文件路徑需要指明父目錄 `file`
hello1Bytes, _ := fileFS.ReadFile("file/hello1.txt")
fmt.Printf("file/hello1.txt content: %s\n", hello1Bytes)
// NOTE: embed.FS 提供了 ReadDir 功能,通過它可以遍歷一個目錄下的所有信息
dir, _ := fs.ReadDir(fileFS, "file")
for _, entry := range dir {
info, _ := entry.Info()
fmt.Printf("%+v\n", struct {
Name string
IsDir bool
Info struct {
Name string
Size int64
Mode fs.FileMode
}
}{
Name: entry.Name(),
IsDir: entry.IsDir(),
Info: struct {
Name string
Size int64
Mode fs.FileMode
}{Name: info.Name(), Size: info.Size(), Mode: info.Mode()},
})
}
// NOTE: embed.FS 實現了 io/fs.FS 接口,可以返回它的子文件夾作爲新的 io/fs.FS 文件系統
subFS, _ := fs.Sub(helloFS, "file")
hello2F, _ := subFS.Open("hello2.txt")
hello2Bytes, _ := io.ReadAll(hello2F)
fmt.Printf("file/hello2.txt content: %s\n", hello2Bytes)
}
示例程序中,將 hello.txt
分別嵌入到 content string
、contentBytes []byte
兩個變量。
我們可以通過 //go:embed 目錄名
的方式,將 file
目錄嵌入到 fileFS embed.FS
文件系統。
對於 embed.FS
文件系統,我們可以連續寫上多個 //go:embed
指令,來嵌入多個文件到 helloFS embed.FS
。
並且,對於 helloFS embed.FS
這段代碼:
//go:embed file/hello1.txt
//go:embed file/hello2.txt
var helloFS embed.FS
還有另一種寫法:
//go:embed file/hello1.txt file/hello2.txt
var helloFS embed.FS
二者等價。
在 main
函數中,首先對 content string
和 contentBytes []byte
的字符串格式內容進行了打印。
接着,使用 embed.FS
提供的 ReadFile
方法讀取 file/hello1.txt
文件內容(注意:文件路徑必須要指明父目錄 file
,否則會找不到 hello1.txt
文件)並打印。
此外,embed.FS
還提供了 fs.ReadDir
方法可以讀取指定目錄下所有文件信息。
最後,由於 embed.FS
實現了 io/fs.FS
接口,我們可以使用 fs.Sub
獲取指定目錄的子文件系統,然後就可以用 subFS.Open("hello2.txt")
方式直接讀取 hello2.txt
內容(無需再指明父目錄)並打印了。
執行示例代碼,輸出結果如下:
$ go run main.go
hello.txt content: Hello World!
hello.txt content: Hello World!
file/hello1.txt content: Hello1!
{Name:hello1.txt IsDir:false Info:{Name:hello1.txt Size:7 Mode:-r--r--r--}}
{Name:hello2.txt IsDir:false Info:{Name:hello2.txt Size:7 Mode:-r--r--r--}}
{Name:sub IsDir:true Info:{Name:sub Size:0 Mode:dr-xr-xr-x}}
file/hello2.txt content: Hello2!
在 HTTP Server 中使用 go:embed
由於 //go:embed
的核心功能就是將靜態文件嵌入到 Go 程序中,所以 //go:embed
存在兩個最常見的使用場景:一個是託管靜態資源服務器,另一個是解決單元測試文件依賴問題。
我們先來看下 //go:embed
在靜態資源服務中的應用。
準備如下項目目錄結構:
$ tree -a http
http
├── go.mod
├── main.go
├── static
│ ├── css
│ │ └── style.css
│ ├── hello.txt
│ ├── html
│ │ └── index.html
│ ├── img
│ │ └── subscribe.jpeg
│ └── js
│ └── script.js
└── template
└── email.tmpl
7 directories, 8 files
其中,static
是典型的靜態資源目錄,裏面存儲了前端三劍客 HTML
、CSS
和 JavaScript
文件。內容分別如下:
http/static/html/index.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="../css/style.css">
<script src="../js/script.js"></script>
<title>Hello World</title>
</head>
<body>
<h1>關注我:Go編程世界</h1>
<div><img src="../img/subscribe.jpeg" alt="Go編程世界"></div>
</body>
</html>
http/static/css/style.css
:
h1 {
text-align: center;
color: #2c49a7;
}
div {
display: flex;
justify-content: center;
}
img {
width: 200px;
}
http/static/js/script.js
:
window.onload = function () {
let body = document.querySelector("body")
body.style.background = "#f3f3f3";
}
template
是模板資源目錄,使用 Go template
語法。email.tmpl
存儲郵件模板,內容如下:
http/template/email.tmpl
:
<pre>
尊敬的用戶:
您好,歡迎您使用 XXX 服務!
您的 XXX 賬號是:<b>{{ .Username }}</b>
請您點擊下面的鏈接完成郵箱驗證:
<a href="{{ .ConfirmURL }}">{{ .ConfirmURL }}</a>
若以上鍊接無法點擊,請將該鏈接複製到瀏覽器(如 Chrome)的地址欄中訪問。
溫馨提示:
1. 上述鏈接 24 小時內有效並且在激活過一次後失效。若驗證鏈接失效,請登錄網站 <a href="https://jianghushinian.cn">https://jianghushinian.cn</a> 重新驗證;
2. 如果您沒有註冊過 XXX 賬號,請您忽略此郵件,由此給您帶來的不便敬請諒解。
- 江湖十年
(這是一封自動產生的 Email,請勿回覆)
</pre>
在 main.go
中編寫示例代碼如下:
package main
import (
"embed"
"io/fs"
"net/http"
"sync"
"text/template"
)
//go:embed static
var staticFS embed.FS
//go:embed template
var templateFS embed.FS
func main() {
var wg sync.WaitGroup
wg.Add(5)
// NOTE: 在 go:embed 出現之前託管靜態文件服務的寫法
go func() {
defer wg.Done()
http.Handle("/", http.FileServer(http.Dir("static")))
_ = http.ListenAndServe(":8000", nil)
}()
// NOTE: 使用 go:embed 實現靜態文件服務
go func() {
defer wg.Done()
_ = http.ListenAndServe(":8001", http.FileServer(http.FS(staticFS)))
}()
// NOTE: 可以使用 http.StripPrefix 去除靜態文件服務的 `/static/` 路由前綴
go func() {
defer wg.Done()
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))))
_ = http.ListenAndServe(":8002", nil)
}()
// NOTE: 也可以使用 fs.Sub 去除靜態文件服務的 `/static/` 路由前綴
go func() {
defer wg.Done()
fsSub, _ := fs.Sub(staticFS, "static")
_ = http.ListenAndServe(":8003", http.FileServer(http.FS(fsSub)))
}()
// NOTE: text/template 和 html/template 同樣可以從嵌入的文件系統中解析模板,這裏以 text/template 爲例
go func() {
defer wg.Done()
tmpl, _ := template.ParseFS(templateFS, "template/email.tmpl")
http.HandleFunc("/email", func(writer http.ResponseWriter, request *http.Request) {
// 設置 Content-Type 爲 text/html
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
// 執行模板併發送響應
_ = tmpl.ExecuteTemplate(writer, "email.tmpl", map[string]string{
"Username": "江湖十年",
"ConfirmURL": "https://jianghushinian.cn",
})
})
_ = http.ListenAndServe(":8004", nil)
}()
wg.Wait()
}
我們使用 staticFS embed.FS
和 templateFS embed.FS
分別嵌入靜態文件和模版文件目錄。
在 main
函數中,分別啓動了 5 個 goroutine
,用來對比演示使用 //go:embed
實現靜態文件服務的效果。
第 1 個 goroutine
演示了在不使用 //go:embed
的情況下,我們如何來編寫一個靜態資源文件服務。可以直接使用 Go 在 net/http
中提供的 http.FileServer
將一整個目錄作爲靜態資源目錄。http.FileServer
返回一個 http.Handler
可以直接作爲 Web Server 的 Handler 使用。這個服務監聽 8000
端口。
第 2 個 goroutine
演示瞭如何使用 //go:embed
實現靜態資源文件服務。因爲 http.FileServer
參數接收 http/fs.FileSystem
類型,所以不能直接將 embed.FS
傳遞給它。http/fs.FileSystem
接口定義如下:
type FileSystem interface {
Open(name string) (File, error)
}
前文說過 embed.FS
實現了 io/fs.FS
接口,剛好 http
提供了 http.FS
函數可以將 io/fs.FS
類型轉換成 http/fs.FileSystem
。http.FS
定義如下:
// FS converts fsys to a [FileSystem] implementation,
// for use with [FileServer] and [NewFileTransport].
// The files provided by fsys must implement [io.Seeker].
func FS(fsys fs.FS) FileSystem {
return ioFS{fsys}
}
可以發現,我們能夠非常方便的使用 //go:embed
來實現靜態資源文件服務器。與第一種實現方式相比,這種方式在部署時更加簡單,無需考慮靜態資源目錄依賴問題。
不過,使用 //go:embed
也引入了一個新問題,就是在我們訪問 /
路由時,是不會直接顯示 static
目錄下內容的,而需要訪問 /static/
路由纔行。
要解決這個問題也很簡單,可以使用 http.StripPrefix
去除靜態文件服務的 /static/
路由前綴。
即第 3 個 goroutine
中的代碼實現:
go func() {
defer wg.Done()
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))))
_ = http.ListenAndServe(":8002", nil)
}()
另外,我們也可以使用 fs.Sub
去除靜態文件服務的 /static/
路由前綴。
即第 4 個 goroutine
中的代碼實現:
go func() {
defer wg.Done()
fsSub, _ := fs.Sub(staticFS, "static")
_ = http.ListenAndServe(":8003", http.FileServer(http.FS(fsSub)))
}()
最後,第 5 個 goroutine
中使用 template.ParseFS(templateFS, "template/email.tmpl")
來解析郵件模板。之所以 template.ParseFS
函數能直接解析 embed.FS
類型變量,同樣是因爲 embed.FS
實現了 io/fs.FS
接口。
我們來執行下示例程序,看下效果:
$ go run main.go
瀏覽器中訪問 http://127.0.0.1:8000/
效果如下:
:8000
訪問 http://127.0.0.1:8001/static/
效果如下:
:8001
訪問 http://127.0.0.1:8002/
和 http://127.0.0.1:8003/
的效果,與訪問 http://127.0.0.1:8000/
效果相同,你可以自行嘗試。
訪問 http://127.0.0.1:8004
效果如下:
:8004
其實這兩種用法在 embed
的 go doc
中也能看到示例。
執行 go doc embed
命令,能在文檔中找到如下內容:
For example, given the content variable in the example above, we can write:
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(content))))
template.ParseFS(content, "*.tmpl")
在單元測試中使用 go:embed
現在我們來介紹下在單元測試場景中如何使用 //go:embed
。
準備如下項目目錄結構:
$ tree -a test
test
├── go.mod
├── main.go
├── main_test.go
├── main_x_test.go
└── testdata
└── test.txt
2 directories, 5 files
這個示例程序中 main.go
代碼並不重要,只要保證程序編譯通過即可。
main_test.go
代碼如下:
package main
import (
_ "embed"
"testing"
)
//go:embed testdata/test.txt
var testF string
func TestEmbed(t *testing.T) {
t.Log(testF)
}
可以發現,其實在單元測試中,//go:embed
的用法並沒什麼兩樣。
不過在單元測試中,測試依賴的靜態文件通常放到 testdata
目錄下,這是約定俗成的做法。
值得注意的是,示例代碼中使用了匿名導入 import _ "embed"
,//go:embed
指令使用時必須要配合 embed
包一起使用,否則執行程序將得到 go:embed only allowed in Go files that import "embed"
報錯。
main_x_test.go
文件是黑盒測試文件,其包名爲 main_test
,代碼如下:
package main_test
import (
_ "embed"
"testing"
)
//go:embed testdata/test.txt
var testF string
func TestEmbed(t *testing.T) {
t.Log(testF)
}
執行測試代碼,輸出結果如下:
$ go test -v
=== RUN TestEmbed
main_test.go:12: test content
--- PASS: TestEmbed (0.00s)
=== RUN TestEmbed
main_x_test.go:12: test content
--- PASS: TestEmbed (0.00s)
PASS
ok github.com/jianghushinian/blog-go-example/embed/test 0.505s
順便提一下,爲了支持分析 Go 包的工具,通過 //go:embed
指令嵌入的目錄或文件可以在 go list
輸出中找到。
NOTE:
go list
命令是一個強大的工具,用於列出當前模塊中的包或者查詢包的特定屬性。此命令可以顯示關於包的詳細信息,這些信息對於理解包的結構和依賴非常有用。
執行 go help list
命令,可以在輸出中找到如下幾條信息:
// Embedded files
EmbedPatterns []string // //go:embed patterns
EmbedFiles []string // files matched by EmbedPatterns
TestEmbedPatterns []string // //go:embed patterns in TestGoFiles
TestEmbedFiles []string // files matched by TestEmbedPatterns
XTestEmbedPatterns []string // //go:embed patterns in XTestGoFiles
XTestEmbedFiles []string // files matched by XTestEmbedPatterns
對應用法和輸出信息如下所示:
# 所有被直接嵌入的 patterns,包括目錄和文件
$ go list -f '{{.EmbedPatterns}}'
[testdata]
# 嵌入的文件
$ go list -f '{{.EmbedFiles}}'
[testdata/test.txt]
# 測試文件中嵌入的 patterns
$ go list -f '{{.TestEmbedPatterns}}'
[testdata/test.txt]
# 黑盒測試文件中嵌入的 patterns
$ go list -f '{{.XTestEmbedPatterns}}'
[testdata/test.txt]
也可以以 JSON 格式輸出以上信息:go list -json
。
這裏僅演示了在測試文件中使用 //go:embed
的簡單用法,並不是真實案例。你可以在我另一篇文章:《在 Go 語言單元測試中如何解決文件依賴問題》,中找到單元測中使用 //go:embed
的真實案例,並詳細講解了如何使用 //go:embed
解決測試依賴。
使用 go:embed 嵌入父目錄
前文有提到://go:embed
指令僅接受相對於包含 Go 源文件的目錄的路徑,即當前程序源碼所在目錄,不能直接嵌入父目錄文件內容。
現在來講解下如何解決這個問題。
準備如下項目目錄結構:
$ tree -a parent-directory
parent-directory
├── go.mod
├── go.sum
├── internal
│ └── controller
│ └── controller.go
├── main.go
└── template
└── email.tmpl
4 directories, 5 files
這是一個微型的 Web Server 程序,main.go
是程序入口,代碼如下:
package main
import (
"github.com/gin-gonic/gin"
"github.com/jianghushinian/blog-go-example/embed/parent-directory/internal/controller"
)
func main() {
r := gin.Default()
r.POST("/users", (&controller.Controller{}).CreateUser)
_ = r.Run(":8005")
}
示例程序提供了一個 創建用戶
的接口,並監聽 8005
端口。
template/email.tmpl
是郵件模板,內容與前文介紹的一樣。
controller.go
中代碼如下:
package controller
import (
"bytes"
"embed"
"net/http"
"text/template"
"github.com/gin-gonic/gin"
)
// 直接嵌入父目錄
//go:embed ../../template
var templateFS embed.FS
type Controller struct{}
func (ctrl *Controller) CreateUser(c *gin.Context) {
// pretend to create user ...
// send email
tmpl, _ := template.ParseFS(templateFS, "template/*.tmpl")
var buf bytes.Buffer
_ = tmpl.ExecuteTemplate(&buf, "email.tmpl", map[string]string{
"Username": "江湖十年",
"ConfirmURL": "https://jianghushinian.cn",
})
c.Data(http.StatusOK, "text/html; charset=utf-8", buf.Bytes())
}
CreateUser
方法模擬實現了創建用戶並返回郵件的邏輯。
示例代碼中,我們嘗試直接使用 //go:embed ../../template
來嵌入模版文件到變量 templateFS embed.FS
中。
執行示例代碼:
$ go run main.go
internal/controller/controller.go:13:12: pattern ../../template: invalid pattern syntax
可以發現,程序直接報錯了。
我在 issues/46056 中找到了這個問題的解決方案。
korya 提供了一種思路:
//go:generate cp -r ../../assets ./local-asset-dir
//go:embed local-asset-dir
var assetFs embed.FS
我們可以使用 //go:generate
+ //go:embed
的方式來解決此問題。
//go:generate
是一個用於自動化生成 Go 代碼的指令。 在執行程序之前,可以通過 go generate
命令自動執行 //go:generate
指令。
所以這個解決問題的思路是:在執行主程序前,先通過 //go:generate
將父目錄所有文件拷貝到當前目錄,然後再將當前目錄中的文件嵌入 Go 程序。
這個思路可行,但不是一個好的解決方案:
-
這樣做相當於代碼中存儲了兩份依賴文件。
-
我們很容易忘記執行
go generate
命令。
我們確實還有更好的選擇。
既然 //go:embed
不支持父目錄,那麼我們可以換個角度,在 template
目錄下新建一個 Go 文件來嵌入模板文件 email.tmpl
,這樣就變成了從當前目錄嵌入文件。然後在使用時,導入 template
目錄下 Go 文件中嵌入靜態文件的變量即可。
新建 template/template.go
文件,代碼如下:
package template
import "embed"
//go:embed *
var TemplateFS embed.FS
//go:embed
的 patterns
參數不僅不支持 ..
這種父目錄寫法,表示當前目錄的 .
寫法也不支持。不過我們可以使用 *
來嵌入當前目錄下的所有文件。
因爲 TemplateFS
變量需要被外部引用,所以首字母大寫,作爲可導出變量。由此可見,//go:embed
可以將文件嵌入爲 exported
的變量,也可以嵌入爲 unexported
的變量。
現在對 controller.go
文件內容做如下修改:
package controller
import (
...
templatefs "github.com/jianghushinian/blog-go-example/embed/parent-directory/template"
)
...
func (ctrl *Controller) CreateUser(c *gin.Context) {
// pretend to create user ...
// send email
tmpl, _ := template.ParseFS(templatefs.TemplateFS, "*.tmpl") // 記得去掉 template 前綴
...
}
重新執行程序:
$ go run main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] POST /users --> github.com/jianghushinian/blog-go-example/embed/parent-directory/internal/controller.(*Controller).CreateUser-fm (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :8005
根據輸出的日誌信息可知,Web Server 已經成功啓動了。
使用 curl
來創建一個用戶看看效果:
$ curl -X POST http://127.0.0.1:8005/users
<pre>
尊敬的用戶:
您好,歡迎您使用 XXX 服務!
您的 XXX 賬號是:<b>江湖十年</b>
請您點擊下面的鏈接完成郵箱驗證:
<a href="https://jianghushinian.cn">https://jianghushinian.cn</a>
若以上鍊接無法點擊,請將該鏈接複製到瀏覽器(如 Chrome)的地址欄中訪問。
溫馨提示:
1. 上述鏈接 24 小時內有效並且在激活過一次後失效。若驗證鏈接失效,請登錄網站 <a href="https://jianghushinian.cn">https://jianghushinian.cn</a> 重新驗證;
2. 如果您沒有註冊過 XXX 賬號,請您忽略此郵件,由此給您帶來的不便敬請諒解。
- 江湖十年
(這是一封自動產生的 Email,請勿回覆)
</pre>
至此,使用 //go:embed
嵌入父目錄的問題已經被我們解決了。
使用 go:embed 的注意事項
我們已經介紹了 //go:embed
的常見用法。不過還有一些注意事項值得討論。
安全性
我們知道 //go:embed
能夠將文件或目錄嵌入到 3 種類型變量中,分別是 string
、[]byte
和 embed.FS
。其中 string
是 Go 語言本身限制爲只讀,而 []byte
是可以修改的,所以使用時需要注意這一點。對於 embed.FS
,其爲只讀 (read-only
) 集合,是故意這麼設計的。如果聲明中沒有 //go:embed
指令對其進行初始化,那麼 embed.FS
就是一個空文件系統。正因爲 embed.FS
是一個只讀的值,因此它是併發安全的,你可以盡情的在多個 goroutine
中使用它。
patterns
規則
//go:embed patterns
指令的 patterns
參數,不支持嵌入空目錄,並且也不支持嵌入符號鏈接(symbolic links
),即軟鏈接。也不能匹配一些特殊符號:" * < > ? ` ' | / \
。
根據前文的示例程序,patterns
指定爲目錄時,該目錄下的所有文件都會被嵌入到變量中。但是以 .
或 _
開頭的文件是會被忽略的。如果想要嵌入 .
或 _
開頭的文件,可以讓 patterns
以 all:
前綴開頭,如 //go:embed all:testdata
。也可以使用通配符 *
,如 //go:embed testdata/*
,不過 *
不具有遞歸性,子目錄下的 .
或 _
開頭文件不會被嵌入。
比如:
-
image
不會嵌入image/.tempfile
文件。 -
image/*
會嵌入image/.tempfile
文件。 -
以上二者都不會嵌入
image/dir/.tempfile
。 -
all:image
則會同時嵌入image/.tempfile
和image/dir/.tempfile
兩個文件。
注意,使用 //go:embed
嵌入帶有路徑的文件時,目錄分隔符采用正斜槓 /
,如 //go:embed file/hello1.txt
,即使是 Windows 系統也是如此。
patterns
支持使用雙引號 "
或者反引號 `
的方式應用到嵌入的文件名、目錄名或者 pattern
上,這對名稱中帶有 空格
或 特殊字符
很有用。
此外,諸如 .bzr
、.hg
、.git
、.svn
這幾個版本控制管理目錄,始終都不會被嵌入,embed
相關代碼中會做檢查。
不要在 patterns
路徑中包含特殊字符,比如 -
、$
等。不然,根據我的實測結果,你將得到類似 imports embed/testdata: invalid input file name "$not-hidden/fortune.txt"
報錯信息。
path.Match
patterns
支持文件名、目錄名以及 path.Match
所支持的路徑通配符。
對於 path.Match
我來更詳細的講解下。
path.Match
函數簽名如下:
func Match(pattern, name string) (matched bool, err error)
對於匹配 path.Match
規則如下:
功能說明
-
path.Match
函數的功能是:報告參數name
是否符合指定的 shell 模式(pattern
)。 -
這個函數要求模式與整個
name
完全匹配,而不是僅匹配其中的一個子串。
模式語法
-
pattern:由一個或多個
term
構成。 -
term:可以是以下幾種類型:
-
*
匹配任意序列的非/
字符。 -
?
匹配任意單個非/
字符。 -
[character-range]
定義一個字符類,必須是非空的。在字符類內部,如果以^
開頭([^character-range]
),則表示取反,否定後續的字符範圍。 -
單個字符
c
,可以直接匹配字符c
(當c
不是*
、?
、\
、[
中的一個時)。 -
\\c
用於轉義匹配字符c
,\\
用於轉義(例如\\
、\*
、\?
等)。 -
character-range:
-
單個字符
c
,直接匹配字符c
(當c
不是\\
、-
、]
中的一個時)。 -
\\c
用於轉義匹配字符c
。 -
lo-hi
匹配從lo
到hi
之間的任意字符(包括lo
和hi
)。
全局變量
//go:embed
指令只能用在包一級的全局變量中,不能用在函數或方法級別。
這個問題在 issues/43216 中有討論。
大致原因如下:
-
如果嵌入資源只初始化一次,那麼每次函數調用都將共享這些資源,考慮到任何函數都可以作爲
goroutine
運行,這會帶來嚴重的潛在風險; -
如果每次函數調用時都重新初始化,這樣做會產生額外的性能開銷。
雖然 //go:embed
在設計之初是支持函數或方法級別變量的,但基於以上兩點考慮,最終這個功能被移除了。
由於以上這些注意事項過於瑣碎,我就不一一舉例說明了,你可以自行嘗試,也可以先跳過,等遇到問題了再過來查找原因。
總結
在 Go 1.16 版本發佈時加入了 //go:embed
指令。
//go:embed
是一個編譯器指令,能夠在程序編譯時期在 Go 的二進制文件中嵌入任意文件和目錄。這使得在 Go 文件中嵌入靜態資源變得異常簡單而直接。
//go:embed
語法非常簡單://go:embed patterns
。//go:embed
能夠將文件或目錄分別嵌入到 string
、[]byte
和 embed.FS
3 中類型變量中。
//go:embed
最常見的兩個使用場景分別時:託管靜態資源服務器,和解決單元測試文件依賴問題。
我們可以在靜態文件目錄下創建一個 Go 文件,然後使用 //go:embed *
將靜態文件嵌入進來,然後在使用時,導入靜態文件目錄下 Go 文件中嵌入靜態文件的變量。這樣可以實現嵌入 Go 源文件的父目錄內容。
//go:embed
還有很多使用細節值得注意:比如在考慮安全性時需要顧及被嵌入靜態文件的變量是否可變;patterns
中不能隨意包含特殊字符;和當 pattern
爲 path.Match
時的語法規範等。
//go:embed
指令只能用在包一級的全局變量中,不能用在函數或方法級別。
此外,在 embed
的測試文件 源碼 中,有一些寫法我們也可以作爲參考。
本文示例源碼我都放在了 GitHub 中,歡迎點擊查看。
希望此文能對你有所啓發。
延伸閱讀
-
embed Documentation: https://pkg.go.dev/embed@go1.22.0
-
Go 1.16 Release Notes: https://go.dev/doc/go1.16#library-embed
-
issues/46056: https://github.com/golang/go/issues/46056
-
issues/43216: https://github.com/golang/go/issues/43216
-
path.Match Documentation: https://pkg.go.dev/path@go1.22.0#Match
-
embed test: https://github.com/golang/go/blob/go1.22.0/src/embed/internal/embedtest/embed_test.go
-
Go by Example: Embed Directive: https://gobyexample.com/embed-directive
-
Go command support for embedded static assets (files) — Draft Design: https://go.googlesource.com/proposal/+/master/design/draft-embed.md
-
Working with Embed in Go 1.16 Version: https://lakefs.io/blog/working-with-embed-in-go/
-
How to use go:embed in Golang: https://www.educative.io/answers/how-to-use-goembed-in-golang
-
在 Go 語言單元測試中如何解決文件依賴問題: https://jianghushinian.cn/2023/07/19/how-to-resolve-file-dependencies-in-go-testing/
-
本文 GitHub 示例代碼:https://github.com/jianghushinian/blog-go-example/tree/main/embed
聯繫我
-
公衆號:Go 編程世界
-
微信:jianghushinian
-
郵箱:jianghushinian007@outlook.com
-
博客:https://jianghushinian.cn
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/aicfI9dMJscDQruO4kyYLg