structwalker

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 7, 2026 License: MIT Imports: 11 Imported by: 0

README

structwalker

CI Go Reference Go Report Card

A Go library for recursively walking structs, parsing tags, and dispatching tag-based callbacks with built-in reflection caching.

Register tag handlers once. Walk any struct. Each field's tags are parsed, matched to registered handlers, and dispatched with full context: parsed options, field value, path, and parent. Recursive, cached, zero-alloc on cache hits.

Zero dependencies. stdlib only.

Install

go get github.com/rjp2525/structwalker

Requires Go 1.26+.

Quick Start

package main

import (
    "fmt"

    "github.com/rjp2525/structwalker"
)

type User struct {
    Name  string `json:"name" validate:"required"`
    Email string `json:"email" validate:"email"`
    Age   int    `json:"age" validate:"min=0"`
}

func main() {
    w := structwalker.New()

    w.Handle("json", func(ctx structwalker.FieldContext) error {
        fmt.Printf("json: %s -> %s\n", ctx.Path, ctx.Tag.Name)
        return nil
    })

    w.Handle("validate", func(ctx structwalker.FieldContext) error {
        fmt.Printf("validate: %s rule=%s\n", ctx.Path, ctx.Tag.Name)
        return nil
    })

    user := &User{Name: "Alice", Email: "alice@example.com", Age: 30}
    if err := w.Walk(user); err != nil {
        panic(err)
    }
}

Output:

json: Name -> name
validate: Name rule=required
json: Email -> email
validate: Email rule=email
json: Age -> age
validate: Age rule=min=0

Tag Format

Tags follow the standard Go convention: key:"name,flag1,flag2,opt=value".

  • First element is the name (field alias)
  • Bare identifiers are flags (omitempty, required)
  • key=value pairs are options (max=255, format=date)
  • - as the name ignores a field
  • !flag negates a flag (!omitempty)
tag := structwalker.ParseTag("validate", "email,required,max=255")
tag.Key                     // "validate"
tag.Name                    // "email"
tag.Raw                     // "email,required,max=255"
tag.HasFlag("required")     // true
tag.HasFlag("optional")     // false
tag.Option("max")           // "255", true
tag.Option("min")           // "", false
tag.OptionOr("min", "0")    // "0"
tag.IsIgnored()             // false
Parsing a Full Struct Tag

ParseAllTags extracts every tag from a reflect.StructTag:

type User struct {
    Name string `json:"name,omitempty" validate:"required,max=100" db:"user_name"`
}

sf, _ := reflect.TypeOf(User{}).FieldByName("Name")
tags := structwalker.ParseAllTags(sf.Tag)
// tags[0] = Tag{Key: "json", Name: "name", Flags: ["omitempty"]}
// tags[1] = Tag{Key: "validate", Name: "required", Options: {"max": "100"}}
// tags[2] = Tag{Key: "db", Name: "user_name"}
Negation Flags

Prefix a flag with ! to negate it. Negated flags are stored with the ! prefix:

tag := structwalker.ParseTag("validate", "field,!omitempty")
tag.HasFlag("!omitempty")   // true
tag.HasFlag("omitempty")    // false

Handlers

Handlers are callbacks registered for specific tag keys. When walking a struct, each field's tags are matched against registered handlers and dispatched in priority order.

Tag-Specific Handlers
w := structwalker.New()

w.Handle("mask", func(ctx structwalker.FieldContext) error {
    if ctx.Tag.Name == "redact" {
        return ctx.SetValue("***REDACTED***")
    }
    return nil
})

type Response struct {
    UserID int    `json:"user_id"`
    Email  string `json:"email" mask:"redact"`
    SSN    string `json:"ssn" mask:"redact"`
}

resp := &Response{UserID: 1, Email: "alice@example.com", SSN: "123-45-6789"}
w.Walk(resp)
// resp.Email == "***REDACTED***"
// resp.SSN == "***REDACTED***"
Global Handlers

HandleAll registers a handler invoked for every field, regardless of tags:

w.HandleAll(func(ctx structwalker.FieldContext) error {
    fmt.Printf("[audit] %s = %v\n", ctx.Path, ctx.Value.Interface())
    return nil
})
Handler Options
Priority

