Code Generation
Override interfaces bypass reflection but require manual implementation. Code generation automates this, keeping implementations in sync with struct tags.
The Problem
Manual override implementations drift from struct definitions:
type User struct {
Email string `json:"email" store.encrypt:"aes" load.decrypt:"aes"`
Password string `json:"password" receive.hash:"argon2"`
SSN string `json:"ssn" send.mask:"ssn"`
// Add new field...
Phone string `json:"phone" send.mask:"phone"`
}
// Oops - forgot to update Mask() for Phone field
func (u *User) Mask(maskers map[cereal.MaskType]cereal.Masker) error {
u.Email = maskers[cereal.MaskEmail].Mask(u.Email)
u.SSN = maskers[cereal.MaskSSN].Mask(u.SSN)
// Phone not masked!
return nil
}
Code generation solves this by reading struct tags and generating implementations automatically.
Simple Generator
A minimal generator using go/ast and go/parser:
//go:build ignore
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"io"
"os"
"reflect"
"strings"
"text/template"
)
func main() {
if len(os.Args) < 3 {
fmt.Fprintln(os.Stderr, "usage: go run gen.go <file.go> <TypeName>")
os.Exit(1)
}
filename := os.Args[1]
typeName := os.Args[2]
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
spec := findType(f, typeName)
if spec == nil {
fmt.Fprintf(os.Stderr, "type %s not found\n", typeName)
os.Exit(1)
}
fields := extractFields(spec)
if err := generate(os.Stdout, typeName, fields); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
type Field struct {
Name string
EncryptAlg string // store.encrypt value
DecryptAlg string // load.decrypt value
HashAlg string // receive.hash value
MaskType string // send.mask value
RedactVal string // send.redact value
}
func findType(f *ast.File, name string) *ast.TypeSpec {
for _, decl := range f.Decls {
gen, ok := decl.(*ast.GenDecl)
if !ok || gen.Tok != token.TYPE {
continue
}
for _, spec := range gen.Specs {
ts := spec.(*ast.TypeSpec)
if ts.Name.Name == name {
return ts
}
}
}
return nil
}
func extractFields(spec *ast.TypeSpec) []Field {
st, ok := spec.Type.(*ast.StructType)
if !ok {
return nil
}
var fields []Field
for _, f := range st.Fields.List {
if len(f.Names) == 0 || f.Tag == nil {
continue
}
tag := reflect.StructTag(strings.Trim(f.Tag.Value, "`"))
field := Field{Name: f.Names[0].Name}
if v, ok := tag.Lookup("store.encrypt"); ok {
field.EncryptAlg = v
}
if v, ok := tag.Lookup("load.decrypt"); ok {
field.DecryptAlg = v
}
if v, ok := tag.Lookup("receive.hash"); ok {
field.HashAlg = v
}
if v, ok := tag.Lookup("send.mask"); ok {
field.MaskType = v
}
if v, ok := tag.Lookup("send.redact"); ok {
field.RedactVal = v
}
if field.EncryptAlg != "" || field.DecryptAlg != "" ||
field.HashAlg != "" || field.MaskType != "" || field.RedactVal != "" {
fields = append(fields, field)
}
}
return fields
}
func generate(w io.Writer, typeName string, fields []Field) error {
return tmpl.Execute(w, map[string]any{
"Type": typeName,
"Fields": fields,
})
}
var tmpl = template.Must(template.New("").Funcs(template.FuncMap{
"hasEncrypt": func(fields []Field) bool {
for _, f := range fields {
if f.EncryptAlg != "" {
return true
}
}
return false
},
"hasDecrypt": func(fields []Field) bool {
for _, f := range fields {
if f.DecryptAlg != "" {
return true
}
}
return false
},
"hasHash": func(fields []Field) bool {
for _, f := range fields {
if f.HashAlg != "" {
return true
}
}
return false
},
"hasMask": func(fields []Field) bool {
for _, f := range fields {
if f.MaskType != "" {
return true
}
}
return false
},
"hasRedact": func(fields []Field) bool {
for _, f := range fields {
if f.RedactVal != "" {
return true
}
}
return false
},
}).Parse(`
// Code generated by gen.go. DO NOT EDIT.
package main
import (
"encoding/base64"
"github.com/zoobzio/cereal"
)
{{- $type := .Type }}
{{ if hasEncrypt .Fields -}}
func (x *{{ $type }}) Encrypt(encryptors map[cereal.EncryptAlgo]cereal.Encryptor) error {
{{- range .Fields }}
{{- if .EncryptAlg }}
if x.{{ .Name }} != "" {
enc := encryptors[cereal.EncryptAlgo("{{ .EncryptAlg }}")]
ciphertext, err := enc.Encrypt([]byte(x.{{ .Name }}))
if err != nil {
return err
}
x.{{ .Name }} = base64.StdEncoding.EncodeToString(ciphertext)
}
{{- end }}
{{- end }}
return nil
}
{{ end }}
{{ if hasDecrypt .Fields -}}
func (x *{{ $type }}) Decrypt(encryptors map[cereal.EncryptAlgo]cereal.Encryptor) error {
{{- range .Fields }}
{{- if .DecryptAlg }}
if x.{{ .Name }} != "" {
enc := encryptors[cereal.EncryptAlgo("{{ .DecryptAlg }}")]
ciphertext, err := base64.StdEncoding.DecodeString(x.{{ .Name }})
if err != nil {
return err
}
plaintext, err := enc.Decrypt(ciphertext)
if err != nil {
return err
}
x.{{ .Name }} = string(plaintext)
}
{{- end }}
{{- end }}
return nil
}
{{ end }}
{{ if hasHash .Fields -}}
func (x *{{ $type }}) Hash(hashers map[cereal.HashAlgo]cereal.Hasher) error {
{{- range .Fields }}
{{- if .HashAlg }}
if x.{{ .Name }} != "" {
hasher := hashers[cereal.HashAlgo("{{ .HashAlg }}")]
hashed, err := hasher.Hash([]byte(x.{{ .Name }}))
if err != nil {
return err
}
x.{{ .Name }} = hashed
}
{{- end }}
{{- end }}
return nil
}
{{ end }}
{{ if hasMask .Fields -}}
func (x *{{ $type }}) Mask(maskers map[cereal.MaskType]cereal.Masker) error {
{{- range .Fields }}
{{- if .MaskType }}
if x.{{ .Name }} != "" {
x.{{ .Name }} = maskers[cereal.MaskType("{{ .MaskType }}")].Mask(x.{{ .Name }})
}
{{- end }}
{{- end }}
return nil
}
{{ end }}
{{ if hasRedact .Fields -}}
func (x *{{ $type }}) Redact() error {
{{- range .Fields }}
{{- if .RedactVal }}
x.{{ .Name }} = "{{ .RedactVal }}"
{{- end }}
{{- end }}
return nil
}
{{ end }}
`))
Usage
Add a go:generate directive to your type file:
//go:generate go run gen.go user.go User
type User struct {
ID string `json:"id"`
Email string `json:"email" store.encrypt:"aes" load.decrypt:"aes" send.mask:"email"`
Password string `json:"password" receive.hash:"argon2"`
SSN string `json:"ssn" send.mask:"ssn"`
Token string `json:"token" send.redact:"[REDACTED]"`
}
func (u User) Clone() User { return u }
Run generation:
go generate ./...
Output (user_cereal.go):
// Code generated by gen.go. DO NOT EDIT.
package main
import (
"encoding/base64"
"github.com/zoobzio/cereal"
)
func (x *User) Encrypt(encryptors map[cereal.EncryptAlgo]cereal.Encryptor) error {
if x.Email != "" {
enc := encryptors[cereal.EncryptAlgo("aes")]
ciphertext, err := enc.Encrypt([]byte(x.Email))
if err != nil {
return err
}
x.Email = base64.StdEncoding.EncodeToString(ciphertext)
}
return nil
}
func (x *User) Decrypt(encryptors map[cereal.EncryptAlgo]cereal.Encryptor) error {
if x.Email != "" {
enc := encryptors[cereal.EncryptAlgo("aes")]
ciphertext, err := base64.StdEncoding.DecodeString(x.Email)
if err != nil {
return err
}
plaintext, err := enc.Decrypt(ciphertext)
if err != nil {
return err
}
x.Email = string(plaintext)
}
return nil
}
func (x *User) Hash(hashers map[cereal.HashAlgo]cereal.Hasher) error {
if x.Password != "" {
hasher := hashers[cereal.HashAlgo("argon2")]
hashed, err := hasher.Hash([]byte(x.Password))
if err != nil {
return err
}
x.Password = hashed
}
return nil
}
func (x *User) Mask(maskers map[cereal.MaskType]cereal.Masker) error {
if x.Email != "" {
x.Email = maskers[cereal.MaskType("email")].Mask(x.Email)
}
if x.SSN != "" {
x.SSN = maskers[cereal.MaskType("ssn")].Mask(x.SSN)
}
return nil
}
func (x *User) Redact() error {
x.Token = "[REDACTED]"
return nil
}
Integration with CI
Add generation check to CI:
- name: Check generated code
run: |
go generate ./...
git diff --exit-code || (echo "Generated code out of date" && exit 1)
This fails the build if someone modifies struct tags without regenerating.
Extending the Generator
The basic generator handles string fields. Extensions to consider:
Nested structs:
type Address struct {
City string `json:"city" send.redact:"[HIDDEN]"`
}
type User struct {
Address Address `json:"address"`
}
Recursively process embedded/nested types.
Slice and map fields:
type User struct {
Emails []string `json:"emails" send.mask:"email"`
}
Generate loops over elements.
Conditional transforms:
type User struct {
Email string `json:"email" store.encrypt:"aes,if=Sensitive"`
}
Parse tag options and generate conditionals.
byte fields:
type Record struct {
Data []byte `json:"data" store.encrypt:"aes"`
}
Skip base64 encoding for byte slices.
Alternative: Reflection at Init
If code generation feels heavy, cache reflection work at init time:
var userFields struct {
email reflect.StructField
password reflect.StructField
}
func init() {
t := reflect.TypeOf(User{})
userFields.email, _ = t.FieldByName("Email")
userFields.password, _ = t.FieldByName("Password")
}
func (u *User) Encrypt(encryptors map[cereal.EncryptAlgo]cereal.Encryptor) error {
// Use cached field info
// Still faster than full reflection each call
}
This trades startup cost for runtime performance without full code generation.
When to Generate
Use code generation when:
- Many types — Manual implementations don't scale
- Frequent changes — Tags change often, implementations drift
- Strict correctness — Can't afford missed fields
- Team scale — Multiple developers modifying types
Skip code generation when:
- Few types — Manual is manageable
- Stable schema — Types rarely change
- Custom logic — Transforms need runtime decisions
- Simple needs — Reflection overhead acceptable