如何在Go中解析嵌入的模板?

huangapple go评论87阅读模式
英文:

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只是巧合地从codegenfoo目录中正确解析。如果我更改目录级别,我认为这个示例将不再工作。

我想通过使用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")

huangapple
  • 本文由 发表于 2023年2月1日 01:09:10
  • 转载请务必保留本文链接:https://go.coder-hub.com/75300659.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定