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

目录:

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


©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容