-
Couldn't load subscription status.
- Fork 197
Structs with Generics
Since v1.5.0 it is possible to generate code with Go generics (type parameters), allowing you to create reusable, type-safe serialization code for generic structs. This guide explains how to use this feature effectively.
- Basic Usage
- How It Works
- Examples
- Working with Nested Generics
- Multiple Type Parameters
- Limitations and Considerations
- Common Patterns
- Troubleshooting
To enable code generation for generic types, you need to add a special constraint using msgp.RTFor[T] (RoundTripper For):
//go:generate msgp
// Basic generic struct with msgp support
type Container[T any, _ msgp.RTFor[T]] struct {
Value T
Items []T
}The msgp.RTFor[T] constraint ensures that the type parameter T supports all necessary MsgPack operations (encoding, decoding, marshaling, unmarshaling, and sizing).
msgp.RTFor[T] is defined as:
type RTFor[T any] interface {
PtrTo[T] // Must be a pointer to T
RT // Must implement all msgp interfaces
}This constraint ensures that:
- The type can be instantiated as a pointer (
*T) - The type implements all required msgp interfaces (
Decodable,Encodable,Sizer,Unmarshaler,Marshaler)
You can use either named or unnamed constraint parameters:
// Named parameter - useful when passing to nested types
type Foo[T any, P msgp.RTFor[T]] struct {
Nested Bar[T, P] // Pass P to nested type
}
// Unnamed parameter - when not needed elsewhere
type Baz[T any, _ msgp.RTFor[T]] struct {
Value T
}You must use the msgp.RTFor, otherwise it will not be picked up be the code generator.
//go:generate msgp
// A generic wrapper that can hold any msgp-serializable type
type Wrapper[T any, _ msgp.RTFor[T]] struct {
Data T `msg:"data"`
Timestamp int64 `msg:"timestamp"`
Metadata map[string]T `msg:"metadata,allownil"`
}
// Usage with a custom type
type User struct {
ID int `msg:"id"`
Name string `msg:"name"`
}
// After generation, you can use:
// var w Wrapper[User, *User]//go:generate msgp
// Generic list with msgp support
type List[T any, _ msgp.RTFor[T]] struct {
Items []T `msg:"items,allownil"`
Count int `msg:"count"`
MaxSize int `msg:"max_size"`
}
// Generic node for linked structures
type Node[T any, P msgp.RTFor[T]] struct {
Value T `msg:"value"`
Next *Node[T, P] `msg:"next,allownil"`
}When using nested generic types, pass the constraint parameter to maintain type consistency:
//go:generate msgp
// Parent type with named constraint parameter
type Parent[T any, P msgp.RTFor[T]] struct {
Direct T `msg:"direct"`
Child Child[T, P] `msg:"child"`
Children []Child[T, P] `msg:"children,allownil"`
ChildMap map[string]Child[T, P] `msg:"child_map,allownil"`
}
// Child type reusing the parent's constraint
type Child[T any, _ msgp.RTFor[T]] struct {
Value T `msg:"value"`
IsValid bool `msg:"is_valid"`
}//go:generate msgp
// Multi-level generic nesting
type Level1[T any, P msgp.RTFor[T]] struct {
Data T
Level2 Level2[T, P, string]
Pointer *Level2[T, P, int]
}
type Level2[T any, P msgp.RTFor[T], K any] struct {
Primary T
Secondary K
Level3 []Level3[T, P]
}
type Level3[T any, _ msgp.RTFor[T]] struct {
Final T
}You can use multiple generic type parameters, each with its own constraint:
//go:generate msgp
// Struct with two generic types
type Pair[A, B any, AP msgp.RTFor[A], BP msgp.RTFor[B]] struct {
First A `msg:"first"`
Second B `msg:"second"`
AList []A `msg:"a_list,allownil"`
BMap map[string]B `msg:"b_map,allownil"`
}
// Alternative syntax with unnamed constraints
type DualContainer[A, B any, _ msgp.RTFor[A], _ msgp.RTFor[B]] struct {
Primary A
Secondary B
}-
No Primitive Types: Generic type parameters cannot be primitive types directly. They must be types that support marshaling.
// Won't work: Container[int, *int] // Will work: Container[MyInt, *MyInt] where type MyInt int
-
Testing Limitations: The code generator cannot create tests for generic types since it cannot reliably instantiate arbitrary type parameters.
-
No RTFor in Struct Fields: Never use
msgp.RTFor[T]types as actual struct fields:// WRONG - will cause runtime crashes type Bad[T any, P msgp.RTFor[T]] struct { Value T Constraint P // Don't do this! } // CORRECT type Good[T any, P msgp.RTFor[T]] struct { Value T Nested Another[T, P] // Pass P to nested types only }
-
Warnings Expected: You may see warnings like:
warn: generics.go: MyStruct: Value: possible non-local identifier: T
These are expected and can be safely ignored for generic type parameters. These may be removed in future versions.
Some preprocessor directives may exhibit unexpected behavior with generic types. Test thoroughly when using:
- Custom marshaling directives
- Ignore directives
- Tuple directives (basic support shown in examples)
//go:generate msgp
type Response[T any, _ msgp.RTFor[T]] struct {
Success bool `msg:"success"`
Data T `msg:"data"`
Error *string `msg:"error,allownil"`
Meta map[string]string `msg:"meta,allownil"`
}//go:generate msgp
type CacheEntry[T any, _ msgp.RTFor[T]] struct {
Key string `msg:"key"`
Value T `msg:"value"`
Expiry int64 `msg:"expiry"`
AccessCount int `msg:"access_count"`
}//go:generate msgp
type TreeNode[T any, P msgp.RTFor[T]] struct {
Value T `msg:"value"`
Children []*TreeNode[T, P] `msg:"children,allownil"`
Parent *TreeNode[T, P] `msg:"parent,omitempty"`
}-
"Cannot instantiate generic type" errors
- Ensure all type parameters have proper
msgp.RTForconstraints - Check that nested types pass constraint parameters correctly
- Ensure all type parameters have proper
-
Runtime panics when creating instances
- Never use
msgp.RTFor[T]types as struct fields - Ensure type parameters are used correctly (T for values, P for constraints)
- Never use
-
Code generation fails
- Verify that the base types you're using support msgp generation
- Check that custom types in the same file have generation directives
-
"Possible non-local identifier" warnings
- These are expected for generic type parameters
- The generated code will still work correctly
Generic types can be used with the tuple directive:
//go:generate msgp
//msgp:tuple GenericTuple
type GenericTuple[T any, P msgp.RTFor[T]] struct {
First T
Second T
Third []T
}Here's a comprehensive example showing various features:
package example
//go:generate msgp
import "github.com/tinylib/msgp/msgp"
// Custom type that will be used with generics
type UserID int
// Generic message queue item
type QueueItem[T any, P msgp.RTFor[T]] struct {
ID string `msg:"id"`
Priority int `msg:"priority"`
Payload T `msg:"payload"`
Metadata map[string]string `msg:"metadata,allownil"`
Timestamp int64 `msg:"timestamp"`
Retries int `msg:"retries"`
}
// Generic batch processor
type Batch[T any, P msgp.RTFor[T]] struct {
Items []T `msg:"items"`
ProcessedAt int64 `msg:"processed_at"`
Results []Result[T, P] `msg:"results,allownil"`
}
// Generic result wrapper
type Result[T any, _ msgp.RTFor[T]] struct {
Success bool `msg:"success"`
Data *T `msg:"data,allownil"`
Error *string `msg:"error,allownil"`
}
// Usage after code generation:
// var queue QueueItem[UserID, *UserID]
// var batch Batch[UserID, *UserID]