Extending functions
Template functions are pluggable. The Go builtins ship in gotmpl4j-core, Sprig adds ~220
more when gotmpl4j-sprig is on the classpath, and your application can contribute its own —
through the same SPI the built-in libraries use. There are three ways in, from quickest to
most reusable.
1. The Function interface
A function is a single method that receives its evaluated pipeline arguments and returns the value passed to the next stage (or rendered):
@FunctionalInterface
public interface Function {
Object invoke(Object... args);
}
The last argument of a pipeline is the piped value, so {{ .name | greet "Hello" }} calls
greet with ("Hello", <value of .name>).
2. 1. Inline functions on a single template
For one-off or application-local helpers, hand a name→Function map to the
Builder:
GoTemplate tpl = GoTemplate.builder()
.withFunctions(Map.of(
"shout", (Function) args -> String.valueOf(args[0]).toUpperCase(Locale.ROOT)))
.build();
tpl.parse("t", "{{ .name | shout }}!"); // -> "WORLD!"
These override any discovered function of the same name (they are applied last).
3. 2. A reusable FunctionProvider
To package a set of functions for reuse — and to have them auto-register on the classpath —
implement FunctionProvider:
public class GreetingFunctions implements FunctionProvider {
@Override
public Map<String, Function> getFunctions(GoTemplate template) {
return Map.of("greet", args -> args[1] + ", " + args[0] + "!");
}
@Override
public int priority() {
return 150; // applied after Sprig (100), before Helm (200)
}
}
getFunctions receives the owning GoTemplate, so a provider may capture it (for example to
share named templates or compose with other functions).
3.1. Auto-discovery via ServiceLoader
Register the provider as a service and it is discovered automatically whenever the jar is on the classpath — no wiring:
src/main/resources/META-INF/services/org.alexmond.gotmpl4j.FunctionProvidercom.example.GreetingFunctions
Now new GoTemplate() already has greet. (Pass GoTemplate.builder().noAutoDiscovery() to
opt out of classpath discovery and register providers explicitly.)
4. Priority and name collisions
When two providers register the same name, the higher priority() wins. The built-in bands are:
| Provider | Priority | Source |
|---|---|---|
Go builtins |
0 |
|
Sprig |
100 |
|
Helm |
200 |
external (jhelm) |
Inline withFunctions(…) always wins, regardless of priority — it is layered on top last.
5. Signalling failure
Throw FunctionExecutionException
(unchecked) to fail a render from inside a function; the engine surfaces it wrapped in a
TemplateExecutionException (both under the GoTemplateException root — see
Error handling):
Function div = args -> {
long d = ((Number) args[1]).longValue();
if (d == 0) {
throw new FunctionExecutionException("div: division by zero");
}
return ((Number) args[0]).longValue() / d;
};
6. Spring Boot
The starter wires the SPI the Spring-idiomatic way (mirroring how Thymeleaf collects
IDialect beans): every FunctionProvider bean in the context is registered with the engine,
and a single Map<String, Function> bean — if one is present — contributes extra
name→function entries that override the providers on top.
@Configuration
class TemplateFunctionsConfig {
@Bean
FunctionProvider greetingFunctions() {
return new GreetingFunctions(); // a whole provider
}
@Bean
Map<String, Function> extraTemplateFunctions() { // one-off helpers, by name
return Map.of("shout", args -> String.valueOf(args[0]).toUpperCase(Locale.ROOT));
}
}
Both are then callable from any view the
GoTemplateService renders.