您好,登錄后才能下訂單哦!
這篇文章給大家介紹golang1.16中怎么內嵌靜態資源,內容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。
首先我們創建一個項目:
mkdir pk && cd pk
go mod init my.mod/pk
go get -u github.com/gobuffalo/packr/v2/... # 安裝庫
go get -u github.com/gobuffalo/packr/v2/packr2 # 安裝資源打包工具
然后我們復制一個png圖片和一個錄屏軟件制造的巨型gif文件進images文件夾,整個項目看起來如下:
然后是我們的代碼:
package main
import (
"fmt"
"github.com/gobuffalo/packr/v2"
)
func main() {
box := packr.New("myBox", "./images") // 創建內嵌資源
data, err := box.Find("screenrecord.gif") // 查找內嵌資源
if err != nil {
log.Fatal(err)
}
fmt.Println(len(data))
}
想要完成資源嵌入,我們需要運行packr2命令,之后直接運行go build即可,順利運行后項目會是這樣:
packr的思路就是將資源文件編碼成合法的golang源文件,然后利用golang把這些代碼化的資源編譯進程序里。這是比較主流的嵌入資源實現方案。
從上面的例子里我們可以看到這類方法有不少缺點:
需要安裝額外的工具
會生成超大體積的生產代碼(是靜態資源的兩倍大,因為需要對二進制數據進行一定的編碼才能正常存儲在go源文件里)
編譯完成的程序體積也是資源文件的兩倍多
程序加載時間長,上圖中程序運行花費了6秒,我們程序是存放在ssd上的,慢是因為庫需要對編碼的資源進行處理
前兩點通過語言內置工具或機制就可以得到解決,而對于后兩點,靜態資源本身在計算機上也是二進制存儲的,重復編碼解碼浪費時間,如果可以直接把資源放進程序里該多好。同時告別了生成代碼還可以讓我們的項目結構更清晰。
所以,golang1.16的官方內置版靜態資源嵌入方案誕生了。
準備工作
golang的embed需要在1.16及之后的版本才能運行,不過我們已經可以自行編譯嘗鮮了(需要電腦已經安裝了穩定版本的golang):
mkdir -p ~/go-next && cd ~/go-next
git clone https://github.com/golang/go
cd go/src && bash ./make.bash
export GOROOT=~/go-next/go
alias newgo=${GOROOT}/bin/go
驗證一下安裝:
$ newgo version
go version devel +256d729c0b Fri Oct 30 15:26:28 2020 +0000 linux/amd64
至此準備工作就結束了。
如何匹配靜態資源
想要嵌入靜態資源,首先我們得利用embed這個新的標準庫。在聲明靜態資源的文件里我們需要引入這個庫。
對于我們想要嵌入進程序的資源,需要使用//go:embed指令進行聲明,注意//之后不能有空格。具體格式如下:
//go:embed pattern // pattern是path.Match所支持的路徑通配符 具體的通配符如下,如果你是在linux系統上,可以用man 7 glob查看更詳細的教程:
通配符 釋義
? 代表任意一個字符(不包括半角中括號)
* 代表0至多個任意字符組成的字符串(不包括半角中括號)
[...]和[!...] 代表任意一個匹配方括號里字符的字符,!表示任意不匹配方括號中字符的字符
[a-z]、[0-9] 代表匹配a-z任意一個字符的字符或是0-9中的任意一個數字
** 部分系統支持,*不能跨目錄匹配,**可以,不過目前個golang中和*是同義詞
我們可以在embed的pattern里自由組合這些通配符。
golang的embed默認的根目錄從module的目錄開始,路徑開頭不可以帶/,不管windows還是其他系統路徑分割副一律使用/。如果匹配到的是目錄,那么目錄下的所有文件都會被嵌入(有部分文件夾和文件會被排除,后面詳細介紹),如果其中包含有子目錄,則對子目錄進行遞歸嵌入。
下面舉一些例子,假設我們的項目在/tmp/proj:
//go:embed images
這是匹配所有位于/tmp/proj/images及其子目錄中的文件
//go:embed images/jpg/a.jpg
匹配/tmp/proj/images/jpg/a.jpg這一個文件
//go:embed a.txt
匹配/tmp/proj/a.txt
//go:embed images/jpg/*.jpg
匹配/tmp/proj/images/jpg下所有.jpg文件
//go:embed images/jpg/a?.jpg
匹配/tmp/proj/images/jpg下的a1.jpg a2.jpg ab.jpg等
//go:embed images/??g/*.*
匹配/tmp/proj/images下的jpg和png文件夾里的所有有后綴名的文件,例如png/123.png jpg/a.jpeg
//go:embed *
直接匹配整個/tmp/proj
//go:embed a.txt
//go:embed *.png *.jpg
//go:embed aa.jpg
可以指定多個//go:embed指令行,之間不能有空行,也可以用空格在一行里寫上對個模式匹配,表示匹配所有這些文件,相當于并集操作
可以包含重復的文件或是模式串,golang對于相同的文件只會嵌入一次,很智能
另外,通配符的默認目錄和源文件所在的目錄是同一目錄,所以我們只能匹配同目錄下的文件或目錄,不能匹配到父目錄。
舉個例子:
.
├── code
│ └── main.go
├── go.mod
├── imgs
│ ├── jpg
│ │ ├── a.jpg
│ │ ├── b.jpg
│ │ └── c.jpg
│ ├── png
│ │ ├── a.png
│ │ ├── b.png
│ │ └── c.png
│ └── screenrecord.gif
└── texts
├── en.txt
├── jp.txt
└── zh.txt
5 directories, 12 files
考慮如上的目錄結構。
在這里的main.go可見的資源只有code目錄及其子目錄里的文件,而imgs和texts里的文件是無法匹配到的。
如何使用嵌入的靜態資源 在了解了如何指定需要的靜態資源之后,我們該學習如何使用它們了,還記得我們前面提到的embed標準庫嗎?
對于一個完整的嵌入資源,代碼中的聲明是這樣的:
//go:embed images
var imgs embed.FS
//go:embed a.txt
var txt []byte
//go:embed b.txt
var txt2 string
一共有三種數據格式可選:
數據類型 說明
[]byte 表示數據存儲為二進制格式,如果只使用[]byte和string需要以import (_ "embed")的形式引入embed標準庫
string 表示數據被編碼成utf8編碼的字符串,因此不要用這個格式嵌入二進制文件比如圖片,引入embed的規則同[]byte
embed.FS 表示存儲多個文件和目錄的結構,[]byte和string只能存儲單個文件
實際上接受嵌入文件數據的變量也可以是string和[]byte的類型別名或基于他們定義的新類型,例如下面的代碼那樣:
type StringAlias = string
//go:embed a.txt
var text1 StringAlias
type NewBytes []byte
//go:embed b.txt
var text2 NewBytes
這一變化是issue 43602中提出的,并在commit ec94701中實現。
下面我們看個更具體例子,目錄結構如下:
$ tree -sh .
.
├── [ 487] embed_fs.go
├── [ 235] embed_img.go
├── [ 187] embed_img2.go
├── [ 513] embed_img_fs.go
├── [ 211] embed_text.go
├── [ 660] embed_text_fs.go
├── [ 30] go.mod
├── [ 0] imgs
│ ├── [ 0] jpg
│ │ ├── [606K] a.jpg
│ │ ├── [976K] b.jpg
│ │ └── [342K] c.jpg
│ ├── [ 0] png
│ │ ├── [4.7M] a.png
│ │ ├── [1.4M] b.png
│ │ └── [1.7M] c.png
│ └── [ 77M] screenrecord.gif
├── [ 98K] macbeth.txt
└── [ 0] texts
├── [ 12] en.txt
├── [ 25] jp.txt
└── [ 16] zh.txt
4 directories, 18 files
目錄包含了一些靜態圖片,一個錄屏文件,一個莎士比亞的麥克白劇本。當然還有我們的測試代碼。
處理單個文件
我們先來看用[]byte和string嵌入單個文件的例子:
package main
import (
"fmt"
_ "embed"
)
//go:embed macbeth.txt
var macbeth string
//go:embed texts/en.txt
var hello string
func main() {
fmt.Println(len(macbeth)) // 麥克白的總字符數
fmt.Println(hello) // Output: Hello, world
}
如你所見,聲明嵌入內容的變量一定要求使用var聲明。我們直接用newgo run embed_txt.go或go build embed_txt.go && ./embed_txt即可完成編譯運行,過程中不會生成任何中間代碼。另外變量是否是公開的(首字母是否大小寫)并不會對資源的嵌入產生影響。
在issue 43216中,基于如下的矛盾golang取消了對本地作用域變量的嵌入資源聲明的支持:
如果嵌入資源只初始化一次,那么每次函數調用都將共享這些資源,考慮到任何函數都可以作為goroutine運行,這會帶來嚴重的潛在風險;
如果每次函數調用時都重新初始化,這樣做會產生昂貴的性能開銷。
因此最后golang官方在commit 54198b0中關閉了本地作用域的靜態資源嵌入功能。現在你的代碼應該這樣寫:
+ //go:embed hello.txt
+ var hello string
func Print() {
- //go:embed hello.txt
- var hello string
+ embedString := hello
....
}
再來看看二進制文件的例子,embed_img.go如下所示:
package main
import (
"fmt"
_ "embed"
)
//go:embed imgs/screenrecord.gif
var gif []byte
//go:embed imgs/png/a.png
var png []byte
func main() {
fmt.Println("gif size:", len(gif)) // gif size: 81100466
fmt.Println("png size:", len(png)) // png size: 4958264
}
如果編譯運行這個程序,你會發現二進制文件的大小是89M(不同系統會有差異),比我們之前使用packr創建的要小了許多。
處理多個文件和目錄
下面就要進入本文的重頭戲了,新的標準庫embed的使用。
如果你newgo doc embed的話會發現整個標準庫里只有一個FS類型(之前按提案被命名為Files,后來考慮到用目錄結構組織多個資源更類似新的io/fs.FS接口,故改名),而我們對靜態資源的操作也全都依賴這個FS。下面接著用例子說明:
package main
import (
"fmt"
"embed"
)
//go:embed texts
var dir embed.FS
// 兩者沒什么區別
//go:embed texts/*
var files embed.FS
func main(){
zh, err := files.ReadFile("texts/zh.txt")
if err != nil {
fmt.Println("read zh.txt error:", err)
} else {
fmt.Println("zh.txt:", string(zh))
}
jp, err := dir.ReadFile("jp.txt")
if err != nil {
fmt.Println("read jp.txt error:", err)
} else {
fmt.Println("jp.txt:", string(jp))
}
jp, err = dir.ReadFile("texts/jp.txt")
if err != nil {
fmt.Println("read jp.txt error:", err)
} else {
fmt.Println("jp.txt:", string(jp))
}
}
運行結果:
zh.txt: 你好,世界
read jp.txt error: open jp.txt: file does not exist
jp.txt: こんにちは、世界
我們想讀取單個文件需要用ReadFile方法,它接受一個path字符串做參數,從中查找對應的文件然后返回([]byte, error)。
要注意的是文件路徑必須要明確寫出自己的父級目錄,否則會報錯,因為嵌入資源是按它存儲路徑相同的結構存儲的,和通配符怎么指定無關。
Open是和ReadFile類似的方法,只不過返回了一個fs.File類型的io.Reader,因此這里就不再贅述,需要使用Open還是ReadFile可以由開發者根據自身需求決定。
embed.FS自身是只讀的,所以我們不能在運行時添加或刪除嵌入的文件,fs.File也是只讀的,所以我們不能修改嵌入資源的內容。
如果只是提供了一個查找讀取資源的能力,那未免小看了embed。在golang1.16里任意實現了io/fs.FS接口的類型都可以表現的像是真實存在于文件系統中的目錄一樣,哪怕它其實是在內存里的類map數據結構。因此我們也可以像遍歷目錄一樣去處理embed.FS:
package main
import (
"embed"
"fmt"
)
// 更推薦直接用imgs去匹配
//go:embed imgs/**
var dir embed.FS
// 遍歷當前目錄,有興趣你可以改成遞歸版本的
func printDir(name string) {
// 返回[]fs.DirEntry
entries, err := dir.ReadDir(name)
if err != nil {
panic(err)
}
fmt.Println("dir:", name)
for _, entry := range entries {
// fs.DirEntry的Info接口會返回fs.FileInfo,這東西被從os移動到了io/fs,接口本身沒有變化
info, _ := entry.Info()
fmt.Println("file name:", entry.Name(), "\tisDir:", entry.IsDir(), "\tsize:", info.Size())
}
fmt.Println()
}
func main() {
printDir("imgs")
printDir("imgs/jpg")
printDir("imgs/png")
}
運行結果:
dir: imgs
file name: jpg isDir: true size: 0
file name: png isDir: true size: 0
file name: screenrecord.gif isDir: false size: 81100466
dir: imgs/jpg
file name: a.jpg isDir: false size: 620419
file name: b.jpg isDir: false size: 999162
file name: c.jpg isDir: false size: 349725
dir: imgs/png
file name: a.png isDir: false size: 4958264
file name: b.png isDir: false size: 1498303
file name: c.png isDir: false size: 1751934
唯一和真實的目錄不一樣的地方是目錄文件的大小,在ext4等文件系統上目錄會存儲子項目的元信息,所以大小通常不為0。
如果想要內嵌整個module,則在引用的時候需要使用"."這個名字,但除了單獨使用之外路徑里不可以包含..或者.,換而言之,embed.FS不支持相對路徑,把上面的代碼稍加修改:
package main
import (
"fmt"
"embed"
)
//go:embed *
var dir embed.FS
func main() {
printDir(".")
//printDir("./texts/../imgs") panic: open ./texts/../imgs: file does not exist
}
程序輸出:
dir: .
file name: embed_fs.go isDir: false size: 484
file name: embed_img.go isDir: false size: 235
file name: embed_img2.go isDir: false size: 187
file name: embed_img_fs.go isDir: false size: 692
file name: embed_text.go isDir: false size: 211
file name: embed_text_fs.go isDir: false size: 603
file name: go.mod isDir: false size: 30
file name: imgs isDir: true size: 0
file name: macbeth.txt isDir: false size: 100095
file name: texts isDir: true size: 0
因為使用了錯誤的文件名或路徑會在運行時panic,所以要格外小心。(當然//go:embed是在編譯時檢查的,而且同樣不支持相對路徑,同時也不支持超出了module目錄的任何路徑,比如go module在/tmp/proj,我們指定了/tmp/proj2)
你也可以用embed.FS處理單個文件,但我個人認為單個文件就沒必要再多包裝一層了。
由于是golang內建的支持,所以上述的代碼無需調用任何第三方工具,也沒有煩人的生成代碼,不得不說golang對工程控制的把握上還是相當可靠的。
一些陷阱
方便的功能背后往往也會有陷阱相隨,golang的內置靜態資源嵌入也不例外。
隱藏文件的處理 根據2020年11月21日的issue,現在golang在對目錄進行遞歸嵌入的時候會忽略名字以下劃線(_)和點(.)開頭的文件或目錄。這些文件名在部分文件系統中為隱藏文件,issue的提出者認為默認不應該包含這些文件,隱藏文件通常包含對程序來說沒有意義的元數據,或是用戶的隱私配置,除非明確聲明,否則嵌入資源中包含隱藏文件是不妥的。
舉個例子,假設我們有個images文件夾,底下有a.jpg,.b.jpg兩個常規文件,以及_photo_metadata和pngs兩個子目錄,根據最新的commit,以下的嵌入資源指令的效果如注釋中的解釋:
//go:embed images var images embed.FS // 不包含.b.jpg和_photo_metadata目錄
//go:embed images/* var images embed.FS // 注意!!!這里包含.b.jpg和_photo_metadata目錄
//go:embed images/.b.jog var bJPG []byte // 明確給出文件名也不會被忽略 注意第二條。使用*相當于明確給出了目錄下所有文件的名字,因此點和下劃線開頭的文件和目錄也會被包含。
當然,隱藏文件不止文件名特殊這么簡單,在部分文件系統上擁有正常文件名的文件通過增加某些flag或者attribute也可以變為隱藏,目前怎么處理此類情況還沒有定論。官方暫且按照社區的習慣使用文件名進行區分。
另外對于*是否應該包含隱藏文件的爭論也沒有停止,官方暫且認為應該包含隱藏文件,這點要多加注意。
資源是否應該被壓縮
靜態資源嵌入的提案被接受后爭論最多的就是是否應該對資源采取壓縮,壓縮后的資源更緊湊,不會浪費太多存儲空間,特別是一些大文本文件。同時更大的程序運行加載時間越長,cpu緩存利用率可能會變低。
而反對意見認為壓縮和運行時的解壓一個浪費編譯的時間一個浪費運行時的效率,在用戶沒有明確指定的情況下用戶需要為自己不需要的功能花費代價。
目前官方采用的實現是不壓縮嵌入資源,并預計在后續版本加入控制是否啟用壓縮的選項。
而真正的陷阱是接下來的內容。
潛在的嵌入資源副本
前文中提到過重復的匹配和相同的文件golang會自動只保留一份在變量中。沒錯,然而這是針對同一個變量的多個匹配說的,如果考慮下面的代碼:
package main
import (
_ "embed"
"fmt"
)
//go:embed imgs/screenrecord.gif
var b []byte
//go:embed imgs/screenrecord.gif
var a []byte
func main() {
fmt.Printf("a: %p %d\n", &a, len(a))
fmt.Printf("b: %p %d\n", &b, len(b))
}
猜猜輸出是什么:
a: 0x9ff5a50 81100466
b: 0x9ff5a70 81100466
a和b的地址不一樣!那也沒關系,我們知道slice是引用類型,底層說不定引用了同一個數組呢?那再來看看文件大小:
tree -sh .
.
├── [ 484] embed_fs.go
├── [ 230] embed_img2.go
├── [157M] embed_img2
├── ...
├── [ 0] imgs
│ ├ ...
│ └── [ 77M] screenrecord.gif
├── ...
4 directories, 19 files
程序是資源的兩倍大,這差不多就可以說明問題了,資源被復制了一份。不過從代碼的角度來考慮,a和b是兩個不同的對象,所以引用不同的數據也說的過去,但在開發的時候一定要小心,不要讓兩個資源集合出現交集,否則就要付出高昂的存儲空間代價了。
過大的可執行文件帶來的性能影響 程序文件過大會導致初次運行加載時間的增長,這是眾所周知的。
然而過大的程序文件還可能會降低運行效率。程序需要利用現代的cpu快速緩存體系來提高性能,而更大的二進制文件意味著對于反復運行的熱點功能cpu的快速緩存很可能會面臨更多的緩存失效,因為緩存的大小有限,需要兩次三次的讀取和刷新才能運行完一個熱點代碼片段。這就是為什么幾乎所有的編譯器都會自行指定函數是否會被內聯化而不是把這種控制權利移交給用戶的原因。
然而嵌入靜態文件之后究竟會對性能有多少影響呢?目前缺乏實驗證據,所以沒有定論。
通過修改二進制文件的一部分格式也可以讓代碼部分和資源部分分離從而代碼在cpu看來更加緊湊,當然這么做會不會嚴重破壞兼容,是否真的有用也未可知。
關于golang1.16中怎么內嵌靜態資源就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。