Documentation
Metago runs before the Go compiler. You annotate ordinary Go declarations, render reusable Go templates, and get ordinary, formatted Go back. There is no runtime framework and no reflection requirement.
Metago is early-stage software. APIs and directives may evolve.
Your first generator
Create stringer.metago anywhere beneath the directory you plan to scan. By default you can just run metago and it will scan all your project recursively for go templates.
{{ define "stringer" }}
func (v {{ name . }}) String() string {
switch v {
{{- range .Values }}
case {{ .Name }}:
return {{ quote .Name }}
{{- end }}
default:
return "unknown"
}
}
{{ end }}
Attach it to a Go type:
package example
//mgo:gen stringer
type Status int
const (
StatusPending Status = iota
StatusRunning
StatusDone
)
Run Metago from the project root:
metago .
Metago finds the template and directive, resolves Status, and writes meta.go beside the source:
// Code generated by metago; DO NOT EDIT.
package example
func (v Status) String() string {
switch v {
case StatusPending:
return "StatusPending"
case StatusRunning:
return "StatusRunning"
case StatusDone:
return "StatusDone"
default:
return "unknown"
}
}
Commit generated code if that fits your project. It is normal Go: inspect it, test it, and compile it with the usual toolchain.
The workflow
A Metago generator has three parts:
- A
//mgo:genor//mgo:inlinedirective selects a template. - Metago resolves the directive’s target and exposes its package, type, field, method, value, and property metadata.
- A named
text/templatedefinition renders Go source.
Template files may live anywhere under the scan root. Hidden directories, vendor, and testdata are skipped. Template names are global to the scan, so every {{ define "name" }} must be unique. Names beginning with std. belong to Metago’s embedded standard templates.
Target declarations
The clearest form puts a directive in the declaration’s doc comment. Metago infers the target:
//mgo:gen validator
type User struct {
Name string
}
//mgo:gen trace
func LoadUser(id string) (User, error) { /* ... */ }
//mgo:gen observe
func (s *Store) Save(user User) error { /* ... */ }
You can also place a directive separately and name a local or imported target explicitly:
type User struct{ Name string }
//mgo:gen validator User
Methods use Type.Method; imported targets use a package qualifier or import path:
//mgo:gen wrapper Server.Serve
//mgo:gen wrapper http.Client.Do
//mgo:gen wrapper net/http.Client.Do
Types, methods, functions, package-level constants, and package-level variables can all be targets. A directive attached to the package declaration creates a package-scoped invocation:
//mgo:gen std.serde.jsonruntime
package jsonruntime
Sidecar or inline output
Use //mgo:gen for a generated sidecar. Ordinary source directives write meta.go; internal tests write meta_test.go; external test packages write meta_<package>_test.go.
Use //mgo:inline when generated code belongs next to its declaration:
//mgo:inline stringer
type Status string
Metago inserts and later replaces a managed region:
//mgo:inline stringer
type Status string
func (s Status) String() string { return string(s) }
//mgo:end
Never edit inside that region. Inline templates can register imports; Metago updates the source import block.
Generation is atomic across the scan root. Metago prepares every package first and changes no files if scanning, target resolution, template execution, or generation fails. Successful runs remove stale Metago-owned sidecars while preserving similarly named files that do not carry Metago’s exact generated header.
Pass arguments
Everything after a template and explicit target is either a positional argument, a bare flag, or key=value:
//mgo:gen endpoint /users/{userID} auth=required cache
func GetUser() {}
Inside the template, read them with .Argv, .Args, or arg:
path: {{ arg 0 }}
auth: {{ default "public" (arg "auth") }}
A token beginning with / or containing { is always treated as a positional argument rather than a target.
Project-wide named defaults live in metago.toml at the exact scan root:
[templates."std.serde".args]
runtime = "example.com/project/internal/jsonruntime"
strict = "true"
Directive-local key=value arguments override configured defaults.
Attach properties
A custom directive namespace attaches metadata without rendering code:
//mgo:api owner=identity
//mgo:gen schema
type User struct {
//mgo:validate required max=100
Name string `json:"name"`
Age int `json:"age"` //mgo:validate min=0 max=150
}
Read properties in a template:
{{ range .Fields }}
{{ if propHas . "validate" "required" }}
// {{ .Name }} is required
{{ end }}
{{ with prop . "validate" "max" }}
// maximum: {{ . }}
{{ end }}
{{ end }}
Properties must be attached to a declaration. Repeating a namespace merges flags and named values; later named values win. Put generation lines before property lines when stacking directives in one comment block.
Work with metadata
The invocation itself is the template’s dot value. Common fields include:
{{ .Package.Name }} {{/* current package */}}
{{ .Name }} {{/* target name */}}
{{ .Kind }} {{/* struct, method, function, const, ... */}}
{{ .Fields }} {{/* target struct fields */}}
{{ .Methods }} {{/* target methods */}}
{{ .Values }} {{/* typed constants for the target type */}}
Helpers keep templates focused on generated code:
{{ define "json-fields" }}
{{ imports "encoding/json" }}
{{ range exportedFields . }}
// {{ name . }}: {{ typeof . }} → {{ tagName . "json" }}
{{ end }}
{{ end }}
Use fail to reject an unsupported invocation. Metago discards that invocation’s output and imports, continues collecting failures, and writes nothing if any invocation fails:
{{ if not (isInt .) }}
{{ fail "requires an integer-backed type" }}
{{ end }}
The reference lists every metadata field and helper.
Aggregate a package
Every generation annotation is available through .Package.Metas in deterministic file and line order. This lets one invocation build a registry from many marker annotations:
{{ define "routes" }}
func Routes() []string {
return []string{
{{- range .Package.Metas }}
{{- if eq .Template "route" }}
{{ quote (index .Argv 0) }},
{{- end }}
{{- end }}
}
}
{{ end }}
{{ define "route" }}{{ end }}
//mgo:gen routes
package api
//mgo:gen route /users
func Users() {}
//mgo:gen route /teams
func Teams() {}
Empty templates are valid markers. Property annotations do not appear in .Package.Metas because they live on their attached symbols.
Reuse standard templates
Metago embeds generators for common jobs:
| Template | Generates |
|---|---|
std.stringer | String() for a primitive-backed type. |
std.enum | String, parse, validation, values, and JSON behavior for enums. |
std.mock | Function-field mocks for interfaces. |
std.mapstruct | Map decoding and encoding for structs. |
std.serde.jsonruntime | A project-owned JSON runtime. |
std.serde | Reflection-free JSON codecs. |
They require no copied .metago files. See examples for complete usage and the reference for supported arguments and behavior.
Run in a project
The command accepts one optional scan root:
metago # current directory
metago ./path # another root
metago -v # verbose output
metago --verbose
With Go 1.24 or later, pin Metago as a project tool:
go get -tool github.com/guillemus/metago@latest
Call it from a single go:generate directive because Metago scans the supplied root recursively:
//go:generate go tool metago .
Then use the normal Go workflow:
go generate ./...
go test ./...
Contribute
Run the complete test and analysis suite from the repository root:
go test ./...
staticcheck ./...
Golden fixtures live under testdata/. Update them only when intentionally accepting generated-output changes:
UPDATE_GOLDEN=1 go test ./...
Install the documentation dependencies once with npm install --prefix docs, then preview this site at localhost:3000:
mise run docs
Build the production site with:
hugo --source docs --destination ../dist --minify