Multiple handlers for the same tag key execute in priority order (lower runs first). Default priority is 100.

w.Handle("validate", validateRequired, structwalker.WithPriority(10))  // runs first
w.Handle("validate", validateFormat, structwalker.WithPriority(50))    // runs second
w.Handle("validate", logValidation, structwalker.WithPriority(200))    // runs last
Filter

Only invoke the handler when a predicate returns true:

// Only validate root-level fields
w.Handle("validate", handler, structwalker.WithFilter(func(ctx structwalker.FieldContext) bool {
    return ctx.Depth == 0
}))

// Only handle string fields
w.Handle("mask", handler, structwalker.WithFilter(func(ctx structwalker.FieldContext) bool {
    return ctx.Value.Kind() == reflect.String
}))
Phase

Handlers run before recursing into child fields by default. Use AfterChildren to run after:

// Runs before walking nested struct fields
w.Handle("tag", beforeHandler)

// Runs after walking nested struct fields
w.Handle("tag", afterHandler, structwalker.WithPhase(structwalker.AfterChildren))

Execution order for a nested struct:

before handler: Parent
  before handler: Parent.Child
  after handler:  Parent.Child
after handler:  Parent

Walker Options

All options are set at walker creation time:

w := structwalker.New(
    structwalker.WithMaxDepth(10),
    structwalker.WithUnexported(true),
    structwalker.WithFollowPointers(false),
    structwalker.WithSliceElements(true),
    structwalker.WithMapEntries(true),
    structwalker.WithErrorMode(structwalker.CollectErrors),
    structwalker.WithContext(ctx),
    structwalker.WithTagParser(myCustomParser),
)
Option Reference
Option Default Description
WithMaxDepth(n) 32 Maximum recursion depth before returning an error
WithUnexported(bool) false Include unexported fields (read-only, not settable)
WithFollowPointers(bool) true Dereference pointers during walk
WithSliceElements(bool) false Walk into individual slice/array elements
WithMapEntries(bool) false Walk into map values
WithErrorMode(mode) StopOnError Error handling strategy (see below)
WithContext(ctx) context.Background() Context for cancellation support
WithTagParser(fn) built-in CSV Override the tag parsing function
Error Modes
Mode Behavior
StopOnError First handler error halts the walk (default)
CollectErrors Collect all errors, return as *MultiError
SkipOnError Ignore handler errors, continue walking
// Collect all validation errors at once
w := structwalker.New(structwalker.WithErrorMode(structwalker.CollectErrors))
w.Handle("validate", func(ctx structwalker.FieldContext) error {
    if ctx.Tag.Name == "required" && ctx.IsZero() {
        return fmt.Errorf("field is required")
    }
    return nil
})

err := w.Walk(&myStruct)
if err != nil {
    var multi *structwalker.MultiError
    if errors.As(err, &multi) {
        for _, e := range multi.Errors {
            fmt.Printf("%s: %s\n", e.Path, e.Err)
        }
    }
}
Custom Tag Parser

Override the default CSV-based tag parser for alternative formats:

w := structwalker.New(structwalker.WithTagParser(func(key, raw string) structwalker.Tag {
    // Custom parsing logic
    return structwalker.Tag{
        Key:  key,
        Name: raw,
        Raw:  raw,
    }
}))

Flow Control

Handlers can return sentinel errors to control walk behavior.

SkipField

Stop processing remaining handlers on the current field and move to the next:

w.Handle("json", func(ctx structwalker.FieldContext) error {
    if ctx.Tag.IsIgnored() {
        return structwalker.ErrSkipField
    }
    // process field...
    return nil
})
SkipChildren

Process the current field's handlers but skip recursion into its nested struct fields:

w.Handle("audit", func(ctx structwalker.FieldContext) error {
    if ctx.Tag.Name == "skip" {
        return structwalker.SkipChildren
    }
    return nil
})

type Config struct {
    Name    string  `json:"name"`
    Secrets Secrets `json:"secrets" audit:"skip"` // won't recurse into Secrets
}

FieldContext

Every handler receives a FieldContext with full field information.

