Menu
Coddy logo textTech

Generic Methods Workaround

Part of the Object Oriented Programming section of Coddy's GO journey — lesson 71 of 107.

Go has a notable limitation: you cannot define methods with their own type parameters on a struct. While generic structs work great, adding a method that introduces a new type parameter isn't allowed.

This code won't compile:

type Box[T any] struct {
    Value T
}

// ERROR: method must have no type parameters
func (b Box[T]) Convert[U any](fn func(T) U) U {
    return fn(b.Value)
}

The workaround is to use a standalone generic function instead of a method:

type Box[T any] struct {
    Value T
}

// Regular method - uses the struct's type parameter
func (b Box[T]) Get() T {
    return b.Value
}

// Standalone function - can have its own type parameters
func Convert[T, U any](b Box[T], fn func(T) U) U {
    return fn(b.Value)
}

func main() {
    intBox := Box[int]{Value: 42}
    
    // Use the method
    fmt.Println(intBox.Get())  // 42
    
    // Use the standalone function
    strResult := Convert(intBox, func(n int) string {
        return fmt.Sprintf("Number: %d", n)
    })
    fmt.Println(strResult)  // Number: 42
}

Methods on generic structs can still use the struct's type parameter T. The restriction only applies to introducing additional type parameters in the method signature.

When you need that flexibility, a generic function that takes the struct as its first argument achieves the same result.

challenge icon

Challenge

Easy

Let's build a data transformation toolkit that demonstrates the workaround for Go's limitation on generic methods! Since methods can't introduce new type parameters, you'll create standalone generic functions that achieve the same flexibility.

You'll organize your code across two files:

  • wrapper.go: Define your generic container and transformation functions.

    Create a generic struct Wrapper[T any] with a single field Value of type T.

    Add a method Get on Wrapper[T] that returns the wrapped value. This method uses the struct's type parameter, which is allowed.

    Create a standalone generic function Transform[T, U any](w Wrapper[T], fn func(T) U) U that applies the transformation function to the wrapper's value and returns the result. This function needs its own type parameter U for the output type, which is why it must be a standalone function rather than a method.

    Create another standalone function TransformToString[T any](w Wrapper[T]) string that converts the wrapped value to a string using fmt.Sprintf("%v", ...).

  • main.go: Demonstrate the workaround pattern with different transformations.

    Read a type indicator (int or string), then read a value. Create a Wrapper of the appropriate type.

    For int input: Create a Wrapper[int], then use Transform to double the value (returning an int), and use TransformToString to get a string representation.

    For string input: Create a Wrapper[string], then use Transform to get the string's length (returning an int), and use TransformToString to get the string representation.

    Print the results in this format:

    Original: [value]
    Transformed: [transformed value]
    As String: [string representation]

The following inputs will be provided:

  • Line 1: Type indicator (int or string)
  • Line 2: The value

For example, given:

int
25

Your output should be:

Original: 25
Transformed: 50
As String: 25

And given:

string
Hello World

Your output should be:

Original: Hello World
Transformed: 11
As String: Hello World

The key insight is that Transform takes a Wrapper[T] as its first argument and introduces a new type parameter U for the return type—something that wouldn't be possible as a method on the struct. This pattern gives you the flexibility of generic transformations while working within Go's type system constraints.

Cheat sheet

Go does not allow methods on generic structs to introduce their own type parameters. Methods can only use the struct's existing type parameters.

This code will not compile:

type Box[T any] struct {
    Value T
}

// ERROR: method must have no type parameters
func (b Box[T]) Convert[U any](fn func(T) U) U {
    return fn(b.Value)
}

The workaround is to use standalone generic functions instead of methods:

type Box[T any] struct {
    Value T
}

// Regular method - uses the struct's type parameter
func (b Box[T]) Get() T {
    return b.Value
}

// Standalone function - can have its own type parameters
func Convert[T, U any](b Box[T], fn func(T) U) U {
    return fn(b.Value)
}

func main() {
    intBox := Box[int]{Value: 42}
    
    // Use the method
    fmt.Println(intBox.Get())  // 42
    
    // Use the standalone function
    strResult := Convert(intBox, func(n int) string {
        return fmt.Sprintf("Number: %d", n)
    })
    fmt.Println(strResult)  // Number: 42
}

Methods on generic structs can use the struct's type parameter. The restriction only applies to introducing additional type parameters in the method signature. When you need that flexibility, use a generic function that takes the struct as its first argument.

Try it yourself

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	reader := bufio.NewReader(os.Stdin)
	
	// Read type indicator
	typeIndicator, _ := reader.ReadString('\n')
	typeIndicator = strings.TrimSpace(typeIndicator)
	
	// Read value
	value, _ := reader.ReadString('\n')
	value = strings.TrimSpace(value)
	
	if typeIndicator == "int" {
		// Parse the integer value
		num, _ := strconv.Atoi(value)
		
		// TODO: Create a Wrapper[int] with the parsed number
		
		// TODO: Use Transform to double the value (hint: pass a function that doubles)
		
		// TODO: Use TransformToString to get string representation
		
		// TODO: Print results in the required format:
		// Original: [value]
		// Transformed: [transformed value]
		// As String: [string representation]
		
		_ = num // Remove this line when you use num
	} else if typeIndicator == "string" {
		// TODO: Create a Wrapper[string] with the value
		
		// TODO: Use Transform to get the string's length (hint: pass a function that returns len())
		
		// TODO: Use TransformToString to get string representation
		
		// TODO: Print results in the required format:
		// Original: [value]
		// Transformed: [transformed value]
		// As String: [string representation]
		
		_ = value // Remove this line when you use value
	}
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming