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
EasyLet'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 fieldValueof typeT.Add a method
GetonWrapper[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) Uthat applies the transformation function to the wrapper's value and returns the result. This function needs its own type parameterUfor 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]) stringthat converts the wrapped value to a string usingfmt.Sprintf("%v", ...).main.go: Demonstrate the workaround pattern with different transformations.Read a type indicator (
intorstring), then read a value. Create aWrapperof the appropriate type.For
intinput: Create aWrapper[int], then useTransformto double the value (returning an int), and useTransformToStringto get a string representation.For
stringinput: Create aWrapper[string], then useTransformto get the string's length (returning an int), and useTransformToStringto 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 (
intorstring) - Line 2: The value
For example, given:
int
25Your output should be:
Original: 25
Transformed: 50
As String: 25And given:
string
Hello WorldYour output should be:
Original: Hello World
Transformed: 11
As String: Hello WorldThe 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
}
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Fundamentals of Go OOP
External FilesGo Workspace & ModulesPackages & ImportsExported vs Unexported NamesIntroduction to OOP in GoStructs as ClassesDefining Methods on StructsPointer vs Value ReceiversStruct InitializationConstructor FunctionsRecap - Simple Calculator4Interfaces
Introduction to InterfacesImplicit ImplementationInterface as ContractEmpty Interface (any)Type AssertionType SwitchInterface CompositionStringer & Error InterfacesRecap - Shape Calculator7Encapsulation
Exported vs Unexported FieldsPackage-Level EncapsulationGetter & Setter MethodsInformation Hiding in GoRecap - Student Records10Generics (Go 1.18+)
Introduction to GenericsType ParametersType ConstraintsGeneric StructsGeneric Methods WorkaroundRecap - Generic Collection2Types & Structs Deep Dive
Basic & Composite TypesCustom Type DefinitionsStruct TagsAnonymous StructsNested StructsZero Values & DefaultsRecap - Contact Book5Composition Over Inheritance
Why Go Has No InheritanceStruct Embedding BasicsMethod PromotionEmbedding Multiple StructsEmbedding vs AggregationShadowing Embedded MethodsRecap - Employee Hierarchy8Error Handling & OOP
The error InterfaceCustom Error TypesError Wrapping (fmt.Errorf)Sentinel Errorserrors.Is() and errors.As()Panic, Defer, and RecoverRecap - File Parser3Pointers & Memory
Pointer Basics in GoPointers to StructsPass by Value vs ReferenceThe new() FunctionGarbage Collection in GoRecap - Linked List Builder6Polymorphism in Go
Polymorphism via InterfacesDuck Typing in GoInterface Satisfaction RulesPolymorphic CollectionsDependency InjectionRecap - Payment Processor9Concurrency & OOP
Goroutines BasicsChannels & CommunicationBuffered vs Unbuffered ChanSelect Statementsync.Mutex & sync.RWMutexsync.WaitGroupThread-Safe Struct DesignRecap - Worker Pool