目录:
1、go-build
2、go-embed
3、go-generate
1、go-build:
构建约束也称之为条件编译,就是可以对某些源代码文件指定在特定的平台,架构,编译器甚至Go版本下面进行编译,在其他环境中会自动忽略这些文件。go支持两种方式的构建约束,一种是文件后缀名的方式一种是在文件的头部通过注释的方式。
文件后缀名构建约束的格式为:格式就是文件名_系统名_架构名.go
例如:
user_windows_amd64.go //在 windows 中 amd64 架构下才会编译,其他的环境中会自动忽略
user_linux_arm.go // 在 linux 中的 arm 架构下才会编译,其他环境中会自动忽略
注释的构建约束则如下规则:
1)、1.16之前老版本构建约束:
go 1.16之前的构建约束格式为构建约束的语法是// +build这种形式,如果多个条件组合,通过空格、逗号或多行构建约束表示,逗号表示 AND,空格表示OR,!表示NOT,换行表示AND。
// +build linux,386 darwin,!cgo
它表示的意思是:(linux AND 386) OR (darwin AND (NOT cgo)) ,有些时候,多个约束分成多行书写,会更易读些:
// +build linux darwin
// +build amd64
这相当于:(linux OR darwin) AND amd64
2)、1.17新版本构建约束:
新版的构建约束,注意"//"和"go"之间不能有空格:
//go:build
同时新版语法使用布尔表达式,而不是逗号、空格等。布尔表达式,会更清晰易懂,出错可能性大大降低。
采用新语法后,一个文件只能有一行构建语句,而不是像旧版那样有多行。这样可以避免多行的关系到底是什么的问题。
Go1.17 中,gofmt 工具会自动根据旧版语法生成对应的新版语法,为了兼容性,两者都会保留。比如原来是这样的:
// +build !windows,!plan9
执行 Go1.17 的 gofmt 后,变成了这样:
//go:build !windows && !plan9
// +build !windows,!plan9
如果文件中已经有了这两种约束形式,gofmt 会根据 //go:buid 自动覆盖 // +build 的形式,确保两者表示的意思一致。如果只有新版语法,不会自动生成旧版的,这时,你需要注意,它不兼容旧版本了。
2、go-embed:
//go:embed指令是Go 1.16版本新增的官方编译指令,它可以将任何文件或者文件夹的内容打包到编译出的可执行文件中。
简单来说,我们可以给代码添加一行特殊的注释来,Go 编译器知道是要嵌入文件还是嵌入文件夹。注释长得像"//go:embed 文件名"。注释占一行,紧接着是一个变量。如果要嵌入文件,变量的类型得是 string 或者 []byte,如果要嵌入一组文件,变量的类型得是embed.FS。指令格式有三种形式:
- //go:embed path…:path… 是需要嵌入的文件或目录,可以为多个,用空格分隔;
- //go:embed regexp:regexp 是需要嵌入的文件名或目录名的正则表达式;
- //go:embed dir/.ext:dir/.ext 是需要嵌入的某个目录下特定扩展名的文件;
注意事项:
- //go:embed是Go语言中的指令,看起来很像注释但是并非是注释,其中//和go:embed两者之间不能有空格,必须挨在一起;
- //go:embed后面接要嵌入的文件路径,以相对路径形式声明文件路径,文件路径和//go:embed指令之间相隔一个空格,这里文件相对路径;相对的是当前源代码文件的路径,并且这个路径不能以/或者./开头;
- 必须要导入embed包才能够使用//go:embed指令;
- 如果嵌入的文件夹中包含有以.或者_开头的文件,这些文件就会被视为隐藏文件,会被排除,不会被嵌入;
- 我们还可以使用通配符形式嵌入文件夹,例如://go:embed resource/*,使用通配符形式时,隐藏文件也会被嵌入,并且文件夹本身也会被嵌入;
1)、嵌入单个文件:
例如:假设我们有一个文件叫 data.txt,然后我们希望在程序中引用它,通过 //go:embed 指令即可嵌入。
data.txt文件内容如下:
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)
}
在这个示例中,我们使用//go:embed data.txt将 data.txt 文件嵌入到了可执行文件中,在go build时编译器会把data.txt内容直接附给变量data,然后在 main() 函数中输出了 data 变量的值。
2)、嵌入多个文件:
package main
import (
"embed"
"fmt"
)
// 嵌入多个文件并作为embed.FS类型
// 将当前目录下test.txt和demo.txt嵌入至可执行文件,并存放到embed.FS对象中
//go:embed test.txt demo.txt
var embedFiles embed.FS
func main() {
// 读取嵌入的文件,返回字节切片
testContent, _ := embedFiles.ReadFile("test.txt")
demoContent, _ := embedFiles.ReadFile("demo.txt")
// 将读取到的字节切片转换成字符串输出
fmt.Println(string(testContent))
fmt.Println(string(demoContent))
}
指令部分并不需要改,将接收变量类型改成embed.FS即可,这样可以同时嵌入多个文件,在//go:embed指令后接多个要嵌入的文件路径即可,多个文件路径之间使用空格隔开。最后通过embed.FS对象的ReadFile方法,即可读取指定的嵌入的文件的内容,参数为嵌入的文件名,返回读取到的文件内容(byte切片形式)和错误对象。
所以,我们完全就可以把embed.FS对象想象成一个文件夹,只不过它是个特殊的文件夹,它位于编译后的可执行文件内部。那么使用ReadFile函数读取文件时,也是指定读取这个内部的文件夹中的文件,上述我们使用//go:embed指令嵌入了两个文件,就可以视为这两个文件在编译时被放入到这个特殊的“文件夹”中去了,只不过文件放进去后文件名是不会改变的。
3、go-generate:
Go 语言注释的另一个有趣的用法是通过 go generate 命令工具生成代码。 go generate 是 Go 语言标准工具包的一部分,它通过运行用户指定的外部命令以编程方式生成源 (或其他) 文件。go generate 的工作方式是扫描 .go 程序,寻找其中包含要运行的命令的特殊注释,然后执行它们。
具体来说,go generate 查找以 go:generate 开头的注释(注释标记和文本开始之间没有空格),如下:
//go:generate <command> <arguments>
3.1、generate命令工具使用:
//打印当前目录下所有文件,将被执行的命令
$go generate -n ./...
// 对包下所有Go文件进行处理
$go generate github.com/ysqi/repo
// 打印包下所有文件,将被执行的命令
$go generate -n runtime
3.2、go-generate注释使用:
需在的代码中配置generate标记,则在执行go generate时可被检测到。go generate执行时,实际在扫描如下内容:
//go:generate command argument...
generate命令不是解析文件,而是逐行匹配以//go:generate 开头的行(前面不要有空格)。故命令可以写在任何位置,也可存在多个命令行。
//go:generate后跟随具体的命令。命令为可执行程序,形同在Shell下执行。所以命令是在环境变量中存在,也可是完整路径。如:
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")
}
// 执行后输出如下结果:
$ go generate
hello
man func
file=main.go pkg=main
在执行go generate时将会加入些信息到环境变量,可在命令程序中使用,相关变量如下:
- $GOARCH:架构 (arm, amd64, etc.);
- $GOOS:linux, windows等等;
- $GOFILE:当前处理中的文件名;
- $GOLINE:当前命令在文件中的行号;
- $GOPACKAGE:当前处理文件的包名;
- $DOLLAR:固定的"$",不清楚用途;
3.2.1、使用示例:
比如我们定义一个对象后,为了打印友好内容,我们经常手工定义对应枚举常量的String方法或映射,用于输出对应枚举常量的友好信息。当增加一个枚举常量的时候我们都需增加对应的字符映射。
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来生成对枚举常量的友好输出信息。
1)、编写使用go generate工具根据注释生成代码的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", "", "必填,逗号连接的多个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{}
}
//解析当前目录下包信息,即获取当前目录下所有go文件信息用于语法树解析
var err error
pkgInfo, err = build.ImportDir(".", 0)
if err != nil {
log.Fatal(err)
}
fset := token.NewFileSet()
for _, file := range pkgInfo.GoFiles {
//解析go文件内容
f, err := parser.ParseFile(fset, file, nil, 0)
if err != nil {
log.Fatal(err)
}
typ := ""
//遍历每个树节点
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 这种结构
typ = ""
continue
}
//如果Type不为空,则确认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,
}
//利用模板库,生成代码文件
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)、为要生成对应友好输出的对象添加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
)
// 执行go工具的generate的命令
$go generate