结论:优先复用 Goa、go-swagger 或 GORM Gen 的模板扩展机制;自研时用 embed.FS + text/template + 预处理结构体最稳。ParseFiles 路径以 os.Getwd() 为根,易错;embed.FS 编译进二进制才可靠。模板中指针字段需预处理防 nil panic,缩进需手动控制并校验 gofmt,FuncMap 必须在 Parse 前注册且函数无副作用。

直接说结论:不要从零写生成器,优先复用 Goa、go-swagger 或 GORM Gen 的模板扩展机制;自研生成器时,embed.FS + text/template + 预处理结构体 是最稳的组合。
为什么 template.ParseFiles 会报 "no such file or directory"
这不是模板写错了,是路径基准错了。Go 的 template.ParseFiles 永远以 os.Getwd()(当前工作目录)为根,不是源码位置、也不是二进制所在目录。你在 IDE 里点运行、CI 里用 cd /tmp && go run cmd/gen/main.go,路径就全乱了。
- 硬编码
"./templates/xxx.tmpl"在 Docker、Windows 和 macOS 下行为不一致,别碰 - 用
filepath.Join(filepath.Dir(runtime.Caller(0)), "..", "templates")推导路径,只适用于包初始化或main()中,且依赖调用栈稳定 - 真正可靠的方案:用
embed.FS编译进二进制。加一行//go:embed templates/*.tmpl,再用template.Must(template.New("").ParseFS(tmplFS, "templates/*.tmpl"))
模板里访问 .Field.Name 却 panic "nil pointer evaluating interface {}.Name"
根本原因是字段是 *string 或嵌套指针,而模板没做空值防护。Go 模板不自动解引用,也不容忍 nil。
- 别在模板里堆
{{with .Field}}{{.Name}}{{end}}—— 它收缩作用域,容易漏掉默认值逻辑 - 推荐预处理:传入前把原始 spec 封装成中间 struct,所有指针字段转为非 nil 值(
*string→string,nil 则设为"";[]int→ 先判空再传) - 对必须保留 nil 语义的字段(比如“不生成某段代码”),定义方法如
func (s *Spec) HasValidator() bool { return s.Validator != nil },模板里写{{if .HasValidator}}
生成的 Go 代码缩进错乱、gofmt 拒绝格式化
模板输出不是人手写的代码,它是机器拼接的字符串。换行、缩进、空格都得手动控制,否则 gofmt 一跑就失败。
立即学习“go语言免费学习笔记(深入)”;
- 每行逻辑结尾加
{{- "\n"}},开头用{{- if}}吞掉前置空白,避免空行堆积 - 别在
{{range}}外围留空行,{{range .Fields}}func (t *T) Get{{.Name}}() {{.Type}} { return t.{{.Name}} }{{end}}这种单行写法更可控 - 生成后务必调用
gofmt -w或用format.SourceAPI 校验,不通过就中断生成 —— 别让坏代码进仓库 - 如果要生成 YAML/JSON 等格式化文本,别靠模板缩进,改用
json.MarshalIndent处理好再注入模板
自定义函数在模板里始终提示 "function 'xxx' not defined"
不是函数写错了,是注册时机错了。template.FuncMap 必须在 Parse 之前完成,且只能对模板实例调用,不能对包级函数返回值操作。
- ❌ 错误:
t := template.New("x"); t.ParseFiles("a.tmpl"); t.Funcs(fm)——Funcs完全无效 - ✅ 正确:
t := template.Must(template.New("x").Funcs(fm).ParseFiles("a.tmpl")) - 函数签名必须严格匹配:
func Upper(s string) string可以,func Upper(s interface{}) string不行;多返回值只接受(T, error) - 禁止在
FuncMap里放闭包、匿名函数、带日志或 IO 的函数 —— 模板函数必须无状态、无副作用
真正难的不是写模板,是设计数据结构和预处理逻辑。字段要不要导出、嵌套层级怎么扁平化、错误是 panic 还是静默跳过——这些决策比语法细节影响更大。每次改模板前,先跑一遍 fmt.Printf("%#v", data) 看清楚你到底传了什么进去。


















