Empty Interfaces — Professional Level¶
Production Patterns¶
Pattern 1: Event payload¶
type Event struct {
Name string
Time time.Time
Payload any
}
func Publish(e Event) {
// any for the event bus — accepts various payload types
}
Pattern 2: Plugin config¶
Pattern 3: Generic logger fields¶
type Logger interface {
Info(msg string, fields ...any)
}
logger.Info("user login", "userID", "u1", "ip", "127.0.0.1")
Pattern 4: Functional options as map¶
But functional options are often preferred:
Pattern 5: ORM dynamic query¶
Anti-patterns¶
1. any instead of generics¶
2. any in domain entity¶
// Bad
type User struct {
ID any
Data any
}
// Good
type User struct {
ID UserID
Name string
Email Email
}
3. any for everything¶
// Bad
func Process(input any) any { ... }
// Good
func Process(input ProcessInput) (ProcessOutput, error) { ... }
4. Type assertion chains¶
// Bad
v, _ := input.(map[string]any)
sub, _ := v["data"].(map[string]any)
list, _ := sub["items"].([]any)
first, _ := list[0].(string)
// Good — typed struct
var data Data
json.Unmarshal(payload, &data)
Library API Design¶
Public API — any rare¶
// Standard library style
func Marshal(v any) ([]byte, error) // json: any makes sense
func (l *log.Logger) Print(v ...any) // logger: any makes sense
func New(name string, args ...any) *Cmd // exec.Command: any makes sense
any — at input boundaries, with generic semantics.
Internal — concrete types¶
The library's internal implementation uses concrete types and generics.
Documentation¶
// Marshal returns the JSON encoding of v.
//
// v can be any Go value. Map keys must be strings or implement
// encoding.TextMarshaler. Channel, complex, and function values
// cannot be encoded.
func Marshal(v any) ([]byte, error) { ... }
It is important to document the types accepted by any.
Migration to Generics¶
Migrate when:¶
- Same algorithm + different types
- Type-safe container
- Performance-sensitive
Don't migrate when:¶
- Heterogeneous collection
- JSON dynamic data
- Uses reflect
- Public API boundary
Example: gradual¶
// v1
type Cache struct{ m map[string]any }
// v1.5 — both old and new
type CacheGeneric[T any] struct{ m map[string]T }
// v2 — old is removed
Documentation Standards¶
// Decode parses payload as JSON into a generic structure.
//
// The result is one of:
// - map[string]any (JSON object)
// - []any (JSON array)
// - float64, string, bool, nil (JSON primitive)
//
// Use a typed structure with json.Unmarshal for known schemas.
func Decode(payload []byte) (any, error) { ... }
Linter Rules¶
gocritic—interfaceUsage:anyoveruse warningunconvert— Unnecessary type assertiongomnd— Magic numbers (often hides type knowledge)forcetypeassert— Single-value type assertion warning
Custom lint:
Cheat Sheet¶
PRODUCTION any USE
─────────────────────
✓ Event payload
✓ Plugin config
✓ Logger fields
✓ JSON dynamic
✓ ORM query
✗ Domain entity ID/data
✗ Same-type container
✗ Algorithm input
LIBRARY API
─────────────────────
Public boundary: any OK
Internal: concrete + generics
Documentation matters
GENERICS MIGRATION
─────────────────────
Migrate: same algo + types, container, hot path
Skip: heterogeneous, JSON dynamic, reflect
DOCUMENTATION
─────────────────────
Document accepted types
JSON unmarshal result map/list/primitive
State boundaries clearly
Summary¶
Professional any: - Production patterns: event payload, plugin config, dynamic query - Anti-patterns: domain entity, hot path, type assertion chains - Library API: public boundary OK, internal concrete - Migration to generics — gradual, selective - Documentation: accepted types matter - Linter — overuse warning
any is Go's powerful boundary tool. Concrete types in the domain core; any at integration boundaries. This is a deliberate decision.