Generics in Go: Your Friendly Guide to Reusable Code
Hi there! I'm Shrijith Venkatrama, founder of Hexmos. Right now, I’m building LiveAPI, a tool that makes generating API docs from your code ridiculously easy. Go’s simplicity and performance make it a developer favorite, but until Go 1.18, it lacked generics. Generics let you write flexible, reusable code while keeping type safety. Want to write one function that handles multiple types or avoid code duplication? Generics are here to help. This guide covers what generics are, how to use them, and why they’re useful. I'll share full programming examples, tables, and tips to make it clear. What Are Generics and Why Do They Matter? Generics let you write functions, types, and methods that work with multiple types without losing Go’s type safety. Before generics, you’d either duplicate code for each type or use interface{} with type assertions, which was messy and unsafe. Key benefits: Code reuse: One function or type works for many types. Type safety: Catch type errors at compile time. Cleaner code: No repetitive code or risky type casting. For example, a function to find the maximum of two values would need separate versions for int, float64, etc., without generics. Now, you can write it once using a custom constraint for ordered types. Example: package main import "fmt" type Ordered interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~float32 | ~float64 | ~string } func Max[T Ordered](a, b T) T { if a > b { return a } return b } func main() { fmt.Println(Max(10, 20)) // Output: 20 fmt.Println(Max(3.14, 2.71)) // Output: 3.14 fmt.Println(Max("go", "golang")) // Output: golang } Here, Ordered is a custom constraint that includes types supporting comparison operators (>, , use comparable or a custom constraint. Building Generic Types Generics also work with structs, interfaces, and other types. This is great for data structures like lists, stacks, or trees that need to handle any type. Example: A generic Stack struct (copy/paste and run): package main import "fmt" type Stack[T any] struct { items []T } func (s *Stack[T]) Push(item T) { s.items = append(s.items, item) } func (s *Stack[T]) Pop() (T, bool) { if len(s.items) == 0 { var zero T return zero, false } item := s.items[len(s.items)-1] s.items = s.items[:len(s.items)-1] return item, true } func main() { intStack := Stack[int]{} intStack.Push(42) intStack.Push(100) value, ok := intStack.Pop() fmt.Println(value, ok) // Output: 100 true strStack := Stack[string]{} strStack.Push("hello") strStack.Push("world") str, ok := strStack.Pop() fmt.Println(str, ok) // Output: world true } Why this matters: Generic types let you create reusable data structures without duplicating code or using interface{}. The Stack[T] works for any type, keeping your code clean. Learn more: Go Generics Tutorial Using Constraints to Limit Types Constraints control which types a generic function or type can use. Go offers any and comparable, but you can define custom constraints with interfaces. Example: A custom constraint for numeric types: package main import "fmt" type Number interface { ~int | ~float64 | ~float32 } func Sum[T Number](numbers []T) T { var total T for _, n := range numbers { total += n } return total } func main() { ints := []int{1, 2, 3} floats := []float64{1.5, 2.5, 3.5} fmt.Println(Sum(ints)) // Output: 6 fmt.Println(Sum(floats)) // Output: 7.5 } Key points: The ~ allows types with int, float64, etc., as their underlying type (e.g., type MyInt int). Use | to combine multiple types. Table: Constraint Examples Constraint Example Allowed Types `~int ~float64` interface{ String() string } Any type with a String() method comparable Types supporting == and != Tip: Choose the tightest constraint to make your code safe and clear. Type Inference: Write Less, Do More Go’s type inference simplifies generics. YouOften don’t need to specify the type when calling a generic function—Go infers it from the arguments. Example (copy/paste and run): package main import "fmt" func Print[T any](value T) { fmt.Println(value) } func main() { Print(42) // Output: 42 Print("hello") // Output: hello } Sometimes, you need explicit types, like with generic types or interfaces: Example: package main import "fmt" func Process[T comparable](a, b T) bool { return a == b } func main() { var x interface{} = 42 var y interface{} = 42 result := Process[int](x.(int), y.(int)) // Explicit type needed fmt.Println(result) // Output: true } When inference fails: With slices of interfaces. When using generic types directly (e.g., Stack[T]). Tip: Lean on inference for simple cases, but spe

Hi there! I'm Shrijith Venkatrama, founder of Hexmos. Right now, I’m building LiveAPI, a tool that makes generating API docs from your code ridiculously easy.
Go’s simplicity and performance make it a developer favorite, but until Go 1.18, it lacked generics.
Generics let you write flexible, reusable code while keeping type safety. Want to write one function that handles multiple types or avoid code duplication? Generics are here to help.
This guide covers what generics are, how to use them, and why they’re useful.
I'll share full programming examples, tables, and tips to make it clear.
What Are Generics and Why Do They Matter?
Generics let you write functions, types, and methods that work with multiple types without losing Go’s type safety. Before generics, you’d either duplicate code for each type or use interface{}
with type assertions, which was messy and unsafe.
Key benefits:
- Code reuse: One function or type works for many types.
- Type safety: Catch type errors at compile time.
- Cleaner code: No repetitive code or risky type casting.
For example, a function to find the maximum of two values would need separate versions for int
, float64
, etc., without generics. Now, you can write it once using a custom constraint for ordered types.
Example:
package main
import "fmt"
type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
~float32 | ~float64 | ~string
}
func Max[T Ordered](a, b T) T {
if a > b {
return a
}
return b
}
func main() {
fmt.Println(Max(10, 20)) // Output: 20
fmt.Println(Max(3.14, 2.71)) // Output: 3.14
fmt.Println(Max("go", "golang")) // Output: golang
}
Here, Ordered
is a custom constraint that includes types supporting comparison operators (>
, <
, etc.). The ~
allows custom types with these underlying types (e.g., type MyInt int
). This function works with int
, float64
, string
, or other ordered types.
Learn more: Go 1.18 Release Notes
Understanding Type Parameters
Type parameters are the heart of generics. They’re placeholders for types, defined in square brackets [T]
after a function or type name. Constraints limit which types can be used.
Syntax:
func FunctionName[T Constraint](param T) T {
// Code here
}
-
T
is the type parameter. -
Constraint
specifies allowed types (e.g.,comparable
,any
, or a custom interface).
Example:
package main
import "fmt"
func Print[T any](value T) {
fmt.Println(value)
}
func main() {
Print(42) // Output: 42
Print("hello") // Output: hello
Print(struct{}{}) // Output: {}
}
Here, T any
means T
can be any type. You can call Print
with int
, string
, or even structs.
Table: Common Constraints
Constraint | Description | Example Types |
---|---|---|
any |
Any type at all |
int , string , structs, etc. |
comparable |
Types that support == and !=
|
int , string , bool
|
Custom Interface | User-defined interface with methods | Custom structs implementing it |
Tip: Use any
for flexibility, but tighter constraints like comparable
for specific operations.
Writing Generic Functions
Generic functions let you write one function that works with multiple types. The key is choosing the right constraint to support the operations you need.
Example: A generic Swap
function (copy/paste and run):
package main
import "fmt"
func Swap[T any](a, b T) (T, T) {
return b, a
}
func main() {
x, y := Swap(10, 20)
fmt.Println(x, y) // Output: 20 10
s1, s2 := Swap("hi", "world")
fmt.Println(s1, s2) // Output: world hi
}
Real-world use case: A generic Filter
function to keep elements matching a condition (copy/paste and run):
package main
import "fmt"
func Filter[T any](slice []T, predicate func(T) bool) []T {
result := []T{}
for _, v := range slice {
if predicate(v) {
result = append(result, v)
}
}
return result
}
func main() {
nums := []int{1, 2, 3, 4, 5}
evens := Filter(nums, func(n int) bool { return n%2 == 0 })
fmt.Println(evens) // Output: [2 4]
strs := []string{"cat", "dog", "bird"}
short := Filter(strs, func(s string) bool { return len(s) == 3 })
fmt.Println(short) // Output: [cat dog]
}
Note: The any
constraint works here because we only store and return T
. For operations like >
, use comparable
or a custom constraint.
Building Generic Types
Generics also work with structs, interfaces, and other types. This is great for data structures like lists, stacks, or trees that need to handle any type.
Example: A generic Stack
struct (copy/paste and run):
package main
import "fmt"
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}
func main() {
intStack := Stack[int]{}
intStack.Push(42)
intStack.Push(100)
value, ok := intStack.Pop()
fmt.Println(value, ok) // Output: 100 true
strStack := Stack[string]{}
strStack.Push("hello")
strStack.Push("world")
str, ok := strStack.Pop()
fmt.Println(str, ok) // Output: world true
}
Why this matters: Generic types let you create reusable data structures without duplicating code or using interface{}
. The Stack[T]
works for any type, keeping your code clean.
Learn more: Go Generics Tutorial
Using Constraints to Limit Types
Constraints control which types a generic function or type can use. Go offers any
and comparable
, but you can define custom constraints with interfaces.
Example: A custom constraint for numeric types:
package main
import "fmt"
type Number interface {
~int | ~float64 | ~float32
}
func Sum[T Number](numbers []T) T {
var total T
for _, n := range numbers {
total += n
}
return total
}
func main() {
ints := []int{1, 2, 3}
floats := []float64{1.5, 2.5, 3.5}
fmt.Println(Sum(ints)) // Output: 6
fmt.Println(Sum(floats)) // Output: 7.5
}
Key points:
- The
~
allows types withint
,float64
, etc., as their underlying type (e.g.,type MyInt int
). - Use
|
to combine multiple types.
Table: Constraint Examples
Constraint Example | Allowed Types |
---|---|
`~int | ~float64` |
interface{ String() string } |
Any type with a String() method |
comparable |
Types supporting == and !=
|
Tip: Choose the tightest constraint to make your code safe and clear.
Type Inference: Write Less, Do More
Go’s type inference simplifies generics. YouOften don’t need to specify the type when calling a generic function—Go infers it from the arguments.
Example (copy/paste and run):
package main
import "fmt"
func Print[T any](value T) {
fmt.Println(value)
}
func main() {
Print(42) // Output: 42
Print("hello") // Output: hello
}
Sometimes, you need explicit types, like with generic types or interfaces:
Example:
package main
import "fmt"
func Process[T comparable](a, b T) bool {
return a == b
}
func main() {
var x interface{} = 42
var y interface{} = 42
result := Process[int](x.(int), y.(int)) // Explicit type needed
fmt.Println(result) // Output: true
}
When inference fails:
- With slices of interfaces.
- When using generic types directly (e.g.,
Stack[T]
).
Tip: Lean on inference for simple cases, but specify types for complex scenarios.
Avoiding Common Generics Pitfalls
Generics are powerful, but they have traps. Here are common issues and fixes.
1. Overusing generics:
- Don’t use generics when a simple function or interface works. A function for just
string
doesn’t need generics. - Fix: Only use generics for code reused across types.
2. Wrong constraints:
- Using
any
when you need+
or>
causes compile errors. -
Fix: Use constraints like
Number
orcomparable
.
3. Nil and zero values:
- Generic types may return zero values (e.g.,
0
forint
,""
forstring
). Handle them carefully. - Example (copy/paste and run):
package main
import "fmt"
func GetFirst[T any](slice []T) T {
if len(slice) == 0 {
var zero T
return zero
}
return slice[0]
}
func main() {
nums := []int{}
fmt.Println(GetFirst(nums)) // Output: 0
strs := []string{}
fmt.Println(GetFirst(strs)) // Output: ""
}
4. Performance:
- Generics generate specialized code for each type, increasing binary size.
- Fix: Test performance for critical code.
Learn more: Why Generics
Real-World Example: A Generic Map Function
Let’s wrap up with a practical Map
function that transforms a slice of one type into another.
Example:
package main
import (
"fmt"
"strings"
)
func Map[T, U any](slice []T, transform func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = transform(v)
}
return result
}
func main() {
nums := []int{1, 2, 3}
doubles := Map(nums, func(n int) int { return n * 2 })
fmt.Println(doubles) // Output: [2 4 6]
strs := []string{"a", "b", "c"}
uppers := Map(strs, strings.ToUpper)
fmt.Println(uppers) // Output: [A B C]
}
Why this rocks:
- Works with any input/output type.
- Reusable for countless transformations.
- Type-safe and concise.
Tip: Use Map
for data transformations like type conversions or calculations.
Generics in Go make your code cleaner and more reusable. From generic functions to type-safe data structures, they solve real problems without complexity. Try a generic function or struct in your next project—you’ll see the difference.
Questions or cool generic patterns? Share in the comments or hit me up on X. Happy coding!