结构体字段必须首字母大写才能被模板访问,因为Go反射仅支持导出字段(如Name),小写字段(如name)不可见;传入map[string]interface{}会导致{{.Name}}失败,应使用导出字段的结构体或指针。

结构体字段必须首字母大写才能被模板访问
传 User{Name: "Alice"} 进模板却报 can't evaluate field Name,不是语法错,是 Go 反射规则卡住你了:只有导出字段(首字母大写)才可见。Name ✅,name ❌。哪怕字段类型是 string 或 int,小写就直接不可见。
别用 map[string]interface{} 模拟结构体——它没有“字段”,只有“键”,所以 {{.Name}} 会失败,得写 {{.Name}};但更推荐用真实结构体,IDE 能提示、编译期能检查。
常见踩坑点:
-
t.Execute(w, &user)和t.Execute(w, user)都合法,但t.Execute(w, nil)必 panic,哪怕模板开头有{{if .}} - 调试时加一行
fmt.Printf("%#v\n", user),确认传进去的是带导出字段的结构体值/指针,不是空 struct 或嵌套错层
模板里调用函数必须先注册 FuncMap
text/template 不允许直接调用未注册函数,连 strings.Title 这种标准库函数也得手动塞进去。没注册就写 {{.Name | title}},运行时会 panic 报 function "title" not defined。
立即学习“go语言免费学习笔记(深入)”;
注册方式固定:
- 用
t.Funcs(template.FuncMap{"title": strings.Title})注册单个函数 - 自定义函数签名必须是
func(interface{}) interface{}或更具体(如func(time.Time, string) string),返回值不能是 error - 管道调用顺序从左到右:
{{.CreatedAt | formatTime | upper}},等价于upper(formatTime(.CreatedAt))
注意:注册必须在 Parse 之前,否则无效;FuncMap 是只读映射,不能动态增删。
ParseFiles 路径是相对于 os.Getwd(),不是源码目录
写 t, err := template.ParseFiles("templates/user.tmpl") 却提示 pattern matches no files,大概率是你 cd 错了目录。Go 运行时工作目录(os.Getwd())决定路径解析起点,跟 .go 文件在哪毫无关系。
更可靠的加载方式:
-
template.ParseGlob("templates/*.tmpl")—— 自动匹配所有匹配文件,不依赖当前路径 - Go 1.16+ 推荐用
//go:embed打包:embed.FS+template.ParseFS(tmplFS, "templates/*.tmpl") - 开发期可加一行
log.Println("cwd:", os.Getwd())确认实际工作目录
template.Must 应该只用在 Parse 阶段(比如 template.Must(t.Parse(...))),它会在解析失败时立刻 panic,避免把错误拖到 Execute 时才暴露。
空白控制要用 {{- 和 -}},别靠缩进对齐
模板默认原样保留换行和空格,生成代码或 YAML 时容易崩格式。比如:
{{range .Fields}}
func (t *T) Get{{.Name}}() {{.Type}} {
return t.{{.Name}}
}
{{end}}这段会产生多余空行和缩进,破坏 Go 代码可读性甚至合法性。
正确做法:
-
{{- if .Active}}吞掉前面所有空白(含换行),{{end -}}吞掉后面换行 - 生成代码优先压成单行:
{{range .Fields}}func (t *T) Get{{.Name}}() {{.Type}} { return t.{{.Name}} }{{end}} - 需要美观缩进的 JSON/YAML,别让模板干这事——先用
json.MarshalIndent或yaml.MarshalIndent序列化好,再注入模板变量
复杂嵌套逻辑里,{{if}} 前加 {{if .Items}} 判断切片非空,否则空切片渲染后还留着无意义的上下文块,容易误导后续处理。


















