Skip to content

Structs with Generics

Klaus Post edited this page Oct 27, 2025 · 1 revision

Code Generation with Generic Types

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.

Table of Contents

Basic Usage

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).

How It Works

The RTFor Constraint

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:

  1. The type can be instantiated as a pointer (*T)
  2. The type implements all required msgp interfaces (Decodable, Encodable, Sizer, Unmarshaler, Marshaler)

Naming Convention

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.

Examples

Simple Generic Container

//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]

Generic List Implementation

//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"`
}

Working with Nested Generics

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"`
}

Complex Nesting Example

//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
}

Multiple Type Parameters

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
}

Limitations and Considerations

Important Limitations

  1. 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
  2. Testing Limitations: The code generator cannot create tests for generic types since it cannot reliably instantiate arbitrary type parameters.

  3. 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
    }
  4. 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.

Directive Compatibility

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)

Common Patterns

Generic Response Wrapper

//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"`
}

Generic Cache Entry

//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"`
}

Generic Tree Structure

//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"`
}

Troubleshooting

Common Issues and Solutions

  1. "Cannot instantiate generic type" errors

    • Ensure all type parameters have proper msgp.RTFor constraints
    • Check that nested types pass constraint parameters correctly
  2. 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)
  3. 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
  4. "Possible non-local identifier" warnings

    • These are expected for generic type parameters
    • The generated code will still work correctly

Using with Tuples

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
}

Complete Example

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]
Clone this wiki locally