效仿 Golang 中的枚举类型

栏目: Go · 发布时间: 5年前

内容简介:在这篇博文中我们看到使用博文的结果是一个生成枚举类型的客户端。Go 并没有对枚举类型提供一流的支持。模拟枚举类型的一种方法是,将一系列相关的常量定义为一个新的类型。Iota 可用于预定义连续自增的整形常量。我们可以像下面这样定义一个

在这篇博文中我们看到使用 go generate 和遍历抽象语法树来生成强大的枚举类型。

博文的结果是一个生成枚举类型的客户端。 全部代码 都可以在 Github 上面找到。

Go 惯用技巧

Go 并没有对枚举类型提供一流的支持。模拟枚举类型的一种方法是,将一系列相关的常量定义为一个新的类型。Iota 可用于预定义连续自增的整形常量。我们可以像下面这样定义一个 Color 类型。

package main

import "fmt"

type Color int

const (
    Red Color = iota // 0
    Blue             // 1
)

func main() {
    var b1 Color = Red
    b1 = Red
    fmt.Println(b1) // 打印 0

    var b2 Color = 1
    fmt.Println(b2 == Blue) // 打印 true

    var b3 Color
    b3 = 42
    fmt.Println(b3)  // 打印 42
}

value - we ’ ll need to convert the const to a display value in code. 这种模式在 Go 的代码中十分常见。虽然很常见但这个方法有其缺陷。因为没有静态语言检测,所以任意的整型都能作为 Color。没有序列化支持 - 开发者想要将其序列化为整型进行传输或者作为数据库记录,这是相当罕见的。没有可读的显示值支持 - 我们会需要在代码中将常量强转为显示值。

知道一门语言的习惯以及何时打破这些习惯是十分重要的。习惯用法的论据往往被用来关闭论点。这有时可能是创造力的死亡。

设计枚举类型

Go 最好的一个特性之一就是它的简便性 - 从其他语言转型而来的开发者通常可以非常快速的进行高效的开发。另一方面,这也带来了限制(译者注:作者想表达的应该是,某些其他语言支持泛型而 Golang 不支持,因而转到 Go 的开发者会受限),例如缺失能让代码变得整洁的泛型。为了克服这些缺点,社区已经将代码生成作为定义更为强大和灵活的类型的方案。

让我们用这个途径来定义枚举类型。其中一种做法是生成枚举结构体。我们还可以将方法附加到结构体中。结构体还提供了元标签,这对定义显示的值和描述很有帮助。

type ColorEnum struct {
    Red  string `enum:"RED"`
    Blue string `enum:"BLUE"`
}

现在我们需要做的是为结构体的每个字段生成一个结构体实例

var Red  = Color{name: "RED"}
var Blue = Color{name: "BLUE"}

然后我们可以对 Color 结构体增加方法以支持 JSON 编码 / 解码。我们实现 Marshaler 接口来提供 JSON 编码。

func (c Color) MarshalJSON() ([]byte, error) {
    return JSON.Marshal(c.name)
}

Go 会在序列化这个类型为 JSON 的时候,调用我们定义的实现。同样,我们可以实现 Unmarshaler 接口,该接口使我们能够使用枚举类型——这允许我们直接在 API 中的数据传输对象上定义枚举类型。

func (c *Color) UnmarshalJSON(b []byte) error {
    return JSON.Unmarshal(b, c.name)
}

我们还可以增加一些辅助方法来生成显示值的切片。

// ColorNames 返回所有枚举实例的显示值的切片
func ColorNames() []string { ... }

我们也需要支持根据 string 生成枚举实例的方法,加上它。

// NewColore 根据提供的显示值生成一个新的 Color
func NewColor(value string) (Color, error) { ... }

这种设计极具扩展性,你可能想要添加其他方法来返回名称,通过实现 Error() string 接口提供 errors,以及通过实现 String() string 支持 Stringer

生成代码

遍历抽象语法树

在渲染模板生成代码之前,我们需要解析源码中的 ColorEnum 类型。两个常用的方法是使用 refelctast 包。我们需要扫描在包级别声明的结构体。 ast 包拥有能力去构造抽象语法树 - 一种代表 Go 源码的可遍历数据结构。然后可以遍历抽象语法树并匹配提供的类型。这个类型和定义的结构体标签可以被解析并用于建立生成模板的模型。我们先加载一个 Go 的包

cfg := &packages.Config{
    Mode:  packages.LoadSyntax,
    Tests: false,
}
pkgs, err := packages.Load(cfg, patterns...)

变量 pkgs 包含了这个包每个文件的抽象语法树。 ast.Inspect 方法可用于遍历 AST( 译者注:抽象语法树 ),我们遍历每个文件,然后处理该文件的语法树。

for _, file := range pkg.files {
...
    ast.Inspect(file.file, func(node ast.Node) bool {
        // 处理节点,检查是否是我们感兴趣的东西
    })
}

消费者应该定义自身的方法来过滤出它们所感兴趣的标志类型。你可以通过在节点上做以下校验来过滤结构体

node.Tok == token.STRUCT { ... }

在我们的例子中,我们对定义 enum: 标签的 struct 进行过滤。我们简单对源码中的每一个标志进行处理,并根据碰到的数据构建模型(自定义 Go struct)。

渲染源码

有几个方法可以生成代码。工具 Stringer 使用 fmt 包将内容写到标准输出。虽然这很容易实现,但随着生成器的扩展,它变得难以操作且难以调试。更为合理的方法是使用 text/template 包并使用 Go 强大的模板库。它允许你从模板中分离生成模型的逻辑,从而导致将关注点和易于推理的代码分离开。(译者注:对比 stringer 源码之后就更精确地了解这句话的意思)生成的类型定义可能如下所示。

// {{.NewType}} 是需要被创建的枚举实例
type {{.NewType}} struct {
    name  string
}

// 枚举实例
{{- range $e := .Fields}}
var {{.Value}} = {{$.NewType}}{name: "{{.Key}}"}
{{- end}}

... 生成方法的代码

然后我们可以根据我们的模型来渲染模板

t, err := template.New(tmpl).Parse(tmpl)
if err != nil {
    log.Fatal("instance template parse error: ", err)
}

err = t.Execute(buf, model)

在开发模板的时候无需担心格式化就最好的。 format 包存在将源码作为参数然后返回格式化后的 Go 代码的方法,所以让 Go 帮你处理这个东西吧。

func Source(src []byte) ([]byte, error) { ... }

结论

在这篇博文中我们看到了解析 Go 源码生成枚举类型的方法。这个方法可作为需要解析源码的其他代码生成器的模板。我们以可维护的方式使用 Go 的 text/template 库来渲染源码。

在 Github 上阅读 所有的代码


以上所述就是小编给大家介绍的《效仿 Golang 中的枚举类型》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

六度分隔

六度分隔

邓肯·J·瓦茨 / 陈禹 / 中国人民大学出版社 / 2011-3 / 46.00元

正如副标题所表明的,《六度分隔:一个相互连接的时代的科学》的基本内容是介绍一门正在形成中的新科学——关于网络的一般规律的科学。有这样一门科学吗?它的内容和方法是什么?近年来,这门学科有什么实质性的进展吗?在《六度分隔:一个相互连接的时代的科学》中,作者根据自己的亲身经历娓娓道来,用讲故事的方式,对于这些问题给出了令人信服的回答 除了简要的背景和总结以外,《六度分隔:一个相互连接的时代的科学》......一起来看看 《六度分隔》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试