Go Generics: Everything You Need to Know
James Reed
Infrastructure Engineer · Leapcell

What is Generics
Generic programming is a style or paradigm of programming languages. Generics allow programmers to write code in strongly-typed programming languages using some types that are specified later, and these types are provided as parameters during instantiation.
Generics enable you to write code that can be applied to multiple types without repeating the same logic for each type. This improves code reusability, flexibility, and type safety.
In Go, generics are implemented through type parameters. A type parameter is a special kind of parameter used as a placeholder that can be any type. They are used in the definitions of functions, methods, and types, and are replaced with specific types during concrete calls.
Before Generics
Consider this requirement: implement a function that takes two int
parameters and returns the smaller of the two. This is a very simple requirement, and we can easily write the following code:
func Min(a, b int) int { if a < b { return a } return b }
This looks great, but the function has a limitation: the parameters can only be of type int
. If the requirement expands and we need to support comparison of two float64
values and return the smaller one, we might write:
func Min(a, b int) int { if a < b { return a } return b } func MinFloat64(a, b float64) float64 { if a < b { return a } return b }
You may notice that as soon as the requirement expands, we have to make changes as well, constantly doing repetitive work. Generics are exactly what solve this problem.
import "golang.org/x/exp/constraints" func Min[T constraints.Ordered](x, y T) T { if x < y { return x } return y }
Basic Syntax of Generics
// Function definition func F[T any](p T){...} // Type definition type M[T any] []T // Constraint represents specific type constraints, such as any, comparable func F[T Constraint](p T){..} // The “~” symbol is used to represent underlying type constraints type E interface { ~string } // Specify several types type UnionElem interface { int | int8 | int32 | int64 }
The ~ Symbol
In Go generics, the ~
symbol is used to represent an underlying type constraint.
For example, ~int
means accepting any type whose underlying type is int
, including custom types. If there is a custom type MyInt
whose underlying type is int
, then this constraint can accept the MyInt
type.
type MyInt int type Ints[T int | int32] []T func main() { a := Ints[int]{1, 2} // Correct b := Ints[MyInt]{1, 2} // Compilation error println(a) println(b) }
MyInt does not satisfy int | int32 (possibly missing ~ for int in int | int32)compilerInvalidTypeArg
Just modify it as follows:
type Ints[T ~int | ~int32] []T
Type Constraints
any
: Accepts any typecomparable
: Supports==
and!=
operationsordered
: Supports comparison operators like>
,<
For other types, refer to: https://pkg.go.dev/golang.org/x/exp/constraints
When to Use Generics
When to Use Generics
- For operations on language-defined container types: When writing functions that operate on special container types defined by the language, such as slices, maps, and channels, and the function code does not make specific assumptions about the element type, using type parameters may be useful. For example, a function that returns a slice of all keys from any type of map.
- General-purpose data structures: For general-purpose data structures like linked lists or binary trees, using type parameters can produce more generic data structures, or store data more efficiently, avoid type assertions, and perform complete type checking at build time.
- Implementing generic methods: When different types need to implement some common methods, and these implementations look exactly the same, using type parameters may be useful. For example, a generic type implementing
sort.Interface
for any slice type. - Prefer functions over methods: When you need something like a comparison function, prefer using functions instead of methods. For general-purpose data types, it is preferable to use functions rather than writing constraints that require methods.
// SliceFn implements sort.Interface for a slice of T. type SliceFn[T any] struct { s []T less func(T, T) bool } func (s SliceFn[T]) Len() int { return len(s.s) } func (s SliceFn[T]) Swap(i, j int) { s.s[i], s.s[j] = s.s[j], s.s[i] } func (s SliceFn[T]) Less(i, j int) bool { return s.less(s.s[i], s.s[j]) }
// SortFn sorts s in place using a comparison function. func SortFn[T any](s []T, less func(T, T) bool) { sort.Sort(SliceFn[T]{s, less}) }
When Not to Use Generics
- Do not replace interface types: If you only need to call methods on a value of a certain type, you should use interface types rather than type parameters. For example, you should not change a function that uses interface types to one that uses type parameters.
- Do not use type parameters when method implementations differ: If the method implementation is different for each type, you should use interface types and write different method implementations, rather than using type parameters.
- Use reflection appropriately: If some operations must support types that do not even have methods, and the operations are different for each type, then use reflection. For example, the
encoding/json
package uses reflection.
Simple Guideline
If you find yourself writing exactly the same code multiple times, with the only difference being the types used, you can consider using type parameters. In other words, you should avoid using type parameters until you notice that you are about to write exactly the same code multiple times.
Trivia
Why use square brackets []
instead of the angle brackets < >
that are common in other languages?
https://github.com/golang/proposal/blob/master/design/15292/2013-12-type-params.md
Use angle brackets, as in Vector. This has the advantage of being familiar to C++ and Java programmers. Unfortunately, it means that f(true) can be parsed as either a call to function f or a comparison of f<T (an expression that tests whether f is less than T) with (true). While it may be possible to construct complex resolution rules, the Go syntax avoids that sort of ambiguity for good reason.
We are Leapcell, your top choice for hosting Go projects.
Leapcell is the Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis:
Multi-Language Support
- Develop with Node.js, Python, Go, or Rust.
Deploy unlimited projects for free
- pay only for usage — no requests, no charges.
Unbeatable Cost Efficiency
- Pay-as-you-go with no idle charges.
- Example: $25 supports 6.94M requests at a 60ms average response time.
Streamlined Developer Experience
- Intuitive UI for effortless setup.
- Fully automated CI/CD pipelines and GitOps integration.
- Real-time metrics and logging for actionable insights.
Effortless Scalability and High Performance
- Auto-scaling to handle high concurrency with ease.
- Zero operational overhead — just focus on building.
Explore more in the Documentation!
Follow us on X: @LeapcellHQ