Fields
Field Type Description
Tag Tag Parsed tag for this handler's registered key
AllTags []Tag Every parsed tag on the field
Field reflect.StructField Reflection field metadata
Value reflect.Value Field value (settable if walker received a pointer)
Path string Dot-separated path from root: "Address.Street", "Items[2].Name"
Depth int Nesting level (0 = root struct fields)
Parent reflect.Value The containing struct's reflect.Value
Index int Field index within the parent struct
Walker *Walker Reference to the walker instance
SetValue

Sets a field's value with type coercion. The walker must receive a pointer for fields to be settable.

Supported coercions from string:

  • string to int, int8, int16, int32, int64
  • string to uint, uint8, uint16, uint32, uint64
  • string to float32, float64
  • string to bool
  • string to time.Time (RFC 3339 format)
w.Handle("default", func(ctx structwalker.FieldContext) error {
    if ctx.IsZero() {
        return ctx.SetValue(ctx.Tag.Name) // coerces "42" to int, "true" to bool, etc.
    }
    return nil
})

type Config struct {
    Port    int    `default:"8080"`
    Debug   bool   `default:"false"`
    Timeout string `default:"30s"`
}

Setting to nil resets the field to its zero value:

ctx.SetValue(nil) // resets to zero value
Helper Methods
ctx.IsZero() bool      // true if the field is the zero value for its type
ctx.IsNil() bool       // true if the field is a nil pointer, slice, map, or interface
ctx.IsExported() bool  // true if the field is exported

Walking Nested Structures

Nested Structs

Struct fields are recursed into automatically. The Path reflects the nesting:

type Address struct {
    Street string `json:"street"`
    City   string `json:"city"`
}
type User struct {
    Name    string  `json:"name"`
    Address Address `json:"address"`
}

// Handler receives paths: "Name", "Address", "Address.Street", "Address.City"
Pointers

Pointers to structs are dereferenced automatically (configurable with WithFollowPointers). Nil pointers are skipped. Circular references are detected and broken.

type Node struct {
    Name string `json:"name"`
    Next *Node  `json:"next"`
}

a := &Node{Name: "a", Next: &Node{Name: "b"}}
a.Next.Next = a // circular reference, handled safely
Slices

Enable with WithSliceElements(true). Each element gets a path like Items[0].Name:

type Item struct {
    Name string `tag:"name"`
}
type Cart struct {
    Items []Item `tag:"items"`
}

w := structwalker.New(structwalker.WithSliceElements(true))
// Paths: "Items", "Items[0].Name", "Items[1].Name"

Nil elements in pointer slices are skipped.

Maps

Enable with WithMapEntries(true). Map keys appear in the path:

type Config struct {
    Settings map[string]Setting `tag:"settings"`
}

w := structwalker.New(structwalker.WithMapEntries(true))
// Paths: "Settings", "Settings[database].Host", "Settings[database].Port"

Middleware

Middleware wraps every handler invocation for logging, timing, error recovery, or custom logic.

w := structwalker.New()
w.Use(myMiddleware1, myMiddleware2)

Middleware executes in order, wrapping the handler from outside in:

middleware1 > middleware2 > handler > middleware2 > middleware1
Built-in Middleware

WithRecovery catches panics in handlers and converts them to errors:

w.Use(structwalker.WithRecovery())

WithLogging logs every field visit at debug level using log/slog:

w.Use(structwalker.WithLogging(slog.Default()))

WithTiming records handler execution time via a callback:

w.Use(structwalker.WithTiming(func(path, tagKey string, d time.Duration) {
    metrics.RecordHistogram("structwalker.handler.duration", d,
        "path", path,
        "tag", tagKey,
    )
}))
Custom Middleware
func RequireExported() structwalker.Middleware {
    return func(ctx structwalker.FieldContext, next structwalker.HandlerFunc) error {
        if !ctx.IsExported() {
            return nil // skip unexported fields
        }
        return next(ctx)
    }
}

w.Use(RequireExported())

Context Cancellation

Pass a context to cancel long-running walks:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

w := structwalker.New(structwalker.WithContext(ctx))
err := w.Walk(&largeStruct)
if errors.Is(err, context.DeadlineExceeded) {
    // walk timed out
}

The context is checked between each field, so cancellation responds quickly even on large structs.


Error Handling

Handler errors are wrapped in *WalkError with field path context:

