go:generate只是命令触发器,不处理模板、AST解析或多文件联动;自研生成器可精准控制入口、调试流程及上下文注入,用text/template足够,复杂逻辑应在Go代码中预处理而非塞入模板。

为什么不用 go:generate 而要自己写生成器
因为 go:generate 只是命令触发器,不处理模板、AST 解析、多文件联动或上下文注入——比如你想根据一个 schema.yaml 生成 struct + CRUD 方法 + HTTP handler + OpenAPI 注释,go:generate 拿不到 YAML 内容,也没法把字段类型映射成 time.Time 或指针。真正需要的是可控的入口、可调试的流程、以及能和项目代码共享配置的能力。
所以自研生成器不是为了炫技,而是当标准工具链覆盖不到时,用最小代价补上那一环。
用 text/template 还是 gotpl?选前者就够了
text/template 是 Go 标准库,稳定、无额外依赖、调试方便。你不需要 gotpl 那类增强语法——90% 的生成需求只是循环字段、判断是否导出、拼接包名。复杂逻辑应该放在 Go 代码里预处理,而不是塞进模板。
常见错误:在模板里写 {{if .Field.Type == "string"}}...{{end}},结果类型是 reflect.Type,根本不能直接比较字符串。正确做法是提前在数据结构里加 IsString bool 字段。
- 模板只做“渲染”,不做“判断”或“转换”
- 所有字段类型、标签解析、嵌套关系,都在
main.go里用ast或yaml.Unmarshal处理完再传入模板 - 用
template.FuncMap注入少量辅助函数,比如camelCase,但别超过 3 个
读取源定义时,别直接解析 Go AST
除非你明确要从已有 .go 文件提取结构体(比如 gRPC 的 proto 到 Go 的桥接),否则优先用 YAML/JSON 定义 schema。Go AST 解析门槛高、版本敏感、错误提示差——parser.ParseFile 遇到一个语法错误就 panic,而 YAML 报错能精准到第几行。
示例场景:你要为订单服务生成 model + validator + db query。这时定义一个 order.schema.yaml:
name: Order fields: - name: ID type: uint64 - name: CreatedAt type: time.Time tag: json:"created_at"
然后用 yaml.Unmarshal 读入结构体,再转成模板数据。比写一堆 ast.Inspect 回调清爽得多。
生成文件前必须检查 go fmt 和 go vet
生成器输出的代码如果格式错乱或有未使用的变量,会直接破坏团队开发流。不要假设“反正用户会自己格式化”。在 os.WriteFile 后立刻调用:
exec.Command("gofmt", "-w", outputPath)-
exec.Command("go", "vet", outputPath)(注意 vet 不接受单文件,需临时建模块或用go tool vet)
更稳妥的做法是生成到临时路径,fmt/vet 通过后再 os.Rename。否则某次生成引入 var _ = fmt.Println,CI 就会卡住。
容易被忽略的一点:生成器本身应支持 --dry-run 和 --diff。后者可用 diff -u 对比旧文件与新内容,避免无意义的 git diff 刷屏。


















