gotmpl
A Rust port of Go's text/template.
Supports the full template syntax (pipelines, control flow, custom functions, template
composition, whitespace trimming) with Go compatible output. no_std compatible (with alloc).
The crate forbids unsafe code and denies the panic-family lints (panic!, unwrap,
expect, unreachable!, todo!, unimplemented!).
User-provided functions that panic are caught under std; see no_std notes below.
Quick start
use ;
let data = tmap! ;
let result = new
.parse
.unwrap
.execute_to_string
.unwrap;
assert_eq!;
For one-shot renders with no configuration, use gotmpl::execute (source
string) or gotmpl::execute_file (reads from disk):
use ;
let result = execute.unwrap;
assert_eq!;
Template syntax
Actions are delimited by {{ and }} (configurable via .delims()).
Data access
{{.}} Current context (dot)
{{.Name}} Field access on dot
{{.User.Email}} Nested field access
{{$}} Top-level data (root context)
{{$x}} Variable access
{{$x.Name}} Field access on variable
Pipelines
{{.Name | printf "%s!"}}
{{"hello" | len | printf "%d chars"}}
Control flow
{{if .Condition}}...{{end}}
{{if .Cond}}...{{else}}...{{end}}
{{if eq .X 1}}...{{else if eq .X 2}}...{{else}}...{{end}}
{{range .Items}}...{{end}}
{{range .Items}}...{{else}}empty{{end}}
{{range $i, $v := .Items}}{{$i}}: {{$v}}{{end}}
{{range .Items}}{{if eq . 3}}{{break}}{{end}}{{.}}{{end}}
{{range .Items}}{{if eq . 3}}{{continue}}{{end}}{{.}}{{end}}
{{range 5}}{{.}} {{end}}
{{with .Value}}...{{end}}
{{with .Value}}...{{else}}fallback{{end}}
Variables
{{$x := .Name}} Declare variable
{{$x = "new value"}} Assign to existing variable
{{$i, $v := range .List}} Range with index and value (declaration)
{{$i, $v = range .List}} Range with index and value (assignment)
Template composition
{{define "name"}}...{{end}} Define a named template
{{template "name" .}} Invoke a named template
{{block "name" .}}default{{end}} Define and invoke (with default)
Comments and whitespace trimming
{{/* This is a comment */}}
{{- .X}} Trim whitespace before
{{.X -}} Trim whitespace after
{{- .X -}} Trim both sides
Built-in functions
| Function | Description |
|---|---|
print |
Concatenate args (spaces between non-string adjacent args) |
printf |
Formatted output (see below) |
println |
Print with spaces between args, trailing newline |
len |
Length of string, list, or map |
index |
Index into list or map: index .List 0, index .Map "key" |
slice |
Slice a list or string: slice .List 1 3 |
call |
Call a function value: call .Func arg1 arg2 |
eq, ne, lt, le, gt, ge |
Comparison operators. eq supports multi-arg: eq .X 1 2 3 |
and, or |
Short-circuit logic, return the deciding value |
not |
Boolean negation |
html, js, urlquery |
Escape for HTML, JavaScript, URL query |
printf verbs and flags
Format strings follow Go's fmt syntax:
%[flags][width][.precision][argument index]verb.
| Verb | Applies to | Output |
|---|---|---|
%s |
string | The string itself |
%q |
string, int(rune) | Go-quoted string, or single-quoted rune literal |
%v |
any | Default formatted value |
%d |
int | Decimal |
%b |
int | Binary |
%o |
int | Octal |
%x, %X |
int, string | Lower/upper hex (on strings: hex of each byte) |
%c |
int | Unicode scalar |
%U |
int | Unicode U+XXXX (with #: appends 'c') |
%f |
float | Decimal, no exponent |
%e, %E |
float | Scientific notation (lower/upper e) |
%g, %G |
float | %e/%E for large exponents, else %f |
%t |
bool | true / false |
%% |
— | Literal % |
Flags: - (left-align), + (always sign numerics), (leading space for non-negative
numerics), # (alternate form: 0b/0o/0x/0X prefix, or quoted rune for %U),
0 (zero-pad numerics).
Width and .precision accept either a literal number or * / .* to read the value
from the next argument (which must be an int — floats and other types yield
%!(BADWIDTH) / %!(BADPREC)).
Argument indexing with %[N]verb selects the N-th (1-based) argument and resets the
sequential cursor. Out-of-range or malformed indices produce %!verb(BADINDEX).
Mismatched verb/argument pairs produce Go's error markers rather than panicking:
%!v(BADVERB), %!v(MISSING), %!(EXTRA …) for unconsumed args, and the
BADWIDTH / BADPREC / BADINDEX forms above.
Custom functions
Register functions before parsing:
use ;
use Value;
let result = new
.func
.parse
.unwrap
.execute_to_string
.unwrap;
assert_eq!;
Function values and call
Value::Function allows passing callable values through templates:
extern crate alloc;
use Arc;
use ;
use ;
let adder: ValueFunc = new;
let result = new
.func
.parse
.unwrap
.execute_to_string
.unwrap;
assert_eq!;
Options
use ;
let tmpl = new
.missing_key // error on missing map keys
.delims // custom delimiters
.parse
.unwrap;
MissingKey implements FromStr, so you can parse from strings (useful for
config files or CLI args):
use MissingKey;
let mk: MissingKey = "error".parse.unwrap;
MissingKey variant |
FromStr value |
Behavior |
|---|---|---|
Invalid (default) |
"invalid", "default" |
Return <no value> |
ZeroValue |
"zero" |
Return <no value> |
Error |
"error" |
Return an error |
ZeroValue exists for parity with Go's {{options "missingkey=zero"}} directive.
Since Value is untyped, it behaves the same as Invalid; the variant is there so
callers can still opt in to the named option.
Number literals
Go-compatible number literal syntax:
{{42}} Decimal
{{3.14}} Float
{{0xFF}} Hexadecimal
{{0o77}} Octal
{{0377}} Octal (legacy leading zero)
{{0b1010}} Binary
{{1_000_000}} Underscore separators
{{'a'}} Character literal (97)
{{0x1.ep+2}} Hex float (7.5)
Data model
Template data uses the Value enum:
| Variant | Rust type | Go equivalent |
|---|---|---|
Nil |
n/a | nil |
Bool(bool) |
bool |
bool |
Int(i64) |
i64 |
int |
Float(f64) |
f64 |
float64 |
String(Arc<str>) |
String |
string |
List(Arc<[Value]>) |
Vec<Value> |
[]any |
Map(Arc<BTreeMap<Arc<str>, Value>>) |
BTreeMap |
map[string]any |
Function(ValueFunc) |
Arc<dyn Fn> |
func(...) |
The tmap! macro builds data maps:
use ;
let data = tmap! ;
no_std support
The crate works in no_std environments (requires alloc). Disable the default std
feature:
[]
= { = "0.3", = false }
Without std, execute_fmt and execute_to_string are available. The io::Write-based
execute/execute_template methods and parse_files require the std feature.
User-defined functions that panic will propagate instead of being caught.
Differences from Go
Rust has no runtime reflection, so:
- No struct field access: use
Value::Mapinstead - No method calls: register functions via
.func() - No pointer/interface indirection:
Valueis always concrete - No complex numbers, channels, or
iter.Seq - NaN comparisons return an error instead of Go's silently wrong results
API shape is also a bit different:
Template::lookupreturns the parsed body (Option<&ListNode>) rather than a re-executable*Template. To run a named definition, useexecute_template_to_string("name", ...)on the parent.Template::templatesreturns the list of defined names, notTemplateobjects — definitions share the parent's func map and options, so there's no per-definition handle to hand back.parse_filesrequires the files to be valid UTF-8. Go'sos.ReadFile+string(b)is a zero-copy reinterpret and accepts any bytes; we usestd::fs::read_to_string, which validates.
A few formatting and slicing edge cases diverge too:
%#vfalls back to%v. Go's Go-syntax output ([]interface {}{1, 2, 3},map[string]interface {}{"a":1}) needs concrete-type info we don't carry.%#Uquotes some non-ASCII codepoints that Go skips. Our gate ischar::is_control; Go uses the fullstrconv.IsPrinttable, which also rejects non-ASCII spacing characters like U+00A0.sliceon a string at a mid-codepoint offset returns an error. Go would hand back the raw bytes (potentially invalid UTF-8); we can't store invalid UTF-8 inValue::String, so we reject the slice instead.
Go cross-check
The test suite can optionally run every template through Go's text/template and assert
output parity:
A Go helper (tests/testdata/go_crosscheck.go) is compiled once per test run. It
reads templates and typed data from stdin as JSON, executes them via Go's
text/template, and prints the result.