type WalkError struct {
    Path   string // "Config.Database.Host"
    TagKey string // "validate"
    Field  string // "Host"
    Err    error  // underlying error
}

WalkError supports errors.Is and errors.As through Unwrap:

err := w.Walk(&myStruct)

var walkErr *structwalker.WalkError
if errors.As(err, &walkErr) {
    fmt.Printf("field %s failed: %v\n", walkErr.Path, walkErr.Err)
}

With CollectErrors mode, *MultiError also supports errors.Is across all collected errors:

var multi *structwalker.MultiError
if errors.As(err, &multi) {
    for _, e := range multi.Errors {
        fmt.Printf("  %s: %v\n", e.Path, e.Err)
    }
}

Reflection Caching

Type metadata (field info, parsed tags, handler key matching) is parsed once per type on first walk and cached with sync.Map. Later walks of the same struct type skip parsing entirely.

  • Cache hits: ~5ns, zero allocations
  • Safe for concurrent use
  • Invalidated automatically when new handlers are registered with Handle

Concurrency

A Walker is safe for concurrent use after handler registration is complete. The reflection cache uses sync.Map. Registering new handlers (Handle, HandleAll) during concurrent walks invalidates the cache and may cause brief re-parsing overhead.

// Set up once
w := structwalker.New()
w.Handle("json", jsonHandler)
w.Handle("validate", validateHandler)
w.Use(structwalker.WithRecovery())

// Use concurrently
go func() { w.Walk(&struct1) }()
go func() { w.Walk(&struct2) }()

Benchmarks

cpu: Apple M4 Pro
BenchmarkWalk_Flat-14              1000000     1185 ns/op     360 B/op    13 allocs/op
BenchmarkWalk_Nested-14             380620     3226 ns/op     960 B/op    39 allocs/op
BenchmarkWalk_Deep-14              1000000     1072 ns/op     376 B/op    14 allocs/op
BenchmarkWalk_MultipleHandlers-14   819598     1508 ns/op     528 B/op    19 allocs/op
BenchmarkWalk_WithMiddleware-14     895304     1331 ns/op     480 B/op    18 allocs/op
BenchmarkWalk_HandleAll-14         1427517      837 ns/op     360 B/op    13 allocs/op
BenchmarkParseTag-14               8229410      147 ns/op     416 B/op     4 allocs/op
BenchmarkParseAllTags-14           3281874      378 ns/op    1088 B/op    11 allocs/op
BenchmarkRawReflection-14        21606843       56 ns/op       0 B/op     0 allocs/op
BenchmarkTypeCache_Hit-14       216546840        5 ns/op       0 B/op     0 allocs/op

Run benchmarks yourself:

go test -bench=. -benchmem ./...

License

MIT

Documentation

Overview

Package structwalker provides recursive struct walking with tag-based handler dispatch.

Register handlers for struct tag keys, then walk any struct. Each field's tags are parsed, matched to registered handlers, and dispatched with full context including the parsed tag, field value, dot-separated path, and parent struct.

Reflection metadata is cached per-type so repeated walks skip parsing entirely.

Basic usage:

w := structwalker.New()
w.Handle("json", func(ctx structwalker.FieldContext) error {
    fmt.Println(ctx.Path, ctx.Tag.Name)
    return nil
})
err := w.Walk(&myStruct)

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrSkipField causes the walker to skip remaining handlers on the current field.
	ErrSkipField = errors.New("skip field")

	// ErrSkipChildren prevents recursion into a nested struct field.
	ErrSkipChildren = errors.New("skip children")
)

Functions

This section is empty.

Types

type ErrorMode

type ErrorMode int

ErrorMode controls how the walker handles errors from handlers.

const (
	StopOnError   ErrorMode = iota // first error halts the walk
	CollectErrors                  // collect all errors, return as *MultiError
	SkipOnError                    // ignore errors, continue walking
)

type FieldContext

type FieldContext struct {
	Tag     Tag
	AllTags []Tag
	Field   reflect.StructField
	Value   reflect.Value
	Path    string
	Depth   int
	Parent  reflect.Value
	Index   int
	Walker  *Walker
}

FieldContext is passed to every handler invocation with full field metadata.

func (FieldContext) IsExported

func (fc FieldContext) IsExported() bool

IsExported reports whether the field is exported.

