jig is a tool for generating source code from a generics library.
go install github.com/reactivego/jig@latestNote: Since Go 1.18, the language has built-in generics support. However, jig remains useful for:
- Legacy codebases that need to maintain compatibility with older Go versions
- Code generation patterns not easily expressed with Go's type parameters
- Existing projects built on jig-based libraries like reactivego/rx
- Install
- Getting Started
- How does jig work?
- Usage
- Pragma Reference
- Advanced Topics
- Available Generics Libraries
- Acknowledgements
- License
go install github.com/reactivego/jig/cmd/jig@latestVerify the installation:
jig -hUsage of jig [flags] [<dir>]:
-c, --clean Remove files generated by jig
-m, --missing Only generate code that is missing
-n, --nodoc No documentation in generated files
-v, --verbose Print details of what jig is doing
Let's implement a simple generic stack to get started with jig.
mkdir -p ~/go/hellojig/generic
cd ~/go/hellojig
go mod init hellojig
cd genericCreate stack.go with the following content:
package stack
type foo int
//jig:template <Foo>Stack
type FooStack []foo
//jig:template <Foo>Stack Push
func (s *FooStack) Push(v foo) {
*s = append(*s, v)
}
//jig:template <Foo>Stack Pop
func (s *FooStack) Pop() (result foo) {
slen := len(*s)
if slen != 0 {
result = (*s)[slen-1]
*s = (*s)[:slen-1]
}
return
}Key concepts:
foois a placeholder typeFooin template names (like<Foo>Stack) marks the type variable- Each template is a code fragment that jig can specialize
Build to verify the code compiles:
go buildGo back to the project root and create main.go:
cd ..package main
import _ "hellojig/generic"
func main() {
var stack StringStack
stack.Push("Hello, World!")
println(stack.Pop())
}First, try running without jig (this will fail):
go run *.go
# command-line-arguments
./main.go:6:13: undefined: StringStackNow run jig to generate the code:
jig -vfound 3 templates in package "stack" (hellojig/generic)
generating "StringStack"
StringStack
generating "StringStack Push"
StringStack Push
generating "StringStack Pop"
StringStack Pop
writing file "stack.go"
Run again:
go run *.go
Hello, World!The jig tool created stack.go in your project directory:
// Code generated by jig; DO NOT EDIT.
//go:generate jig
package main
//jig:name StringStack
type StringStack []string
//jig:name StringStackPush
func (s *StringStack) Push(v string) {
*s = append(*s, v)
}
//jig:name StringStackPop
func (s *StringStack) Pop() (result string) {
slen := len(*s)
if slen != 0 {
result = (*s)[slen-1]
*s = (*s)[:slen-1]
}
return
}The generated code is a statically typed stack where foo has been replaced with string.
Go's interface{} allows storing different types in the same container, but you lose static type checking. There are valid use cases for this approach:
- Heterogeneous containers — when you need to store
int,string, andstructvalues in the same slice or map - Large implementations — when the implementation is very large but not type-specific, you can wrap an
interface{}-based implementation with strongly typed methods
jig supports specializing templates to interface{}. Simply use Stack instead of StringStack:
package main
import _ "hellojig/generic"
func main() {
var stack Stack
stack.Push("Hello, World!")
stack.Push(42) // Can mix types
println(stack.Pop())
}jig recognizes the absence of a reference type name and uses interface{}:
// Code generated by jig; DO NOT EDIT.
//go:generate jig
package main
//jig:name Stack
type Stack []interface{}
//jig:name StackPush
func (s *Stack) Push(v interface{}) {
*s = append(*s, v)
}
//jig:name StackPop
func (s *Stack) Pop() (result interface{}) {
slen := len(*s)
if slen != 0 {
result = (*s)[slen-1]
*s = (*s)[:slen-1]
}
return
}jig works by repeatedly compiling your code. Each compile cycle may return errors about missing types or methods. jig creates type signatures for these errors and searches templates to find ones it can specialize to fix them. After generating code, jig compiles again. This continues until either no errors remain or no templates can fix the remaining errors.
jig generates the minimal amount of code needed to make your code compile. Even for large generics libraries, only the code actually needed is generated. This approach is called just-in-time-generics (jig) for Go.
- Automatic type discovery — no explicit specialization needed
- Flexible specialization — templates can target specific types (
int32,string) orinterface{} - Minimal code generation — compilation drives generation
- Working Go code — templates are buildable and testable
- Standard distribution — template libraries are normal
go get-able packages
A jig holds a work in a fixed location and guides a tool to manufacture a product. A jig's primary purpose is to provide repeatability, accuracy, and interchangeability in the manufacturing process. — Wikipedia
In this project, a jig is working code written with placeholder types that generates specialized code for specific type combinations:
//jig:template <Foo>Stack Push
func (s *FooStack) Push(v foo) {
*s = append(*s, v)
}Note the placeholders:
Foo(capitalized, for type references) andfoo(lowercase, for actual type usage).
This section revisits the Getting Started example with more detail.
jig -hUsage of jig [flags] [<dir>]:
-c, --clean Remove files generated by jig
-m, --missing Only generate code that is missing
-n, --nodoc No documentation in generated files
-v, --verbose Print details of what jig is doing
jig is a self-contained binary. Run it from your project directory:
- Default behavior — removes previously generated code and regenerates everything
--missing/-m— only generates new code, keeps existing--verbose/-v— shows what jig is doing--clean/-c— removes all jig-generated files
jig discovers templates from imported packages. You must import the generics library in your code:
import _ "github.com/reactivego/jig/example/stack/generic"To see which templates jig finds:
jig -vremoving file "stack.go"
found 4 jig templates in package "github.com/reactivego/jig/example/stack/generic"
...
Generated code includes //go:generate jig, allowing you to regenerate with go generate ./....
Templates should be granular — each type, method, and function typically gets its own jig:template. This ensures:
- Methods your code doesn't use won't be generated
- jig can find templates for specific missing methods
Templates use placeholder types like Foo and Bar (called metasyntactic names). Here's a complete example:
package stack
type foo int
type FooStack []foo
func (s *FooStack) Push(v foo) {
*s = append(*s, v)
}
func (s *FooStack) Pop() (foo, bool) {
var zeroFoo foo
if len(*s) == 0 {
return zeroFoo, false
}
i := len(*s) - 1
v := (*s)[i]
*s = (*s)[:i]
return v, true
}Note:
- This is valid Go code using placeholder type
fooFooStack,Push, andPopall containFooin their names- You can use any placeholder names, not just
FooFoorefers to a type reference;foois the actual type name
This code compiles and can be tested. jig uses it as a template, replacing placeholders with actual type names — including in identifiers and comments.
To tell jig which code is part of templates, add pragmas:
package stack
type foo int
//jig:template <Foo>Stack
type FooStack []foo
//jig:template <Foo>Stack Push
func (s *FooStack) Push(v foo) {
*s = append(*s, v)
}
//jig:template <Foo>Stack Pop
func (s *FooStack) Pop() (foo, bool) {
var zeroFoo foo
if len(*s) == 0 {
return zeroFoo, false
}
i := len(*s) - 1
v := (*s)[i]
*s = (*s)[:i]
return v, true
}Important:
- Three templates:
<Foo>Stack,<Foo>Stack Push,<Foo>Stack Pop- Angle brackets (
<and>) mark type variables- No space between
//andjig:template- An empty line must separate the pragma from the code
This stack is available at github.com/reactivego/jig/example/stack/generic.
Here's a program using the generic stack:
package main
import (
_ "github.com/reactivego/jig/example/stack/generic"
)
//jig:file stack.go
func main() {
var s StringStack
s.Push("Hello, World!")
}Note:
- The import makes the templates available to jig
jig:filespecifies where generated code goesStringStacksignals we want a stack of strings- jig knows
Stringmeans the built-instringtype
After running jig, the file stack.go contains:
// Code generated by jig; DO NOT EDIT.
//go:generate jig
package main
//jig:name StringStack
type StringStack []string
//jig:name StringStackPush
func (s *StringStack) Push(v string) {
*s = append(*s, v)
}Note:
go:generatepragma enables regenerationjig:namepragmas identify each fragment- All
Foo/fooreplaced withString/stringPopisn't generated because it's not used!
Using Stack instead (for interface{}):
package main
import (
"fmt"
_ "github.com/reactivego/jig/example/stack/generic"
)
//jig:file stack.go
func main() {
var s Stack
s.Push("Hello, World!")
if value, ok := s.Pop(); ok {
fmt.Println(value)
}
}Running jig -v:
removing file "stack.go"
found 4 jig templates in package "github.com/reactivego/jig/example/stack/generic"
generating "Stack"
Stack
generating "Stack Push"
Stack Push
generating "Stack Pop"
Stack Pop
writing file "stack.go"
Generated code:
// Code generated by jig; DO NOT EDIT.
//go:generate jig
package main
//jig:name Stack
type Stack []interface{}
var zero interface{}
//jig:name StackPush
func (s *Stack) Push(v interface{}) {
*s = append(*s, v)
}
//jig:name StackPop
func (s *Stack) Pop() (interface{}, bool) {
if len(*s) == 0 {
return zero, false
}
i := len(*s) - 1
v := (*s)[i]
*s = (*s)[:i]
return v, true
}Now Pop is generated because it's used.
Best practices for structuring a generics library:
$GOPATH/src/github.com/reactivego/jig/example/stack
├── generic/ # Template library
│ └── stack.go
├── test/ # Tests for each function/method
│ ├── Pop/
│ │ ├── doc.go
│ │ ├── example_test.go
│ │ └── stack.go
│ ├── Push/
│ └── doc.go
├── doc.go # Package documentation
├── example_test.go # Examples that drive code generation
└── stack.go # Generated code
Exports a package specialized on interface{}. This makes the package useful without requiring users to run jig. The generated code is heterogeneous — it accepts any type but isn't type-safe.
example_test.go— examples using generics that drive code generationdoc.go— package documentation (godoc.org doesn't show docs from test files)stack.go— generated code
Contains all template code. Must be valid, compilable Go code.
Note: The package name should be
stack(notgeneric) so jig knows what to name generated files.
No tests here — just templates.
Contains tests for all aspects of the library, exercising it with different types. Each exported function or method gets its own directory for isolated testing.
Run jig in each test subdirectory to generate stack.go files.
Pragmas tell jig your intent. There are two kinds:
- Template Pragmas — used in template library code
- Generator Pragmas — used in code that consumes templates
Important: Pragmas must be separated from template code by an empty line.
Used when writing a template library.
Defines a template. Also marks the end of any previous template.
//jig:template Subscriber
//jig:template Observable<Foo>
//jig:template Observable<Foo> Map<Bar>- First: template without type variables
- Second: template for
ObservableFoowith variableFoo - Third: template for method
MapBaronObservableFoowith variablesFooandBar
During generation:
FooandBar→ capitalized type names (Int32,String)fooandbar→ actual type names (int32,string)
Real example from github.com/reactivego/rx/generic:
//jig:template Observable<Foo> Map<Bar>
// MapBar transforms the items emitted by an ObservableFoo by applying a function to each item.
func (o ObservableFoo) MapBar(project func(foo) bar) ObservableBar {
observable := func(observe BarObserveFunc, subscribeOn Scheduler, subscriber Subscriber) {
observer := func(next foo, err error, done bool) {
var mapped bar
if !done {
mapped = project(next)
}
observe(mapped, err, done)
}
o(observer, subscribeOn, subscriber)
}
return observable
}Use sparingly.
Marks a template as needed by practically every other template. Alternative to adding jig:needs to every template. Common templates must not have type variables.
//jig:commonThere are usually only a few common templates for shared support code.
Optional pragma to speed up code generation.
Explicitly declares template dependencies. Without it, jig finds dependencies through compilation errors, which takes more iterations.
//jig:needs Observable<Foo> Concat, SubscribeOptionsThis generates needed templates first, reducing compile cycles.
Note: You can't specify partially matching templates.
TimeStamp<Foo>Observermust exist as an actual template to be referenced.
Tells jig that a type embeds other types. If a method is missing, jig can generate it for the embedded type instead.
//jig:template Connectable<Foo>
//jig:embeds Observable<Foo>
//jig:needs SubscribeOptions
// ConnectableFoo is an ObservableFoo that has an additional method Connect()
// used to Subscribe to the parent observable and then multicasting values to
// all subscribers of ConnectableFoo.
type ConnectableFoo struct {
ObservableFoo
connect func(options []SubscribeOptionSetter) Subscriber
}Prevents a template from specializing on interface{}. Use when a specific interface{} version already exists.
//jig:required-vars Foo, BarWith this pragma, StackString and StackInt work, but Stack (suggesting interface{}) does not.
Explicitly marks the end of a template.
//jig:endUsed in code consuming a template library.
Specifies where jig writes generated code.
Default: {{.package}}.go — one file per template library.
Variables available:
{{.Package}}/{{.package}}— template package name (title case / lowercase){{.Name}}/{{.name}}— fragment signature (title case / lowercase)
Examples (assuming package rx):
| Template | Result |
|---|---|
SomeName.go |
SomeName.go |
jig{{.Package}}.go |
jigRx.go |
jig{{.package}}.go |
jigrx.go |
{{.Package}}{{.Name}}.go |
RxObservableInt32.go |
{{.package}}{{.name}}.go |
rxobservableint32.go |
Tells jig about unexported types.
By default, jig assumes type names start with capitals. Use jig:type for lowercase types:
type vector struct{ x, y float64 }
//jig:type Vector vector
var vstack StackVectorjig generates:
// Code generated by jig; DO NOT EDIT.
var v vectorPunctuation is allowed:
//jig:type Size size
//jig:type Points []pointYou'll probably never need this.
When developing a template library, jig skips certain code in "inception" mode (generating into the library itself). This pragma forces generation of that skipped code — useful for debugging.
You'll probably never need this.
Skips generating documentation. Useful when developing templates and you want to see just the code.
You'll never use this yourself.
Written by jig to identify generated fragments. Used when updating code to determine what's already present.
Example: ObservableInt32MapFloat32 for template Observable<Foo> Map<Bar> with int32 and float32.
Template code often depends on code that could also be generated. For example, in github.com/reactivego/rx/generic, mapping between observables requires both ObservableFoo and ObservableBar.
jig supports this. Configure internal generation with jig:type:
package rx
// foo is the first metasyntactic type. Use jig:type to tell jig that
// Foo references actual type foo.
//jig:type Foo foo
type foo int
// bar is the second metasyntactic type.
//jig:type Bar bar
type bar intjig analyzes Go compilation errors:
./main.go:11:8: undefined: StringStack
If template <Foo>Stack exists, jig replaces Foo with String and foo with string.
./main.go:12:3: s.Push undefined (type StringStack has no field or method Push)
If template <Foo>Stack Push exists, jig generates the method.
All detection uses five regular expressions matching Go errors:
^(.*):([0-9]*):([0-9]*): undeclared name: (.*)$
^(.*):([0-9]*):([0-9]*): invalid operation: .* [(]value of type [*](.*)[)] has no field or method (.*)
^(.*):([0-9]*):([0-9]*): invalid operation: .* [(]variable of type [*](.*)[)] has no field or method (.*)
^(.*):([0-9]*):([0-9]*): invalid operation: .* [(]value of type (.*)[)] has no field or method (.*)
^(.*):([0-9]*):([0-9]*): invalid operation: .* [(]variable of type (.*)[)] has no field or method (.*)Note: These match errors from the
loaderpackage, which differ slightly fromgocommand errors.
For template libraries, the API contract works differently — users copy source fragments rather than linking compiled code.
Consider adding an exported revision function:
func Revision123() {}Users reference this in their code. When you update to Revision124(), their code fails to build, signaling they should regenerate.
jig supports higher-order types that can recursively change order level:
//jig:template ObservableObservable<Foo> MergeAll
// MergeAll flattens a higher order observable by merging the observables it emits.
func (o ObservableObservableFoo) MergeAll() ObservableFoo {
observable := func(observe FooObserveFunc, subscribeOn Scheduler, subscriber Subscriber) {
// <SNIP>
}
return observable
}Note: 2nd-order ObservableObservableFoo returns 1st-order ObservableFoo.
This template also matches 3rd-order ObservableObservableObservableFoo returning 2nd-order ObservableObservableFoo.
import _ "github.com/reactivego/rx/generic"import _ "github.com/reactivego/multicast/generic"jig is built on excellent Go tooling:
golang.org/x/tools/go/loader— compilation and error detectiontext/template— code generationregexp— error analysis and type signature matching
This library is licensed under the MIT License. See LICENSE for details.