Interfaces
Custom serialization with Atomizable and Deatomizable.
Overview
By default, atom uses reflection to convert structs to atoms. You can bypass reflection by implementing the Atomizable and Deatomizable interfaces:
type Atomizable interface {
Atomize(*Atom)
}
type Deatomizable interface {
Deatomize(*Atom) error
}
Why Use Interfaces?
- Performance: Eliminate reflection overhead
- Custom Logic: Transform data during serialization
- Computed Fields: Derive values not stored in the struct
- Backwards Compatibility: Handle schema migrations
Implementing Atomizable
type User struct {
FirstName string
LastName string
BirthYear int64
}
func (u *User) Atomize(a *Atom) {
a.Strings["FirstName"] = u.FirstName
a.Strings["LastName"] = u.LastName
a.Ints["BirthYear"] = u.BirthYear
// Computed field
a.Strings["FullName"] = u.FirstName + " " + u.LastName
}
Usage
atomizer, _ := atom.Use[User]()
user := &User{FirstName: "Alice", LastName: "Smith", BirthYear: 1990}
atom := atomizer.Atomize(user)
// Uses User.Atomize() instead of reflection
fmt.Println(atom.Strings["FullName"]) // "Alice Smith"
Implementing Deatomizable
func (u *User) Deatomize(a *Atom) error {
u.FirstName = a.Strings["FirstName"]
u.LastName = a.Strings["LastName"]
u.BirthYear = a.Ints["BirthYear"]
// Validation
if u.BirthYear < 1900 || u.BirthYear > 2100 {
return fmt.Errorf("invalid birth year: %d", u.BirthYear)
}
return nil
}
Usage
atom := &Atom{
Strings: map[string]string{"FirstName": "Bob", "LastName": "Jones"},
Ints: map[string]int64{"BirthYear": 1985},
}
user, err := atomizer.Deatomize(atom)
// Uses User.Deatomize() instead of reflection
Partial Implementation
You can implement only one interface:
Atomizable Only
type Metrics struct {
RequestCount int64
ErrorCount int64
// Internal, not serialized
lastUpdate time.Time
}
func (m *Metrics) Atomize(a *Atom) {
a.Ints["RequestCount"] = m.RequestCount
a.Ints["ErrorCount"] = m.ErrorCount
// Computed
a.Floats["ErrorRate"] = float64(m.ErrorCount) / float64(m.RequestCount)
}
// No Deatomize - uses reflection
Deatomizable Only
type Config struct {
Host string
Port int64
}
// No Atomize - uses reflection
func (c *Config) Deatomize(a *Atom) error {
c.Host = a.Strings["Host"]
c.Port = a.Ints["Port"]
// Apply defaults
if c.Host == "" {
c.Host = "localhost"
}
if c.Port == 0 {
c.Port = 8080
}
return nil
}
Handling Nested Types
For nested structs, call their methods:
type Order struct {
ID int64
Items []OrderItem
}
type OrderItem struct {
ProductID int64
Quantity int64
}
func (o *Order) Atomize(a *Atom) {
a.Ints["ID"] = o.ID
items := make([]Atom, len(o.Items))
for i, item := range o.Items {
itemAtom := Atom{Ints: make(map[string]int64)}
item.Atomize(&itemAtom)
items[i] = itemAtom
}
a.NestedSlices["Items"] = items
}
func (item *OrderItem) Atomize(a *Atom) {
a.Ints["ProductID"] = item.ProductID
a.Ints["Quantity"] = item.Quantity
}
Schema Migration
Handle old and new field names:
type User struct {
Email string // Was "EmailAddress" in v1
}
func (u *User) Deatomize(a *Atom) error {
// Try new name first
if email, ok := a.Strings["Email"]; ok {
u.Email = email
} else if email, ok := a.Strings["EmailAddress"]; ok {
// Fall back to old name
u.Email = email
}
return nil
}
func (u *User) Atomize(a *Atom) {
// Always write new name
a.Strings["Email"] = u.Email
}
Encryption/Encoding
Transform sensitive data:
type Secret struct {
Data []byte
}
func (s *Secret) Atomize(a *Atom) {
// Encrypt before storing
encrypted := encrypt(s.Data)
a.Bytes["Data"] = encrypted
}
func (s *Secret) Deatomize(a *Atom) error {
encrypted := a.Bytes["Data"]
// Decrypt after loading
decrypted, err := decrypt(encrypted)
if err != nil {
return err
}
s.Data = decrypted
return nil
}
Validation
Validate during deatomization:
type User struct {
Age int64
Email string
}
func (u *User) Deatomize(a *Atom) error {
u.Age = a.Ints["Age"]
u.Email = a.Strings["Email"]
// Validation
if u.Age < 0 || u.Age > 150 {
return fmt.Errorf("invalid age: %d", u.Age)
}
if !strings.Contains(u.Email, "@") {
return fmt.Errorf("invalid email: %s", u.Email)
}
return nil
}
Code Generation
The interface pattern enables code generation to eliminate reflection:
// Generated by atomgen
func (u *User) Atomize(a *Atom) {
a.Strings["FirstName"] = u.FirstName
a.Strings["LastName"] = u.LastName
a.Ints["Age"] = u.Age
a.Bools["Active"] = u.Active
}
func (u *User) Deatomize(a *Atom) error {
u.FirstName = a.Strings["FirstName"]
u.LastName = a.Strings["LastName"]
u.Age = a.Ints["Age"]
u.Active = a.Bools["Active"]
return nil
}
See Code Generation Cookbook for details.
Best Practices
Keep It Simple
// Good - straightforward mapping
func (u *User) Atomize(a *Atom) {
a.Strings["Name"] = u.Name
a.Ints["Age"] = u.Age
}
// Avoid - complex logic in serialization
func (u *User) Atomize(a *Atom) {
// Don't do heavy computation here
a.Strings["Name"] = expensiveTransform(u.Name)
}
Match the Structure
// Good - mirrors struct fields
func (u *User) Atomize(a *Atom) {
a.Strings["Name"] = u.Name
a.Strings["Email"] = u.Email
}
// Confusing - different structure
func (u *User) Atomize(a *Atom) {
a.Strings["user_name"] = u.Name // Different key
a.Ints["email_hash"] = hash(u.Email) // Different type/value
}
Document Migrations
func (u *User) Deatomize(a *Atom) error {
// Migration: "name" was renamed to "full_name" in v2.0
if name, ok := a.Strings["full_name"]; ok {
u.Name = name
} else {
u.Name = a.Strings["name"] // Legacy
}
return nil
}
Next Steps
- Testing Guide - Testing atom-based code
- Code Generation Cookbook - Generating implementations