目錄:
1礁扮、go-build
2太伊、go-embed
3、go-generate
1锰提、go-build:
構(gòu)建約束也稱之為條件編譯立肘,就是可以對(duì)某些源代碼文件指定在特定的平臺(tái)芭概,架構(gòu),編譯器甚至Go版本下面進(jìn)行編譯踢故,在其他環(huán)境中會(huì)自動(dòng)忽略這些文件惹苗。go支持兩種方式的構(gòu)建約束桩蓉,一種是文件后綴名的方式一種是在文件的頭部通過(guò)注釋的方式。
文件后綴名構(gòu)建約束的格式為:格式就是文件名_系統(tǒng)名_架構(gòu)名.go
例如:
user_windows_amd64.go //在 windows 中 amd64 架構(gòu)下才會(huì)編譯洽瞬,其他的環(huán)境中會(huì)自動(dòng)忽略
user_linux_arm.go // 在 linux 中的 arm 架構(gòu)下才會(huì)編譯伙窃,其他環(huán)境中會(huì)自動(dòng)忽略
注釋的構(gòu)建約束則如下規(guī)則:
1)样漆、1.16之前老版本構(gòu)建約束:
go 1.16之前的構(gòu)建約束格式為構(gòu)建約束的語(yǔ)法是// +build這種形式,如果多個(gè)條件組合鳍怨,通過(guò)空格鞋喇、逗號(hào)或多行構(gòu)建約束表示,逗號(hào)表示 AND醒串,空格表示OR芜赌,!表示NOT伴逸,換行表示AND。
// +build linux,386 darwin,!cgo
它表示的意思是:(linux AND 386) OR (darwin AND (NOT cgo)) 洲愤,有些時(shí)候柬赐,多個(gè)約束分成多行書(shū)寫(xiě)官紫,會(huì)更易讀些:
// +build linux darwin
// +build amd64
這相當(dāng)于:(linux OR darwin) AND amd64
2)束世、1.17新版本構(gòu)建約束:
新版的構(gòu)建約束,注意"http://"和"go"之間不能有空格:
//go:build
同時(shí)新版語(yǔ)法使用布爾表達(dá)式沉帮,而不是逗號(hào)贫堰、空格等。布爾表達(dá)式粱檀,會(huì)更清晰易懂,出錯(cuò)可能性大大降低。
采用新語(yǔ)法后渗常,一個(gè)文件只能有一行構(gòu)建語(yǔ)句皱碘,而不是像舊版那樣有多行隐孽。這樣可以避免多行的關(guān)系到底是什么的問(wèn)題菱阵。
Go1.17 中,gofmt 工具會(huì)自動(dòng)根據(jù)舊版語(yǔ)法生成對(duì)應(yīng)的新版語(yǔ)法都办,為了兼容性虑稼,兩者都會(huì)保留蛛倦。比如原來(lái)是這樣的:
// +build !windows,!plan9
執(zhí)行 Go1.17 的 gofmt 后溯壶,變成了這樣:
//go:build !windows && !plan9
// +build !windows,!plan9
如果文件中已經(jīng)有了這兩種約束形式,gofmt 會(huì)根據(jù) //go:buid 自動(dòng)覆蓋 // +build 的形式躲庄,確保兩者表示的意思一致钾虐。如果只有新版語(yǔ)法效扫,不會(huì)自動(dòng)生成舊版的,這時(shí)浩习,你需要注意谱秽,它不兼容舊版本了。
2郊供、go-embed:
//go:embed指令是Go 1.16版本新增的官方編譯指令近哟,它可以將任何文件或者文件夾的內(nèi)容打包到編譯出的可執(zhí)行文件中吉执。
簡(jiǎn)單來(lái)說(shuō),我們可以給代碼添加一行特殊的注釋來(lái)熙掺,Go 編譯器知道是要嵌入文件還是嵌入文件夾适掰。注釋長(zhǎng)得像"http://go:embed 文件名"荠列。注釋占一行肌似,緊接著是一個(gè)變量。如果要嵌入文件力细,變量的類型得是 string 或者 []byte固额,如果要嵌入一組文件斗躏,變量的類型得是embed.FS啄糙。指令格式有三種形式:
- //go:embed path…:path… 是需要嵌入的文件或目錄,可以為多個(gè)沈堡,用空格分隔燕雁;
- //go:embed regexp:regexp 是需要嵌入的文件名或目錄名的正則表達(dá)式;
- //go:embed dir/.ext:dir/.ext 是需要嵌入的某個(gè)目錄下特定擴(kuò)展名的文件崩泡;
注意事項(xiàng):
- //go:embed是Go語(yǔ)言中的指令猬膨,看起來(lái)很像注釋但是并非是注釋,其中//和go:embed兩者之間不能有空格呛伴,必須挨在一起勃痴;
- //go:embed后面接要嵌入的文件路徑,以相對(duì)路徑形式聲明文件路徑热康,文件路徑和//go:embed指令之間相隔一個(gè)空格沛申,這里文件相對(duì)路徑;相對(duì)的是當(dāng)前源代碼文件的路徑姐军,并且這個(gè)路徑不能以/或者./開(kāi)頭铁材;
- 必須要導(dǎo)入embed包才能夠使用//go:embed指令;
- 如果嵌入的文件夾中包含有以.或者_(dá)開(kāi)頭的文件奕锌,這些文件就會(huì)被視為隱藏文件,會(huì)被排除惊暴,不會(huì)被嵌入饼丘;
- 我們還可以使用通配符形式嵌入文件夾,例如://go:embed resource/*辽话,使用通配符形式時(shí)肄鸽,隱藏文件也會(huì)被嵌入,并且文件夾本身也會(huì)被嵌入油啤;
1)典徘、嵌入單個(gè)文件:
例如:假設(shè)我們有一個(gè)文件叫 data.txt,然后我們希望在程序中引用它益咬,通過(guò) //go:embed 指令即可嵌入逮诲。
data.txt文件內(nèi)容如下:
hello go embed!
將data.txt文件嵌入到程序中賦值到data變量中
package main
import (
"embed"
"fmt"
)
//go:embed data.txt
var data string
func main() {
fmt.Println("embed data info:", data)
}
在這個(gè)示例中,我們使用//go:embed data.txt將 data.txt 文件嵌入到了可執(zhí)行文件中础废,在go build時(shí)編譯器會(huì)把data.txt內(nèi)容直接附給變量data汛骂,然后在 main() 函數(shù)中輸出了 data 變量的值。
2)评腺、嵌入多個(gè)文件:
package main
import (
"embed"
"fmt"
)
// 嵌入多個(gè)文件并作為embed.FS類型
// 將當(dāng)前目錄下test.txt和demo.txt嵌入至可執(zhí)行文件帘瞭,并存放到embed.FS對(duì)象中
//go:embed test.txt demo.txt
var embedFiles embed.FS
func main() {
// 讀取嵌入的文件,返回字節(jié)切片
testContent, _ := embedFiles.ReadFile("test.txt")
demoContent, _ := embedFiles.ReadFile("demo.txt")
// 將讀取到的字節(jié)切片轉(zhuǎn)換成字符串輸出
fmt.Println(string(testContent))
fmt.Println(string(demoContent))
}
指令部分并不需要改蒿讥,將接收變量類型改成embed.FS即可蝶念,這樣可以同時(shí)嵌入多個(gè)文件抛腕,在//go:embed指令后接多個(gè)要嵌入的文件路徑即可,多個(gè)文件路徑之間使用空格隔開(kāi)媒殉。最后通過(guò)embed.FS對(duì)象的ReadFile方法担敌,即可讀取指定的嵌入的文件的內(nèi)容,參數(shù)為嵌入的文件名廷蓉,返回讀取到的文件內(nèi)容(byte切片形式)和錯(cuò)誤對(duì)象全封。
所以,我們完全就可以把embed.FS對(duì)象想象成一個(gè)文件夾桃犬,只不過(guò)它是個(gè)特殊的文件夾刹悴,它位于編譯后的可執(zhí)行文件內(nèi)部。那么使用ReadFile函數(shù)讀取文件時(shí)攒暇,也是指定讀取這個(gè)內(nèi)部的文件夾中的文件土匀,上述我們使用//go:embed指令嵌入了兩個(gè)文件,就可以視為這兩個(gè)文件在編譯時(shí)被放入到這個(gè)特殊的“文件夾”中去了形用,只不過(guò)文件放進(jìn)去后文件名是不會(huì)改變的就轧。
3、go-generate:
Go 語(yǔ)言注釋的另一個(gè)有趣的用法是通過(guò) go generate 命令工具生成代碼田度。 go generate 是 Go 語(yǔ)言標(biāo)準(zhǔn)工具包的一部分妒御,它通過(guò)運(yùn)行用戶指定的外部命令以編程方式生成源 (或其他) 文件。go generate 的工作方式是掃描 .go 程序每币,尋找其中包含要運(yùn)行的命令的特殊注釋携丁,然后執(zhí)行它們。
具體來(lái)說(shuō)兰怠,go generate 查找以 go:generate 開(kāi)頭的注釋(注釋標(biāo)記和文本開(kāi)始之間沒(méi)有空格)梦鉴,如下:
//go:generate <command> <arguments>
3.1、generate命令工具使用:
//打印當(dāng)前目錄下所有文件揭保,將被執(zhí)行的命令
$go generate -n ./...
// 對(duì)包下所有Go文件進(jìn)行處理
$go generate github.com/ysqi/repo
// 打印包下所有文件肥橙,將被執(zhí)行的命令
$go generate -n runtime
3.2、go-generate注釋使用:
需在的代碼中配置generate標(biāo)記秸侣,則在執(zhí)行g(shù)o generate時(shí)可被檢測(cè)到存筏。go generate執(zhí)行時(shí),實(shí)際在掃描如下內(nèi)容:
//go:generate command argument...
generate命令不是解析文件味榛,而是逐行匹配以//go:generate 開(kāi)頭的行(前面不要有空格)椭坚。故命令可以寫(xiě)在任何位置,也可存在多個(gè)命令行搏色。
//go:generate后跟隨具體的命令善茎。命令為可執(zhí)行程序,形同在Shell下執(zhí)行频轿。所以命令是在環(huán)境變量中存在垂涯,也可是完整路徑烁焙。如:
package main
import "fmt"
//go:generate echo hello
//go:generate go run main.go
//go:generate echo file=$GOFILE pkg=$GOPACKAGE
func main() {
fmt.Println("main func")
}
// 執(zhí)行后輸出如下結(jié)果:
$ go generate
hello
man func
file=main.go pkg=main
在執(zhí)行g(shù)o generate時(shí)將會(huì)加入些信息到環(huán)境變量,可在命令程序中使用,相關(guān)變量如下:
- $GOARCH:架構(gòu) (arm, amd64, etc.)耕赘;
- $GOOS:linux, windows等等骄蝇;
- $GOFILE:當(dāng)前處理中的文件名;
- $GOLINE:當(dāng)前命令在文件中的行號(hào)操骡;
- $GOPACKAGE:當(dāng)前處理文件的包名九火;
- $DOLLAR:固定的"$",不清楚用途;
3.2.1当娱、使用示例:
比如我們定義一個(gè)對(duì)象后吃既,為了打印友好內(nèi)容,我們經(jīng)常手工定義對(duì)應(yīng)枚舉常量的String方法或映射跨细,用于輸出對(duì)應(yīng)枚舉常量的友好信息。當(dāng)增加一個(gè)枚舉常量的時(shí)候我們都需增加對(duì)應(yīng)的字符映射河质。
type Status int
const (
Offline Status = iota
Online
Disable
Deleted
)
var statusText = []string{"Offline", "Online", "Desable", "Deleted"}
func (s Status) String() string {
v := int(s)
if v < 0 || v > len(statusText) {
return fmt.Sprintf("Status(%d)", s)
}
return statusText[v]
}
這里我們可以使用generate來(lái)生成對(duì)枚舉常量的友好輸出信息冀惭。
1)、編寫(xiě)使用go generate工具根據(jù)注釋生成代碼的go程序:
package main
import (
"bytes"
"flag"
"fmt"
"go/ast"
"go/build"
"go/format"
"go/parser"
"go/token"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"text/template"
)
var (
pkgInfo *build.Package
)
var (
typeNames = flag.String("type", "", "必填掀鹅,逗號(hào)連接的多個(gè)Type名")
)
func main() {
flag.Parse()
if len(*typeNames) == 0 {
log.Fatal("-type 必填")
}
consts := getConsts()
src := genString(consts)
//保存到文件
outputName := "./status2str_gen.go"
if outputName == "" {
types := strings.Split(*typeNames, ",")
baseName := fmt.Sprintf("%s_string.go", types[0])
outputName = filepath.Join(".", strings.ToLower(baseName))
}
err := ioutil.WriteFile(outputName, src, 0644)
if err != nil {
log.Fatalf("writing output: %s", err)
}
}
func getConsts() map[string][]string {
//獲得待處理的Type
types := strings.Split(*typeNames, ",")
typesMap := make(map[string][]string, len(types))
for _, v := range types {
typesMap[strings.TrimSpace(v)] = []string{}
}
//解析當(dāng)前目錄下包信息,即獲取當(dāng)前目錄下所有g(shù)o文件信息用于語(yǔ)法樹(shù)解析
var err error
pkgInfo, err = build.ImportDir(".", 0)
if err != nil {
log.Fatal(err)
}
fset := token.NewFileSet()
for _, file := range pkgInfo.GoFiles {
//解析go文件內(nèi)容
f, err := parser.ParseFile(fset, file, nil, 0)
if err != nil {
log.Fatal(err)
}
typ := ""
//遍歷每個(gè)樹(shù)節(jié)點(diǎn)
ast.Inspect(f, func(n ast.Node) bool {
decl, ok := n.(*ast.GenDecl)
// 只需要const
if !ok || decl.Tok != token.CONST {
return true
}
for _, spec := range decl.Specs {
vspec := spec.(*ast.ValueSpec)
if vspec.Type == nil && len(vspec.Values) > 0 {
// 排除 v = 1 這種結(jié)構(gòu)
typ = ""
continue
}
//如果Type不為空散休,則確認(rèn)typ
if vspec.Type != nil {
ident, ok := vspec.Type.(*ast.Ident)
if !ok {
continue
}
typ = ident.Name
}
//typ是否是需處理的類型
consts, ok := typesMap[typ]
if !ok {
continue
}
//將所有const變量名保存
for _, n := range vspec.Names {
consts = append(consts, n.Name)
}
typesMap[typ] = consts
}
return true
})
}
return typesMap
}
func genString(types map[string][]string) []byte {
const strTmp = `
package {{.pkg}}
import "fmt"
{{range $typ,$consts :=.types}}
func (c {{$typ}}) String() string{
switch c { {{range $consts}}
case {{.}}:return "{{.}}"{{end}}
}
return fmt.Sprintf("Status(%d)", c)
}
{{end}}
`
pkgName := os.Getenv("GOPACKAGE")
if pkgName == "" {
pkgName = pkgInfo.Name
}
data := map[string]interface{}{
"pkg": pkgName,
"types": types,
}
//利用模板庫(kù),生成代碼文件
t, err := template.New("").Parse(strTmp)
if err != nil {
log.Fatal(err)
}
buff := bytes.NewBufferString("")
err = t.Execute(buff, data)
if err != nil {
log.Fatal(err)
}
//格式化
src, err := format.Source(buff.Bytes())
if err != nil {
log.Fatal(err)
}
return src
}
// 將上面的程序編譯為myenumstr
$go build -o myenumstr
2)乐尊、為要生成對(duì)應(yīng)友好輸出的對(duì)象添加generate注釋:
package main
type Status int
//go:generate ./myenumstr -type Status,Color
const (
Offline Status = iota
Online
Disable
Deleted
)
type Color int
const (
Write Color = iota
Red
Blue
)
// 執(zhí)行g(shù)o工具的generate的命令
$go generate