go语言中特殊注释使用(go语言特性)

234 阅读6分钟

目录:

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