func (FieldContext) IsNil

func (fc FieldContext) IsNil() bool

IsNil reports whether the field is a nil pointer, slice, map, or interface.

func (FieldContext) IsZero

func (fc FieldContext) IsZero() bool

IsZero reports whether the field holds the zero value for its type.

func (FieldContext) SetValue

func (fc FieldContext) SetValue(v any) error

SetValue sets the field value with automatic type coercion. Supports string-to-numeric, string-to-bool, and string-to-time.Time (RFC 3339).

type HandlerFunc

type HandlerFunc func(ctx FieldContext) error

HandlerFunc is the callback signature for tag processors.

type HandlerOption

type HandlerOption func(*handlerConfig)

HandlerOption configures individual handler behavior.

func WithFilter

func WithFilter(fn func(FieldContext) bool) HandlerOption

WithFilter restricts the handler to fields where fn returns true.

func WithPhase

func WithPhase(phase Phase) HandlerOption

WithPhase sets when the handler runs relative to child processing.

func WithPriority

func WithPriority(p int) HandlerOption

WithPriority sets execution order (lower runs first). Default: 100.

type Middleware

type Middleware func(ctx FieldContext, next HandlerFunc) error

Middleware wraps handler execution for cross-cutting concerns.

func WithLogging

func WithLogging(logger *slog.Logger) Middleware

WithLogging returns middleware that logs field visits at debug level.

func WithRecovery

func WithRecovery() Middleware

WithRecovery returns middleware that converts handler panics into errors.

Example
package main

import (
	"fmt"

	"github.com/rjp2525/structwalker"
)

func main() {
	type S struct {
		Name string `tag:"name"`
	}

	w := structwalker.New()
	w.Use(structwalker.WithRecovery())
	w.Handle("tag", func(ctx structwalker.FieldContext) error {
		panic("something went wrong")
	})

	err := w.Walk(&S{Name: "test"})
	fmt.Println("recovered:", err != nil)

}
Output:
recovered: true

func WithTiming

func WithTiming(fn func(path string, tagKey string, duration time.Duration)) Middleware

WithTiming returns middleware that records handler execution duration.

type MultiError

type MultiError struct {
	Errors []*WalkError
}

MultiError collects multiple errors from a walk (used with CollectErrors mode).

func (*MultiError) Error

func (e *MultiError) Error() string

func (*MultiError) Unwrap

func (e *MultiError) Unwrap() []error

type Phase

type Phase int

Phase determines when a handler runs relative to child field processing.

const (
	// BeforeChildren runs the handler before recursing into nested struct fields.
	BeforeChildren Phase = iota
	// AfterChildren runs the handler after nested struct fields have been processed.
	AfterChildren
)

type Tag

type Tag struct {
	Key     string
	Name    string
	Raw     string
	Flags   []string
	Options map[string]string
}

Tag represents a single parsed struct tag value. For example, `scan:"email,redact,max=255"` parses to: Key="scan", Name="email", Flags=["redact"], Options={"max":"255"}.

func ParseAllTags

func ParseAllTags(tag reflect.StructTag) []Tag

ParseAllTags extracts all tags from a reflect.StructTag.

func ParseTag

func ParseTag(key, raw string) Tag

ParseTag parses a raw tag value string "name,flag1,key=value" into a Tag.

func (Tag) HasFlag

func (t Tag) HasFlag(flag string) bool

HasFlag reports whether the tag contains the given flag.

func (Tag) IsIgnored

func (t Tag) IsIgnored() bool

IsIgnored reports whether the tag name is "-", the conventional ignore marker.

func (Tag) Option

func (t Tag) Option(key string) (string, bool)

Option returns the value for a key=value option and whether it exists.

func (Tag) OptionOr

func (t Tag) OptionOr(key, fallback string) string

OptionOr returns the option value for key, or fallback if not present.

type WalkError

type WalkError struct {
	Path   string
	TagKey string
	Field  string
	Err    error
}

WalkError wraps an error with field path context.

func (*WalkError) Error

func (e *WalkError) Error() string

func (*WalkError) Unwrap

func (e *WalkError) Unwrap() error

type Walker

type Walker struct {
	// contains filtered or unexported fields
}

Walker is the entry point. Register handlers for tag keys, then Walk any struct.

