美文网首页
go语言中特殊注释使用(go语言特性)

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

作者: Feel_狗焕 | 来源:发表于2024-05-14 17:10 被阅读0次

目录:

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


相关文章

  • TODO:Go语言goroutine和channel使用

    TODO:Go语言goroutine和channel使用 goroutine是Go语言中的轻量级线程实现,由Go语...

  • Go语言和C语言的交叉访问

    Go语言中导入C语言 方法 Go语言中导入C语言都需要单行注释和多行注释注释起来 在C语言紧随其后的位置编写" i...

  • go语言中的方法

    go支持OO语言的一些特性,方法就是其中之一。本文将介绍go语言中方法相关的概念。 方法声明 在go语言中,我们可...

  • 04-Go语言关键字和标识符-指趣学院

    关键字 Go语言中的关键字和C语言中的关键字的含义样, 是指被Go语言赋予特殊含义的单词 Go语言中关键字的特征和...

  • Go语言指针

    Go 语言中指针是很容易学习的,Go 语言中使用指针可以更简单的执行一些任务。 接下来让我们来一步步学习 Go 语...

  • GO 一文搞懂指针和地址值的区别

    GO 语言的指针和地址值 go语言中的指针和地址值,在使用上常常具有迷惑性,主要是其特殊的*、&符号的使用,可能会...

  • Go var

    Go语言基础语法,Go在很多特性上和C非常相近。 标识符 编程语言中标识符实际上是程序员定义的具有特殊意义的名称,...

  • 课程目录

    初识Go语言 Go语言的特性 环境搭建 分享人:洪波 时间:2019-6-25 17:00-18:00 Go语...

  • Go语言操作Redis

    Go语言操作Redis 在项目开发中redis的使用也比较频繁,本文介绍了Go语言中go-redis库的基本使用。...

  • 模板的使用

    模版 Go模板使用 在Go语言中,我们使用template包来进行模版处理,使用类似Parse,ParseFile...

网友评论

      本文标题:go语言中特殊注释使用(go语言特性)

      本文链接:https://www.haomeiwen.com/subject/yawmfjtx.html