Code Generation
Eliminating reflection overhead with generated code.
Overview
While atom's reflection-based approach is convenient, generated implementations of Atomizable and Deatomizable provide:
- Zero reflection overhead at runtime
- Compile-time type checking for field mappings
- Faster serialization (typically 5-10x)
Manual Implementation
Before automating, understand the pattern:
type User struct {
Name string
Age int64
Active bool
Score float64
}
// Generated or hand-written
func (u *User) Atomize(a *atom.Atom) {
a.Strings["Name"] = u.Name
a.Ints["Age"] = u.Age
a.Bools["Active"] = u.Active
a.Floats["Score"] = u.Score
}
func (u *User) Deatomize(a *atom.Atom) error {
u.Name = a.Strings["Name"]
u.Age = a.Ints["Age"]
u.Active = a.Bools["Active"]
u.Score = a.Floats["Score"]
return nil
}
Generator Template
A simple Go template for generating implementations:
// generator.go
package main
import (
"os"
"text/template"
)
var tmpl = template.Must(template.New("atomizer").Parse(`
// Code generated by atomgen. DO NOT EDIT.
package {{.Package}}
import "github.com/zoobz-io/atom"
{{range .Types}}
func (x *{{.Name}}) Atomize(a *atom.Atom) {
{{- range .Fields}}
{{- if eq .Table "strings"}}
a.Strings["{{.Name}}"] = x.{{.Name}}
{{- else if eq .Table "ints"}}
a.Ints["{{.Name}}"] = x.{{.Name}}
{{- else if eq .Table "floats"}}
a.Floats["{{.Name}}"] = x.{{.Name}}
{{- else if eq .Table "bools"}}
a.Bools["{{.Name}}"] = x.{{.Name}}
{{- end}}
{{- end}}
}
func (x *{{.Name}}) Deatomize(a *atom.Atom) error {
{{- range .Fields}}
{{- if eq .Table "strings"}}
x.{{.Name}} = a.Strings["{{.Name}}"]
{{- else if eq .Table "ints"}}
x.{{.Name}} = a.Ints["{{.Name}}"]
{{- else if eq .Table "floats"}}
x.{{.Name}} = a.Floats["{{.Name}}"]
{{- else if eq .Table "bools"}}
x.{{.Name}} = a.Bools["{{.Name}}"]
{{- end}}
{{- end}}
return nil
}
{{end}}
`))
Using go:generate
Add a directive to your types file:
//go:generate atomgen -type=User,Order,Product
type User struct {
Name string
Age int64
Active bool
}
Run generation:
go generate ./...
Handling Complex Types
Pointers
func (u *User) Atomize(a *atom.Atom) {
// Pointer field
a.StringPtrs["Nickname"] = u.Nickname // *string
}
func (u *User) Deatomize(a *atom.Atom) error {
u.Nickname = a.StringPtrs["Nickname"]
return nil
}
Slices
func (u *User) Atomize(a *atom.Atom) {
a.StringSlices["Tags"] = u.Tags
a.IntSlices["Scores"] = u.Scores
}
func (u *User) Deatomize(a *atom.Atom) error {
u.Tags = a.StringSlices["Tags"]
u.Scores = a.IntSlices["Scores"]
return nil
}
Nested Structs
func (u *User) Atomize(a *atom.Atom) {
a.Strings["Name"] = u.Name
// Nested struct
addressAtom := atom.Atom{
Strings: make(map[string]string),
}
u.Address.Atomize(&addressAtom)
a.Nested["Address"] = addressAtom
}
func (u *User) Deatomize(a *atom.Atom) error {
u.Name = a.Strings["Name"]
if addrAtom, ok := a.Nested["Address"]; ok {
if err := u.Address.Deatomize(&addrAtom); err != nil {
return err
}
}
return nil
}
Slice of Structs
func (o *Order) Atomize(a *atom.Atom) {
a.Ints["ID"] = o.ID
items := make([]atom.Atom, len(o.Items))
for i := range o.Items {
itemAtom := atom.Atom{
Ints: make(map[string]int64),
Floats: make(map[string]float64),
}
o.Items[i].Atomize(&itemAtom)
items[i] = itemAtom
}
a.NestedSlices["Items"] = items
}
func (o *Order) Deatomize(a *atom.Atom) error {
o.ID = a.Ints["ID"]
if itemAtoms, ok := a.NestedSlices["Items"]; ok {
o.Items = make([]OrderItem, len(itemAtoms))
for i := range itemAtoms {
if err := o.Items[i].Deatomize(&itemAtoms[i]); err != nil {
return err
}
}
}
return nil
}
Width Conversion
For narrow integer types, add conversion:
type Narrow struct {
Small int8
Med int16
}
func (n *Narrow) Atomize(a *atom.Atom) {
a.Ints["Small"] = int64(n.Small)
a.Ints["Med"] = int64(n.Med)
}
func (n *Narrow) Deatomize(a *atom.Atom) error {
// Add overflow checking
small := a.Ints["Small"]
if small < math.MinInt8 || small > math.MaxInt8 {
return fmt.Errorf("Small overflow: %d", small)
}
n.Small = int8(small)
med := a.Ints["Med"]
if med < math.MinInt16 || med > math.MaxInt16 {
return fmt.Errorf("Med overflow: %d", med)
}
n.Med = int16(med)
return nil
}
Named Types
Handle named types by using the underlying operations:
type UserID string
type Status int
type User struct {
ID UserID
Status Status
}
func (u *User) Atomize(a *atom.Atom) {
a.Strings["ID"] = string(u.ID)
a.Ints["Status"] = int64(u.Status)
}
func (u *User) Deatomize(a *atom.Atom) error {
u.ID = UserID(a.Strings["ID"])
u.Status = Status(a.Ints["Status"])
return nil
}
Performance Comparison
Typical benchmarks (your results may vary):
BenchmarkAtomize_Reflection-8 500000 2400 ns/op 1024 B/op 12 allocs/op
BenchmarkAtomize_Generated-8 5000000 240 ns/op 512 B/op 4 allocs/op
BenchmarkDeatomize_Reflection-8 300000 4100 ns/op 1536 B/op 18 allocs/op
BenchmarkDeatomize_Generated-8 3000000 450 ns/op 256 B/op 2 allocs/op
Best Practices
Regenerate on Change
generate:
go generate ./...
test: generate
go test ./...
Keep Generated Files
Commit generated files to version control:
models/
├── user.go # Source
├── user_atom.go # Generated
├── order.go # Source
└── order_atom.go # Generated
Validate Generation
Test that generated code produces correct round-trips:
func TestGeneratedRoundTrip(t *testing.T) {
atomizer, _ := atom.Use[User]()
original := &User{Name: "Alice", Age: 30, Active: true}
// Atomize (uses generated Atomize method)
a := atomizer.Atomize(original)
// Verify expected fields
if a.Strings["Name"] != "Alice" {
t.Errorf("Name: got %q, want %q", a.Strings["Name"], "Alice")
}
if a.Ints["Age"] != 30 {
t.Errorf("Age: got %d, want %d", a.Ints["Age"], 30)
}
// Deatomize (uses generated Deatomize method)
restored, err := atomizer.Deatomize(a)
if err != nil {
t.Fatal(err)
}
// Verify round-trip
if !reflect.DeepEqual(original, restored) {
t.Errorf("round-trip failed:\noriginal: %+v\nrestored: %+v", original, restored)
}
}
Next Steps
- Serialization Cookbook - Encoding atoms
- API Reference - Complete API