Example (BasicUsage)
package main

import (
	"fmt"

	"github.com/rjp2525/structwalker"
)

func main() {
	type User struct {
		Name  string `json:"name" validate:"required"`
		Email string `json:"email" validate:"email"`
		Age   int    `json:"age" validate:"min=0"`
	}

	w := structwalker.New()

	w.Handle("json", func(ctx structwalker.FieldContext) error {
		fmt.Printf("json field: %s -> %s\n", ctx.Path, ctx.Tag.Name)
		return nil
	})

	w.Handle("validate", func(ctx structwalker.FieldContext) error {
		fmt.Printf("validate: %s rule=%s\n", ctx.Path, ctx.Tag.Name)
		return nil
	})

	user := &User{Name: "Alice", Email: "alice@example.com", Age: 30}
	if err := w.Walk(user); err != nil {
		fmt.Println("error:", err)
	}

}
Output:
json field: Name -> name
validate: Name rule=required
json field: Email -> email
validate: Email rule=email
json field: Age -> age
validate: Age rule=min=0
Example (ErrorCollection)
package main

import (
	"fmt"

	"github.com/rjp2525/structwalker"
)

func main() {
	type Form struct {
		Name  string `validate:"required"`
		Email string `validate:"required"`
		Age   int    `validate:"required"`
	}

	w := structwalker.New(structwalker.WithErrorMode(structwalker.CollectErrors))
	w.Handle("validate", func(ctx structwalker.FieldContext) error {
		if ctx.Tag.Name == "required" && ctx.IsZero() {
			return fmt.Errorf("field is required")
		}
		return nil
	})

	form := &Form{} // all zero values
	err := w.Walk(form)
	if err != nil {
		fmt.Println("Validation failed:")
		var multi *structwalker.MultiError
		if ok := err.(*structwalker.MultiError); ok != nil {
			multi = ok
			for _, e := range multi.Errors {
				fmt.Printf("  - %s: %s\n", e.Path, e.Err)
			}
		}
	}

}
Output:
Validation failed:
  - Name: field is required
  - Email: field is required
  - Age: field is required
Example (HandleAll)
package main

import (
	"fmt"
	"strings"

	"github.com/rjp2525/structwalker"
)

func main() {
	type Config struct {
		Host string
		Port int
		Name string
	}

	var fields []string
	w := structwalker.New()
	w.HandleAll(func(ctx structwalker.FieldContext) error {
		fields = append(fields, ctx.Field.Name)
		return nil
	})

	if err := w.Walk(&Config{Host: "localhost", Port: 8080, Name: "app"}); err != nil {
		fmt.Println("error:", err)
	}
	fmt.Println(strings.Join(fields, ", "))

}
Output:
Host, Port, Name
Example (NestedStructs)
package main

import (
	"fmt"

	"github.com/rjp2525/structwalker"
)

func main() {
	type Address struct {
		Street string `json:"street"`
		City   string `json:"city"`
	}
	type User struct {
		Name    string  `json:"name"`
		Address Address `json:"address"`
	}

	w := structwalker.New()
	w.Handle("json", func(ctx structwalker.FieldContext) error {
		fmt.Printf("[depth=%d] %s\n", ctx.Depth, ctx.Path)
		return nil
	})

	user := &User{Name: "Alice", Address: Address{Street: "123 Main St", City: "Springfield"}}
	if err := w.Walk(user); err != nil {
		fmt.Println("error:", err)
	}

}
Output:
[depth=0] Name
[depth=0] Address
[depth=1] Address.Street
[depth=1] Address.City
Example (SensitiveDataMasking)
package main

import (
	"fmt"

	"github.com/rjp2525/structwalker"
)

func main() {
	type APIResponse struct {
		UserID    int    `json:"user_id"`
		Email     string `json:"email" mask:"redact"`
		SSN       string `json:"ssn" mask:"redact"`
		PublicKey string `json:"public_key"`
	}

	w := structwalker.New()
	w.Handle("mask", func(ctx structwalker.FieldContext) error {
		if ctx.Tag.Name == "redact" {
			return ctx.SetValue("***REDACTED***")
		}
		return nil
	})

	resp := &APIResponse{
		UserID:    123,
		Email:     "alice@example.com",
		SSN:       "123-45-6789",
		PublicKey: "ssh-rsa AAAA...",
	}
	if err := w.Walk(resp); err != nil {
		fmt.Println("error:", err)
	}

	fmt.Println("Email:", resp.Email)
	fmt.Println("SSN:", resp.SSN)
	fmt.Println("PublicKey:", resp.PublicKey)

}
Output:
Email: ***REDACTED***
SSN: ***REDACTED***
PublicKey: ssh-rsa AAAA...
Example (SkipChildren)
package main

