zoobzio January 5, 2025 Edit this page

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