效仿 Golang 中的枚举类型

LSivan · 2019-06-15 19:53:36 · 2191 次点击 · 预计阅读时间 4 分钟 · 大约8小时之前 开始浏览    
这是一个创建于 2019-06-15 19:53:36 的文章,其中的信息可能已经有所发展或是发生改变。

在这篇博文中我们看到使用 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 上阅读所有的代码


via: https://stein.wtf/posts/2019-04-16/enums/

作者:Stein Fletcher  译者:LSivan  校对:polaris1119

本文由 GCTT 原创编译,Go语言中文网 荣誉推出


有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2191 次点击  
加入收藏 微博
被以下专栏收入,发现更多相似内容
2 回复  |  直到 2019-06-16 08:47:12
ji32k8au4a83
ji32k8au4a83 · #1 · 6年之前

Perl 枚举 @$ 符号引用的正整型数据类的集合,没有一个数据类可以如此载入数据而不占用系统资源的数据数值比数值更加高效和快捷,在世界时间和文件输入输出层面没有一个数据类和函数和方法可以替代其作用。

ji32k8au4a83
ji32k8au4a83 · #2 · 6年之前

IOTA等联合组建一个名叫MOBI移动开放区块链计划的联盟,主要专注于探索区块链技术在汽车和移动领域的应用潜力,国际掩食计时协会The International Occultation Timing Association的缩写。这样的数值增自计数器都是在天体的附近才能存在。

添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传