import (
	"fmt"

	"github.com/rjp2525/structwalker"
)

func main() {
	type Secrets struct {
		APIKey string `json:"api_key"`
		Token  string `json:"token"`
	}
	type Config struct {
		Name    string  `json:"name"`
		Secrets Secrets `json:"secrets" audit:"skip"`
	}

	w := structwalker.New()
	w.Handle("json", func(ctx structwalker.FieldContext) error {
		fmt.Printf("visited: %s\n", ctx.Path)
		return nil
	})
	w.Handle("audit", func(ctx structwalker.FieldContext) error {
		if ctx.Tag.Name == "skip" {
			return structwalker.ErrSkipChildren
		}
		return nil
	})

	if err := w.Walk(&Config{Name: "myapp", Secrets: Secrets{APIKey: "key", Token: "tok"}}); err != nil {
		fmt.Println("error:", err)
	}

}
Output:
visited: Name
visited: Secrets
Example (SliceWalking)
package main

import (
	"fmt"

	"github.com/rjp2525/structwalker"
)

func main() {
	type Item struct {
		Name string `tag:"name"`
	}
	type Cart struct {
		Items []Item `tag:"items"`
	}

	w := structwalker.New(structwalker.WithSliceElements(true))
	w.Handle("tag", func(ctx structwalker.FieldContext) error {
		fmt.Printf("%s\n", ctx.Path)
		return nil
	})

	cart := &Cart{Items: []Item{{Name: "Widget"}, {Name: "Gadget"}}}
	if err := w.Walk(cart); err != nil {
		fmt.Println("error:", err)
	}

}
Output:
Items
Items[0].Name
Items[1].Name

func New

func New(opts ...WalkerOption) *Walker

New creates a Walker configured with the given options.

func (*Walker) Handle

func (w *Walker) Handle(tagKey string, fn HandlerFunc, opts ...HandlerOption)

Handle registers a handler for a specific tag key.

func (*Walker) HandleAll

func (w *Walker) HandleAll(fn HandlerFunc, opts ...HandlerOption)

HandleAll registers a handler invoked for every field regardless of tags.

func (*Walker) Use

func (w *Walker) Use(mw ...Middleware)

Use adds middleware to the walker's chain, executed in the order added.

func (*Walker) Walk

func (w *Walker) Walk(v any) error

Walk processes the given struct. Pass a pointer for fields to be settable.

func (*Walker) WalkValue

func (w *Walker) WalkValue(v reflect.Value) error

WalkValue processes a reflect.Value directly.

type WalkerOption

type WalkerOption func(*walkerOptions)

WalkerOption configures walker behavior via functional options.

func WithContext

func WithContext(ctx context.Context) WalkerOption

WithContext attaches a context for cancellation support during walks.

func WithErrorMode

func WithErrorMode(mode ErrorMode) WalkerOption

WithErrorMode sets the error handling strategy for the walk.

func WithFollowPointers

func WithFollowPointers(b bool) WalkerOption

WithFollowPointers controls automatic pointer dereferencing (default: true).

func WithMapEntries

func WithMapEntries(b bool) WalkerOption

WithMapEntries enables walking into map values.

func WithMaxDepth

func WithMaxDepth(n int) WalkerOption

WithMaxDepth limits recursion depth (default: 32).

func WithSliceElements

func WithSliceElements(b bool) WalkerOption

WithSliceElements enables walking into individual slice/array elements.

func WithTagParser

func WithTagParser(fn func(key, raw string) Tag) WalkerOption

WithTagParser overrides the default CSV-based tag parsing logic.

func WithUnexported

func WithUnexported(b bool) WalkerOption

WithUnexported includes unexported fields in the walk (read-only, not settable).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL