Go中如何解析嵌入模板?为何ParseFS无法找到模板文件?
问题描述
我正在编写一个生成代码的Go程序,想要结合embed包与ParseFS解析模板,确保代码能在仓库任意目录运行。
现有基于ParseFiles的可用实现,目录结构如下:
. ├── codegen │ └── main.go ├── foo │ ├── foo.go │ ├── foo.go.tmpl │ └── foo_test.go ├── gen │ └── foo.go └── go.mod
foo.go的代码生成逻辑如下:
package foo import ( "bytes" "fmt" "html/template" "filepath" ) const templateFile = "../foo/foo.go.tmpl" type GeneratedType struct { Name string StringFields []string } func GenerateCode() ([]byte, error) { tmpl, err := template.New(filepath.Base(templateFile)).ParseFiles(templateFile) if err != nil { return nil, fmt.Errorf("parse template: %v", err) } var buf bytes.Buffer if err := tmpl.Execute(&buf, GeneratedType{ Name: "Foo", StringFields: []string{"Bar"}, }); err != nil { return nil, fmt.Errorf("execute template: %v", err) } return buf.Bytes(), nil }
模板文件foo.go.tmpl内容:
package foo type {{.Name}} struct { {{- range .StringFields}} {{.}} string {{- end}} }
单元测试foo_test.go:
package foo import ( "go/format" "testing" ) func TestGenerateCode(t *testing.T) { code, err := GenerateCode() if err != nil { t.Errorf("generate code: %v", err) } if _, err := format.Source(code); err != nil { t.Errorf("format source code: %v", err) } }
codegen/main.go通过go generate运行,调用GenerateCode在gen目录生成代码:
//go:generate go run github.com/khpeek/codegen-example/codegen package main import ( "errors" "log" "os" "github.com/khpeek/codegen-example/foo" ) func main() { code, err := foo.GenerateCode() if err != nil { log.Fatalf("generate code: %v", err) } if err := os.Mkdir("../gen", 0700); err != nil && !errors.Is(err, os.ErrExist) { log.Fatalf("create directory for generated code: %v", err) } if err := os.WriteFile("../gen/foo.go", code, 0644); err != nil { log.Fatalf("write file: %v", err) } }
该实现可以正常运行go generate ./...和go test ./...,但模板路径"../foo/foo.go.tmpl"依赖目录层级,鲁棒性差。尝试改用embed包优化foo.go:
package foo import ( "bytes" "embed" "fmt" "text/template" ) //go:embed foo.go.tmpl var templateFS embed.FS type GeneratedType struct { Name string StringFields []string } func GenerateCode() ([]byte, error) { tmpl, err := template.New("foo.go.tmpl").ParseFS(templateFS) if err != nil { return nil, fmt.Errorf("parse template: %v", err) } var buf bytes.Buffer if err := tmpl.Execute(&buf, GeneratedType{ Name: "Foo", StringFields: []string{"Bar"}, }); err != nil { return nil, fmt.Errorf("execute template: %v", err) } return buf.Bytes(), nil }
但此时运行生成或测试会报错:"no files name in call to ParseFiles",具体错误如下:
> go generate ./... 2023/01/31 09:06:21 generate code: parse template: template: no files named in call to ParseFiles exit status 1 codegen/main.go:1: running "go": exit status 1
> go test ./... ? github.com/khpeek/codegen-example/codegen [no test files] --- FAIL: TestGenerateCode (0.00s) foo_test.go:11: generate code: parse template: template: no files named in call to ParseFiles FAIL FAIL github.com/khpeek/codegen-example/foo 0.137s ? github.com/khpeek/codegen-example/gen [no test files] FAIL
请问为何ParseFS无法找到模板文件?
问题原因与解决方法
错误原因
template.ParseFS函数的签名是func ParseFS(fsys fs.FS, patterns ...string) (*Template, error),它需要传入要解析的文件路径模式作为第二个及以后的参数。当前代码只传了嵌入文件系统templateFS,没有指定要读取的模板文件名,导致函数找不到目标文件,从而抛出no files named in call to ParseFiles错误。
修正后的代码
修改foo.go中的GenerateCode函数,在ParseFS中添加模板文件名参数:
package foo import ( "bytes" "embed" "fmt" "text/template" ) //go:embed foo.go.tmpl var templateFS embed.FS type GeneratedType struct { Name string StringFields []string } func GenerateCode() ([]byte, error) { // 传入模板文件名作为ParseFS的第二个参数 tmpl, err := template.New("foo.go.tmpl").ParseFS(templateFS, "foo.go.tmpl") if err != nil { return nil, fmt.Errorf("parse template: %v", err) } var buf bytes.Buffer if err := tmpl.Execute(&buf, GeneratedType{ Name: "Foo", StringFields: []string{"Bar"}, }); err != nil { return nil, fmt.Errorf("execute template: %v", err) } return buf.Bytes(), nil }
补充说明
- 因为
//go:embed foo.go.tmpl已经将模板文件嵌入到templateFS中,所以直接传入文件名"foo.go.tmpl"即可,不需要额外路径。 - 修改后代码不再依赖外部目录结构,在仓库任意目录下运行
go generate ./...或go test ./...都能正常工作。
内容的提问来源于stack exchange,提问作者Kurt Peek
相关产品推荐
相关产品推荐

