英文:
How to parse an embedded template in Go?
问题
我正在尝试编写一个生成代码的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"
"path/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的生成特性,并包含了调用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
只是巧合地从codegen
和foo
目录中正确解析。如果我更改目录级别,我认为这个示例将不再工作。
我想通过使用embed
包使其更加健壮,以便我始终引用包目录中的文件(在这种情况下是foo
)。为此,我尝试将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
无法“找到”模板文件吗?
英文:
I'm trying to write a Go program that generates code and to use the embed
package together with ParseFS
to parse the template. The code should ultimately satisfy the requirement that it can be run from any directory in the repository.
So far, I have the following working implementation using ParseFiles
. Using this directory structure,
.
├── codegen
│   └── main.go
├── foo
│   ├── foo.go
│   ├── foo.go.tmpl
│   └── foo_test.go
├── gen
│   └── foo.go
└── go.mod
The foo.go
file contains the code generation code,
package foo
import (
"bytes"
"fmt"
"html/template"
"path/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
}
where the template foo.go.tmpl
is
package foo
type {{.Name}} struct {
{{- range .StringFields}}
{{.}} string
{{- end}}
}
It also has a unit test, 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)
}
}
The codegen/main.go
contains is run with Go's generate feature and contains the invocation of GenerateCode
that generates code in an output directory 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)
}
}
This works in that I can call both go generate ./...
and go test ./...
to generate the code and run tests successfully. However, it is a bit fragile because the template file path ../foo/foo.go.tmpl
only "coincidentally" resolves correctly from both the codegen
and the foo
directory. If I were to change the directory level, I suspect this example would no longer work.
I would like to make this more robust by using the embed
package so that I'm always referencing files in the package directory (foo
in this case). To that end, I attempted to change foo.go
into the following:
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
}
Now however, when I try to generate code or run unit tests I get a no files name in call to ParseFiles
error:
> 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
Can anyone explain why ParseFS
isn't "finding" the template file?
答案1
得分: 2
将mkopriva的评论转换为答案,我需要提供一个第二个可变参数给ParseFS
,表示匹配模板文件的通配符模式:
tmpl, err := template.New("foo.go.tmpl").ParseFS(templateFS, "*.go.tmpl")
英文:
To convert mkopriva's comment to an answer, I needed to provide a second, variadic argument to ParseFS
representing a glob pattern that would match the template file:
tmpl, err := template.New("foo.go.tmpl").ParseFS(templateFS, "*.go.tmpl")
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论