Menu
CoddyTech

代码生成概述

本课属于 Coddy GO 学习旅程的“面向对象编程”单元。第 84 课,共 107 课。

代码生成是反射的一种替代方案,它会在编译时创建 Go 源代码,而不是在运行时检查类型。这种方法在减少重复样板代码的同时,还能提供类型安全性和更好的性能。

Go 提供了 go generate 命令来自动生成代码。你可以在源文件中放置一条特殊的注释指令,然后运行 go generate 来执行指定的工具:

//go:generate stringer -type=Status

type Status int

const (
    Pending Status = iota
    Active
    Completed
)

运行 go generate ./... 会调用 stringer 工具,该工具会创建一个新文件,其中包含 Status 类型的 String() 方法。生成的代码是普通的 Go 代码,会与你的程序一起编译。

Go 生态系统中常见的代码生成工具包括用于生成枚举字符串方法的 stringer、用于根据接口创建测试模拟对象的 mockgen,以及用于根据 Protocol Buffer 定义生成代码的 protoc。许多 ORM 和 API 框架也会使用代码生成来创建类型安全的数据库查询或客户端代码。

与反射相比,代码生成的主要优势在于生成的代码会经过编译器检查。错误会在构建时发现,而不是在运行时。代价是需要增加一个构建步骤,并在代码仓库中管理生成的文件。对于需要在多种类型中重复实现的 OOP 模式,代码生成通常能在安全性和便利性之间取得最佳平衡。

challenge icon

挑战

简单

我们来构建一个代码生成器,为类似枚举的类型创建 String() 方法!虽然在这个环境中无法实际运行 go generate,但你会模拟代码生成工具的工作方式:根据类型定义,以编程方式生成 Go 源代码。

你会把代码分布在两个文件中:

  • generator.go:编写代码生成逻辑。

    创建一个 EnumType 结构体,用来表示类似枚举的类型定义,其中包含 Name(字符串)和 Values(表示常量名称的字符串切片)。

    创建一个函数 GenerateStringMethod(e EnumType) string,用于生成 String() 方法的 Go 源代码。生成的代码应遵循以下模式:

    func (e TypeName) String() string {
        switch e {
        case Value1:
            return "Value1"
        case Value2:
            return "Value2"
        default:
            return "Unknown"
        }
    }

    再创建一个函数 GenerateConstants(e EnumType) string,使用 iota 生成常量声明:

    const (
        Value1 TypeName = iota
        Value2
        Value3
    )

    最后,创建 GenerateFullCode(e EnumType) string,将两种输出组合起来,并添加一条表明这是生成代码的头部注释:

    // Code generated by enum generator. DO NOT EDIT.
    
    [constants]
    
    [string method]
  • main.go:使用你的生成器为不同的枚举类型生成代码。

    读取枚举类型名称,然后读取值的数量,再读取每个值的名称。将这些信息传给生成器,并打印完整的生成代码。

将提供以下输入:

  • 第 1 行:枚举类型名称
  • 第 2 行:值的数量
  • 接下来的各行:每个值的名称

例如,给定:

Status
3
Pending
Active
Completed

你的输出应为:

// Code generated by enum generator. DO NOT EDIT.

const (
    Pending Status = iota
    Active
    Completed
)

func (e Status) String() string {
    switch e {
    case Pending:
        return "Pending"
    case Active:
        return "Active"
    case Completed:
        return "Completed"
    default:
        return "Unknown"
    }
}

再比如,给定:

Priority
2
Low
High

你的输出应为:

// Code generated by enum generator. DO NOT EDIT.

const (
    Low Priority = iota
    High
)

func (e Priority) String() string {
    switch e {
    case Low:
        return "Low"
    case High:
        return "High"
    default:
        return "Unknown"
    }
}

这个练习展示了 stringer 等工具背后的核心概念:在编译时生成重复但类型安全的代码,而不是在运行时使用反射。你的生成器会生成有效的 Go 代码,这些代码可以保存到文件中,并与项目的其余部分一起编译。

亲手试试

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	reader := bufio.NewReader(os.Stdin)

	// 读取枚举类型名称
	typeName, _ := reader.ReadString('\n')
	typeName = strings.TrimSpace(typeName)

	// 读取值的数量
	countStr, _ := reader.ReadString('\n')
	count, _ := strconv.Atoi(strings.TrimSpace(countStr))

	// 读取每个值的名称
	values := make([]string, count)
	for i := 0; i < count; i++ {
		value, _ := reader.ReadString('\n')
		values[i] = strings.TrimSpace(value)
	}

	// TODO: 用读取的数据创建 EnumType

	// TODO: 使用 GenerateFullCode 生成并打印完整代码
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: Go 在线编译器