A Go template engine with Django-style control flow, Liquid-style filters, and optional layout-aware HTML rendering.
For development guidelines and project conventions, see AGENTS.md.
- One entry point: Configure a single
Enginefor source-string parsing or loader-backed rendering. - Two output modes:
FormatTextfor raw text,FormatHTMLfor automatic escaping of{{ expr }}output. - Layout on demand:
WithLayout()enablesinclude,extends,block,raw, andsafe. - Composable loaders:
NewMemoryLoader,NewDirLoader,NewFSLoader, andNewChainLoaderfor tests, embedded assets, and override layers. - Sandboxed disk reads:
NewDirLoaderusesos.Rootso template lookups cannot escape the configured directory. - Typed diagnostics:
*RenderErrorcarries the failing template name, line, column, and a sentinel cause forerrors.Is/errors.As. - Engine-local extensions: Register filters and tags on a single engine without touching global state.
go get github.com/kaptinlin/templateRequires Go 1.26+.
package main
import (
"fmt"
"log"
"github.com/kaptinlin/template"
)
func main() {
engine := template.New()
tmpl, err := engine.ParseString("Hello, {{ name | upcase }}!")
if err != nil {
log.Fatal(err)
}
out, err := tmpl.Render(template.Data{"name": "alice"})
if err != nil {
log.Fatal(err)
}
fmt.Println(out) // Hello, ALICE!
}| Task | API | Notes |
|---|---|---|
| Parse a source string | Engine.ParseString + Template.Render / Template.RenderTo |
Best for one-off templates |
| Render named templates | WithLoader(...) + Engine.Render / Engine.RenderTo |
Compiles and caches by loader-resolved name |
| Choose output semantics | WithFormat(FormatText) / WithFormat(FormatHTML) |
FormatHTML auto-escapes {{ expr }} output |
| Enable layout syntax | WithLayout() |
Turns on include, extends, block, raw, and safe |
| Provide shared defaults | WithDefaults(Data) |
Render-time keys override engine defaults |
| Extend behavior | RegisterFilter, ReplaceFilter, RegisterTag, ReplaceTag |
Scoped to the engine instance |
Use a loader when templates live outside the source string:
loader := template.NewMemoryLoader(map[string]string{
"base.html": `<h1>{{ page.title }}</h1>{% block content %}{% endblock %}`,
"page.html": `{% extends "base.html" %}{% block content %}{{ page.content }}{% endblock %}`,
})
engine := template.New(
template.WithLoader(loader),
template.WithFormat(template.FormatHTML),
template.WithLayout(),
)
out, err := engine.Render("page.html", template.Data{
"page": map[string]any{
"title": "Hello <world>",
"content": "<p>escaped</p>",
},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(out)
// <h1>Hello <world></h1><p>escaped</p>For runnable programs, see the examples below.
Parse-time failures return *ParseError. Render-time failures return
*RenderError, which carries the failing template name, 1-based line and
column, and the underlying sentinel.
out, err := engine.Render("page.html", data)
if err != nil {
var re *template.RenderError
if errors.As(err, &re) {
log.Printf("%s:%d:%d: %v", re.Template, re.Line, re.Col, re.Cause)
}
if errors.Is(err, template.ErrFilterNotFound) {
// branch on the sentinel category
}
return err
}Sentinels live in errors.go and are matched with errors.Is.
The human-readable RenderError.Error() format is not part of the contract;
read the fields for stable output.
| Need | API |
|---|---|
| Custom filter | Engine.RegisterFilter, Engine.ReplaceFilter, Engine.MustRegisterFilter |
| Custom tag | Engine.RegisterTag, Engine.ReplaceTag, Engine.MustRegisterTag |
| Custom loader | Implement Loader and pass it through WithLoader(...) |
| Direct runtime control | Template.Execute with RenderContext |
| Trusted HTML | SafeString or ` |
Runnable examples live in examples/:
| Example | Path | What it shows |
|---|---|---|
| Basic usage | examples/usage |
Parse and render a source string through Engine |
| Custom filters | examples/custom_filters |
Register an engine-local filter |
| Custom tags | examples/custom_tags |
Register an engine-local tag parser |
| HTML layouts | examples/layout |
WithLayout(), FormatHTML, includes, extends, and block.super |
| Text generation | examples/multifile_text |
FormatText with loader-backed multi-file output |
| Secret redaction | examples/secret_redaction |
Redact rendered output at the writer or filter boundary |
Run any example with:
go run ./examples/<name>- docs/layout.md — Includes, extends, blocks, raw blocks, and
block.super - docs/loaders.md — Loader types and cache behavior
- docs/filters.md — Built-in filter reference
- docs/variables.md — Variable access, dot paths, and strict mode
- docs/control-structure.md —
if,for,break, andcontinue - docs/security.md — Loader sandbox, escaping rules, and redaction
- docs/liquid-compatibility.md — Compatibility notes and differences
task test # Run all tests with race detection
task test-coverage # Generate coverage.out and coverage.html
task bench # Run benchmarks
task fmt # Run go fmt ./...
task vet # Run go vet ./...
task lint # Run golangci-lint and go mod tidy checks
task verify # Run deps, fmt, vet, lint, and testContributions are welcome. See CONTRIBUTING.md for design boundaries, documentation expectations, and pull request guidance.
This project is licensed under the MIT License - see the LICENSE file for details.