The author selected the Diversity in Tech Fund to receive a donation as part of the Write for DOnations program.
Generics in Go let you write reusable, type-safe code that works with multiple types without giving up compile-time checking. You declare type parameters (placeholders for types) and optionally restrict them with type constraints, so the compiler enforces correct usage and you avoid the type assertions and runtime errors that often come with interface{}. In this tutorial you will build a card-deck program: start with a deck that stores cards as interface{}, then refactor it to use generics, add a second card type, constrain the deck to card types only, and create a generic function that works with any card. By the end you will know when to use generics in Go and how type parameters, constraints, and type inference work in practice.
Go introduced generics in version 1.18, and later versions have continued to stabilize the feature. The type parameter syntax and the idea of constraints as interfaces give you a way to substitute different types for the same generic type or function while keeping strong typing. This tutorial assumes you are on a current stable Go toolchain (Go 1.18 or later).
To follow this tutorial, you will need:
Go 1.18 or later installed. To set this up, follow the How To Install Go tutorial for your operating system.
A solid understanding of the Go language, such as variables, How To Define and Call Functions in Go, Defining Structs in Go, for loops, and slices. If you’d like to read more about these concepts, our How To Code in Go series has a number of tutorials covering these and more.
[T any], [C Card]) for types and functions. They provide type-safe, reusable code without giving up static checking.Name(), <, ==).go build ./... and go vet ./... after migrating existing code to generics.If you want to go deeper, the Go blog: Introduction to generics and the Tutorial Getting started with generics cover more patterns and constraints. This tutorial is part of the DigitalOcean How to Code in Go series, which covers Go from installation through language features.
Go can represent many types through interfaces, and a lot of code works well without generics. The cost shows up at the call site: you store values in interface{} and then must assert back to a concrete type to use them, which loses compile-time checking and adds boilerplate.
The type systems in programming languages can generally be classified into two different categories: typing and type checking. A language can use either strong or weak typing, and static or dynamic type checking. Some languages use a mix of these, but Go fits pretty well into the strongly-typed and statically-checked languages. Being strongly-typed means that Go ensures a value in a variable matches the type of the variable, so you can’t store an int value in a string variable, for example. As a statically-checked type system, Go’s compiler will check these type rules when it compiles the program instead of while the program is running.
A benefit of using a strongly-typed, statically-checked language like Go is that the compiler lets you know about any potential mistakes before your program is released, avoiding certain “invalid type” runtime errors. This does add a limitation to Go programs, though, because you must know the types you intend to use before compiling your program. One way to handle this is by using the interface{} type (or its alias any). An interface{} works for any value because it declares no required methods, so every type satisfies it. In practice you lose type information at the call site and must use type assertions to get back to a concrete type, which is where generics improve on this pattern.
To start creating your program using an interface{} to represent your cards, you’ll need a directory to keep the program’s directory in. In this tutorial, you’ll use a directory named projects.
First, make the projects directory and navigate to it:
- mkdir projects
- cd projects
Next, make the directory for your project and navigate to it. In this case, use the directory generics:
- mkdir generics
- cd generics
Inside the generics directory, use nano, or your favorite editor, to open the main.go file:
- nano main.go
In the main.go file, begin by adding your package declaration and import the packages you’ll need:
package main
import (
"fmt"
"math/rand"
"os"
)
The package main declaration tells Go to compile your program as a binary so you can run it directly, and the import statement tells Go which packages you’ll be using in the later code.
Now, define your PlayingCard type and its associated functions and methods:
...
type PlayingCard struct {
Suit string
Rank string
}
func NewPlayingCard(suit string, card string) *PlayingCard {
return &PlayingCard{Suit: suit, Rank: card}
}
func (pc *PlayingCard) String() string {
return fmt.Sprintf("%s of %s", pc.Rank, pc.Suit)
}
In this snippet, you define a struct named PlayingCard with the properties Suit and Rank, to represent the cards from a deck of 52 playing cards. The Suit will be one of Diamonds, Hearts, Clubs, or Spades, and the Rank will be A, 2, 3, and so on through K.
You also defined a NewPlayingCard function to act as the constructor for the PlayingCard struct, and a String method, which returns the rank and suit of the card using fmt.Sprintf.
Next, create your Deck type with the AddCard and RandomCard methods, as well as a NewPlayingCardDeck function to create a *Deck filled with all 52 playing cards:
...
type Deck struct {
cards []interface{}
}
func NewPlayingCardDeck() *Deck {
suits := []string{"Diamonds", "Hearts", "Clubs", "Spades"}
ranks := []string{"A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"}
deck := &Deck{}
for _, suit := range suits {
for _, rank := range ranks {
deck.AddCard(NewPlayingCard(suit, rank))
}
}
return deck
}
func (d *Deck) AddCard(card interface{}) {
d.cards = append(d.cards, card)
}
func (d *Deck) RandomCard() interface{} {
cardIdx := rand.Intn(len(d.cards))
return d.cards[cardIdx]
}
In the Deck you define above, you create a field called cards to hold a slice of cards. Since you want the deck to be able to hold multiple different types of cards, you can’t just define it as []*PlayingCard, though. You define it as []interface{} so it can hold any type of card you may create in the future. In addition to the []interface{} field on the Deck, you also create an AddCard method that accepts the same interface{} type to append a card to the Deck’s cards field.
You also create a RandomCard method that returns a random card from the Deck’s cards slice. This method uses the math/rand package to generate a random index. As of Go 1.20, the global rand functions are automatically seeded, so a separate generator and manual seeding are not required. The rand.Intn(len(d.cards)) call returns an int in the range [0, len(d.cards)), and RandomCard returns the card at that index.
Warning: Be careful which random number generator you use in your programs. The math/rand package is not cryptographically secure and should not be used for security-sensitive programs. The crypto/rand package provides a cryptographically secure random number generator for those use cases.
Finally, the NewPlayingCardDeck function returns a *Deck value populated with all the cards in a playing card deck. You use two slices, one with all the available suits and one with all the available ranks, and then loop over each value to create a new *PlayingCard for each combination before adding it to the deck using AddCard. Once the cards for the deck are generated, the value is returned.
Create your main function to use them to draw cards:
...
func main() {
deck := NewPlayingCardDeck()
fmt.Printf("--- drawing playing card ---\n")
card := deck.RandomCard()
fmt.Printf("drew card: %s\n", card)
playingCard, ok := card.(*PlayingCard)
if !ok {
fmt.Printf("card received wasn't a playing card!")
os.Exit(1)
}
fmt.Printf("card suit: %s\n", playingCard.Suit)
fmt.Printf("card rank: %s\n", playingCard.Rank)
}
In the main function, you first create a new deck of playing cards using the NewPlayingCardDeck function and assign it to the deck variable. Then, you use fmt.Printf to print that you’re drawing a card and use deck’s RandomCard method to get a random card from the deck. After, you use fmt.Printf again to print the card you drew from the deck.
Next, since the type of the card variable is interface{}, you need to use a type assertion to get a reference to the card as its original *PlayingCard type. If the type in the card variable is not a *PlayingCard type, which it should be given how your program is written right now, the value of ok will be false and your program will print an error message using fmt.Printf and exit with an error code of 1 using os.Exit. If it is a *PlayingCard type, you then print out the playingCard’s Suit and Rank values using fmt.Printf.
Once you have all your changes saved, you can run your program using go run with main.go, the name of the file to run:
- go run main.go
In the output of your program, you should see a card randomly chosen from the deck, as well as the card suit and rank:
--- drawing playing card ---
drew card: Q of Diamonds
card suit: Diamonds
card rank: Q
If you add a TradingCard to the interface{} deck by accident, the assertion in main fails when that card is drawn: ok is false and the program exits with an error. For example:
// Intentionally wrong: adding a TradingCard to a PlayingCard deck
deck.AddCard(&TradingCard{CollectableName: "Sammy"})
card := deck.RandomCard()
playingCard, ok := card.(*PlayingCard)
if !ok {
fmt.Printf("card received wasn't a playing card!\n")
os.Exit(1)
}
When that wrong card is drawn, the program prints:
--- drawing playing card ---
card received wasn't a playing card!
exit status 1
The compiler accepted this code because AddCard takes interface{}; the failure only appears at runtime when the wrong card is drawn. That type assertion is required every time you pull a value out of the deck, and if someone adds a different card type to the same deck, the assertion can fail at runtime. Making the deck generic removes both the assertion and that failure mode.
Type parameters let you write one Deck type that works with any card type while keeping full type safety: no interface{} and no type assertions at the call site. The compiler enforces the card type at every use.
To make your first update, open your main.go file and remove the os package import:
package main
import (
"fmt"
"math/rand"
)
As you’ll see in the later updates, you won’t need to use the os.Exit function any more so it’s safe to remove this import.
Next, update your Deck struct to be a generic type:
...
type Deck[C any] struct {
cards []C
}
This update introduces the new syntax used by generics to create placeholder types, or type parameters, within your struct declaration. You can almost think of these type parameters as similar to the parameters you’d include in a function. When calling a function, you provide values for each function parameter. Likewise, when creating a generic type value, you provide types for the type parameters.
You’ll see after the name of your struct, Deck, that you’ve added a statement inside square brackets ([]). These square brackets allow you to define one or more of these type parameters for your struct.
In the case of your Deck type, you only need one type parameter, named C, to represent the type of the cards in your deck. By declaring C any inside your type parameters, your code says, “create a generic type parameter named C that I can use in my struct, and allow it to be any type”. Behind the scenes, the any type is actually an alias to the interface{} type. This makes generics easier to read, and you don’t need to use C interface{}. Your deck only needs one generic type to represent the cards, but if you require additional generic types, you could add them using a comma-separated syntax, such as C any, F any. The name you use for your type parameters can be anything you’d like if it isn’t reserved, but they are typically short and capitalized.
Lastly, in your update to the Deck declaration, you updated the type of the cards slice in the struct to use the C type parameter. When using generics, you can use your type parameters anywhere you typically put a specific type. In this case, you want your C parameter to represent each card in the slice, so you put the [] slice type declaration followed by the C parameter declaration.
Next, update your Deck type’s AddCard method to use the generic type you defined. For now, you’ll skip over updating the NewPlayingCardDeck function, but you will be coming back to it shortly:
...
func (d *Deck[C]) AddCard(card C) {
d.cards = append(d.cards, card)
}
In the update to Deck’s AddCard method, you first added the [C] generic type parameter to the method’s receiver. This lets Go know the name of the type parameter you’ll be using elsewhere in the method declaration and follows a similar square bracket syntax as the struct declaration. In this case, though, you don’t need to provide the any constraint because it was already provided in the Deck’s declaration. Then, you updated the card function parameter to use the C placeholder type instead of the original interface{} type. This allows the method to use the specific type C will eventually become.
After updating the AddCard method, update the RandomCard method to use the C generic type as well:
...
func (d *Deck[C]) RandomCard() C {
cardIdx := rand.Intn(len(d.cards))
return d.cards[cardIdx]
}
This time, instead of using the C generic type as a function parameter, you have updated the method to return C instead of interface{}. The method uses the global rand.Intn (auto-seeded as of Go 1.20) to pick a random index and returns that element from cards. Since cards is []C, the return type is C, so callers get a concrete type with no type assertion.
Now that your Deck type is updated to use generics, go back to your NewPlayingCardDeck function and update it to use the generic Deck type for *PlayingCard types:
...
func NewPlayingCardDeck() *Deck[*PlayingCard] {
suits := []string{"Diamonds", "Hearts", "Clubs", "Spades"}
ranks := []string{"A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"}
deck := &Deck[*PlayingCard]{}
for _, suit := range suits {
for _, rank := range ranks {
deck.AddCard(NewPlayingCard(suit, rank))
}
}
return deck
}
...
Most of the code in the NewPlayingCardDeck stays the same, but now that you’re using a generic version of Deck, you need to specify the type you’d like to use for C when using the Deck. You do this by referencing your Deck type as you normally would, whether it’s Deck or a reference like *Deck, and then providing the type that should replace C by using the same square brackets you used when initially declaring the type parameters.
For the NewPlayingCardDeck return type, you still use *Deck as you did before, but this time, you also include the square brackets and *PlayingCard. By providing [*PlayingCard] for the type parameter, you’re saying that you want the *PlayingCard type in your Deck declaration and methods to replace the value of C. This means that the type of the cards field on Deck essentially changes from []C to []*PlayingCard.
Similarly, when creating a new instance of Deck, you also need to provide the type replacing C. Where you might usually use &Deck{} to make a new reference to Deck, you instead include the type inside square brackets to end up with &Deck[*PlayingCard]{}.
Now that your types have been updated to use generics, you can update your main function to take advantage of them:
...
func main() {
deck := NewPlayingCardDeck()
fmt.Printf("--- drawing playing card ---\n")
playingCard := deck.RandomCard()
fmt.Printf("drew card: %s\n", playingCard)
fmt.Printf("card suit: %s\n", playingCard.Suit)
fmt.Printf("card rank: %s\n", playingCard.Rank)
}
This time your update is to remove code because you no longer need to assert an interface{} value into a *PlayingCard value. When you updated Deck’s RandomCard method to return C and updated NewPlayingCardDeck to return *Deck[*PlayingCard], it changed RandomCard to return a *PlayingCard value instead of interface{}. When RandomCard returns *PlayingCard, it means the type of playingCard is also *PlayingCard instead of interface{} and you can access the Suit or Rank fields right away.
To see your program running after you’ve saved your changes to main.go, use the go run command again:
- go run main.go
You should see output similar to the following output, but the card drawn will likely be different:
--- drawing playing card ---
drew card: 8 of Hearts
card suit: Hearts
card rank: 8
Even though the output is the same as the previous version of your program using interface{}, you no longer need the type assertion, and the compiler now prevents a TradingCard from being added to a *PlayingCard deck at all.
Because the deck is generic over the card type, you can reuse the same Deck definition for other card types without changing its code.
The same Deck[C] works as *Deck[*TradingCard] with no changes to Deck; you only swap the type argument when constructing the deck.
To create your TradingCard type, open your main.go file again and add the definition:
...
import (
...
)
type TradingCard struct {
CollectableName string
}
func NewTradingCard(collectableName string) *TradingCard {
return &TradingCard{CollectableName: collectableName}
}
func (tc *TradingCard) String() string {
return tc.CollectableName
}
This TradingCard is similar to your PlayingCard, but instead of having the Suit and Rank fields, it has a CollectableName field to keep track of the trading card’s name. It also includes the NewTradingCard constructor function and String method, similar to PlayingCard.
Now, create the NewTradingCardDeck constructor for a Deck filled with *TradingCards:
...
func NewPlayingCardDeck() *Deck[*PlayingCard] {
...
}
func NewTradingCardDeck() *Deck[*TradingCard] {
collectables := []string{"Sammy", "Droplets", "Spaces", "App Platform"}
deck := &Deck[*TradingCard]{}
for _, collectable := range collectables {
deck.AddCard(NewTradingCard(collectable))
}
return deck
}
When you create or return the *Deck this time, you’ve replaced *PlayingCard with *TradingCard, but that’s the only change you need to make to the deck. You have a slice of special DigitalOcean collectables, which you then loop over to add each *TradingCard to the deck. The deck’s AddCard method still works the same way, but this time it accepts the *TradingCard value from NewTradingCard. If you tried passing it a value from NewPlayingCard, the compiler would give you an error because it would be expecting a *TradingCard, but you’re providing a *PlayingCard.
Lastly, update your main function to create a new Deck of *TradingCards, draw a random card with RandomCard, and print out the card’s information:
...
func main() {
playingDeck := NewPlayingCardDeck()
tradingDeck := NewTradingCardDeck()
fmt.Printf("--- drawing playing card ---\n")
playingCard := playingDeck.RandomCard()
...
fmt.Printf("card rank: %s\n", playingCard.Rank)
fmt.Printf("--- drawing trading card ---\n")
tradingCard := tradingDeck.RandomCard()
fmt.Printf("drew card: %s\n", tradingCard)
fmt.Printf("card collectable name: %s\n", tradingCard.CollectableName)
}
Because Deck is generic, RandomCard() returns a *TradingCard when called on tradingDeck, so you can access CollectableName directly without a type assertion.
To see your updated code in action, save your changes and run your program again with go run:
- go run main.go
Then, review your output:
--- drawing playing card ---
drew card: Q of Diamonds
card suit: Diamonds
card rank: Q
--- drawing trading card ---
drew card: App Platform
card collectable name: App Platform
The only change to Deck was adding a new constructor; the type parameter did the rest. Your current Deck still uses C any, so nothing stops a caller from creating &Deck[int]{}. To restrict the deck to card types only, you need a type constraint.
Constraints limit which types can be used as type arguments so the compiler can allow operations that only those types support. When your generic code must call methods or use operations that not every type has, you need a narrower constraint than any.
To begin the updates, open the main.go file and add your Card interface:
...
import (
...
)
type Card interface {
fmt.Stringer
Name() string
}
Your Card interface is defined the same as any other Go interface you may have used in the past; there are no special requirements to use it with generics. In this Card interface, you’re saying that for something to be considered a Card, it must implement the fmt.Stringer type (it must have the String method your cards already have), and it must also have a Name method that returns a string value.
Next, update your TradingCard and PlayingCard types to add a new Name method, in addition to the existing String methods, so they implement the Card interface:
...
type TradingCard struct {
...
}
...
func (tc *TradingCard) Name() string {
return tc.String()
}
...
type PlayingCard struct {
...
}
...
func (pc *PlayingCard) Name() string {
return pc.String()
}
The TradingCard and PlayingCard already have String methods that implement the fmt.Stringer interface. So to implement the Card interface, you only need to add the new Name method. Also, since fmt.Stringer is already implemented to return the names of the cards, you can just return the result of the String method for Name.
Now, update your Deck so it only allows Card types to be used for C:
...
type Deck[C Card] struct {
cards []C
}
Before this update, you had C any for the type restriction (known as the type constraint), which isn’t much of a restriction. Since any means the same as interface{}, it allowed any type in Go to be used for the C type parameter. Now that you’ve replaced any with your new Card interface, the Go compiler will ensure that any types used for C implement Card when you compile your program.
Since you’ve added this restriction, you can now use any methods provided by Card inside your Deck type’s methods. If you wanted RandomCard to also print out the name of the card being drawn, it would be able to access the Name method because it’s part of the Card interface. You will use this in the Creating Generic Functions section, where printCard calls card.Name() directly because C is constrained to Card.
These few updates are the only ones you need to make to restrict your Deck type to only using Card values. After saving your changes, run your updated program using go run:
- go run main.go
Then, once your program is finished running, review the output:
--- drawing playing card ---
drew card: 5 of Clubs
card suit: Clubs
card rank: 5
--- drawing trading card ---
drew card: Droplets
card collectable name: Droplets
You’ll see that, aside from choosing different cards, the output hasn’t changed. The constraint only restricts which types can be used as C; it does not change behavior for types that already implement Card. With Deck[C Card], you can safely call Name() and other Card methods on values of type C inside Deck’s methods, which leads to generic functions that work over cards.
Type constraints are interfaces that specify which types a type parameter may be; they can be method sets (like Card) or type sets (including union constraints and built-ins like comparable). Knowing the difference and when to use each keeps your generic code correct and easy to reason about.
In normal Go code, an interface describes values: it defines a set of methods, and any type that implements those methods satisfies the interface. When an interface is used as a type constraint for a type parameter, it can still be a method set, but it can also be a type set: a list of allowed types. The same syntax is used for both; the compiler treats an interface used in a type parameter list as a constraint.
Interface as type constraint vs interface as value. When you write func F[T io.Reader](r T), T can only be instantiated with types that implement io.Reader; inside F you can call r.Read(). When you write func G[T any](x T), you cannot call any methods on x because any places no requirement on T. So the constraint determines what operations are allowed on the type parameter.
Union type constraints and ~T. You can constrain a type parameter to a set of types using a union, for example int | string or ~int | ~int64. The ~ (tilde) means “underlying type”: ~int allows int and any type whose underlying type is int (for example a custom type type MyInt int). This is useful for numeric or string-like generics without accepting every type.
The comparable constraint. The language predeclares the constraint comparable for types that support == and !=. You need it when a generic function must compare two values of type T (for example in a map key or a set). If you use T any and compare two T values, the compiler rejects it; use T comparable instead.
The constraints package. The standard library does not ship a constraints package. Common ordered and numeric constraints live in golang.org/x/exp/constraints, which provides types like constraints.Ordered (types that support <, <=, >, >=) and constraints.Integer. These are useful for generic min/max or sort functions.
The following example shows a generic Min function using constraints.Ordered. Run this example in a separate directory outside your tutorial project. Then initialize a module and fetch the dependency:
- go mod init example/min
- go get golang.org/x/exp/constraints
Then create a main.go file with the following:
package main
import (
"fmt"
"golang.org/x/exp/constraints"
)
func Min[T constraints.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
func main() {
fmt.Println(Min(3, 5)) // 3
fmt.Println(Min("apple", "banana")) // "apple"
}
Output:
3
apple
The Min function works for any type that supports comparison with <. The constraints.Ordered constraint includes built-in numeric and string types, so you can call Min with integers or strings without writing separate functions. In practice, keeping constraints as narrow as possible (for example Ordered instead of any) gives you both type safety and readable, reusable code.
Note: Type sets and union syntax are part of the Go 1.18 language; the exact set of constraint types is defined in the Go spec. The constraints package in golang.org/x/exp is experimental and may change; for production code you can define your own small constraint interfaces (e.g. type Ordered interface { ~int | ~int64 | ~float64 }) if you prefer not to depend on exp.
Generic functions use the same type-parameter syntax as generic types: add a type parameter list in square brackets before the regular parameters. The constraint on that type parameter determines which methods you can call on the argument.
To implement your new function, open your main.go file and make the following updates:
...
func printCard[C any](card C) {
fmt.Println("card name:", card.Name())
}
func main() {
...
fmt.Printf("card collectable name: %s\n", tradingCard.CollectableName)
fmt.Printf("--- printing cards ---\n")
printCard[*PlayingCard](playingCard)
printCard(tradingCard)
}
In the printCard function, you’ll see the familiar square bracket syntax for the generic type parameters, followed by the regular function parameters in the parentheses. Then, in the main function, you use the printCard function to print out both a *PlayingCard and a *TradingCard.
You may notice that one of the calls to printCard includes the [*PlayingCard] type parameter, while the second one doesn’t include the same [*TradingCard] type parameter. The Go compiler is able to figure out the intended type parameter by the value you’re passing in to the function parameters, so in cases like this, the type parameters are optional. If you wanted to, you could also remove the [*PlayingCard] type parameter.
Now, save your changes and run your program again using go run:
- go run main.go
This time, though, you’ll see a compiler error:
# command-line-arguments
./main.go:87:33: card.Name undefined (type C has no field or method Name)
In the type parameter for the printCard function, you have any as the type constraint for C. When Go compiles the program, it expects to see only methods defined by the any interface, where there are none. This is the benefit of using specific type constraints on type parameters. In order to access the Name method on your card types, you need to tell Go the only types being used for the C parameter are Cards.
Update your main.go file one last time to replace the any type constraint with the Card type constraint:
...
func printCard[C Card](card C) {
fmt.Println("card name:", card.Name())
}
Then, save your changes and run your program using go run:
- go run main.go
Your program should now run successfully:
--- drawing playing card ---
drew card: 6 of Hearts
card suit: Hearts
card rank: 6
--- drawing trading card ---
drew card: App Platform
card collectable name: App Platform
--- printing cards ---
card name: 6 of Hearts
card name: App Platform
In this output, you’ll see both cards being drawn and printed as you’re familiar with, but now the printCard function also prints out the cards and uses their Name method to get the name to print. Using C Card instead of C any is what allows the call to card.Name() to compile.
The compiler infers type parameters from the types of the function arguments when it can; when there are no arguments (or the type is ambiguous), you must pass the type arguments explicitly. That is why printCard(tradingCard) works without [*TradingCard] while some calls need the type in brackets.
When you call printCard(tradingCard), the argument tradingCard has type *TradingCard, so Go infers the type parameter C as *TradingCard and you do not need to write printCard[*TradingCard](tradingCard). When you call printCard(playingCard), the same applies: playingCard is *PlayingCard, so you can write either printCard(playingCard) or printCard[*PlayingCard](playingCard). Both are valid; the explicit form is optional when the argument type uniquely determines the type parameter.
You must specify the type parameter when the compiler cannot infer it. Examples: a generic function with no parameters (e.g. func ReturnZero[T any]() T { var z T; return z }), or a function that only returns a generic type and has no arguments of type T. In those cases, call sites must supply the type, e.g. ReturnZero[int](). For printCard, the single argument always determines C, so inference usually removes the need to write the type argument and keeps call sites clean.
If you forget to supply the type argument for a function where inference cannot work, the compiler reports:
# command-line-arguments
./main.go:NN:NN: cannot infer T
This happens when the function has no parameters of type T for the compiler to inspect. For example, calling ReturnZero() without [int] gives this error. The fix is always to supply the type argument explicitly: ReturnZero[int]().
Generics in Go are implemented via GCShape stenciling: the compiler generates a small set of underlying shapes (for example one for pointer types and one for each size class of value type) rather than a fully separate implementation per type like C++ templates. In practice this means generics add little or no measurable overhead when the instantiated types share the same shape, and you should benchmark before optimizing.
Pointer types (e.g. *PlayingCard, *TradingCard) typically share one implementation because they have the same representation. Value types (e.g. int, string, struct{...}) may get their own copy of the code depending on size and layout. There is no implicit boxing of values as in some other languages; the compiler generates concrete code for the type arguments you use. When generics do add cost, it is usually from extra copying of large value types or from many distinct instantiations increasing binary size. The Go blog Introduction to generics and the implementation proposal describe the design; the spec does not guarantee a particular implementation strategy.
Recommendation: write clear, correct code first. If a hot path uses generics, measure with go test -bench before changing design. For example:
Add the First function to your main.go:
func First[T any](s []T) T { return s[0] }
Create a file named first_test.go in the same directory with the following benchmark:
package main
import "testing"
func BenchmarkFirstInt(b *testing.B) {
s := make([]int, 100)
for i := 0; i < b.N; i++ {
_ = First(s)
}
}
Then run the benchmark from your project directory:
- go test -bench=. -benchmem
In most code, the benefit of type-safe, reusable generics outweighs any negligible runtime cost.
Note: Avoid making broad performance claims without measuring. Go’s generics implementation can change in future toolchains; the above reflects the general GCShape-based approach and practical guidance rather than a guarantee.
Migrating from interface{} or any to a type parameter gives you compile-time type safety and removes type assertions at call sites. The deck example in this tutorial is a direct migration: the original Deck stored []interface{} and the generic Deck[C any] (later Deck[C Card]) stores []C. You can use the same pattern for other containers or functions that currently accept any.
Before and after (summary):
| Element | Before (pre-generics) | After (generics) |
|---|---|---|
Deck struct |
cards []interface{} |
cards []C with type Deck[C Card] struct |
AddCard |
func (d *Deck) AddCard(card interface{}) |
func (d *Deck[C]) AddCard(card C) |
RandomCard |
func (d *Deck) RandomCard() interface{} |
func (d *Deck[C]) RandomCard() C |
| Call site | card := deck.RandomCard(); pc, ok := card.(*PlayingCard) |
playingCard := deck.RandomCard() (no assertion) |
After changing signatures to use type parameters, run go build ./... and fix any call sites that still pass interface{} or rely on type assertions. Use go vet ./... to catch suspicious patterns. If the existing interface-based API is clear and type assertions are rare or isolated, migrating to generics may not be worth it; prefer generics when you want stronger typing and less boilerplate at call sites.
Changing RandomCard() interface{} to RandomCard() C is a breaking API change for any caller outside the package. If the type you are migrating is exported and consumed by other modules, that signature change breaks their code. The safe migration path in that case is to introduce a new generic type (e.g. DeckGeneric[C Card]) alongside the existing one, add a deprecation notice to the old type, and let callers move over before removing the old API. Do not change the existing exported type in place if external callers depend on it.
Go’s generics design omits several features that exist in other languages so that the language and tooling stay simple. Being aware of these limits helps you choose the right abstraction.
Methods cannot have their own type parameters. Only the receiver type (and top-level functions) can be generic. If you try to add a second type parameter to a method:
func (d *Deck[C]) Map[U any](f func(C) U) []U {
out := make([]U, 0, len(d.cards))
for _, c := range d.cards {
out = append(out, f(c))
}
return out
}
the compiler reports:
# command-line-arguments
./main.go:NN:NN: methods cannot have type parameters
The workaround is a package-level generic function that takes the receiver as its first argument:
func MapDeck[C any, U any](d *Deck[C], f func(C) U) []U {
out := make([]U, 0, len(d.cards))
for _, c := range d.cards {
out = append(out, f(c))
}
return out
}
Call it as MapDeck(deck, func(c *PlayingCard) string { return c.String() }) instead of deck.Map(...). Because MapDeck accesses the unexported cards field on Deck, it must be defined in the same package as Deck; this pattern does not work across package boundaries without adding an exported accessor method.
No operator overloading. You cannot define + or * for your type and then use them in a generic function. Constraints like constraints.Ordered work for built-in types that already have operators; custom types must expose methods (e.g. Add) and you call those methods in the generic function.
Type parameters in type switches. You cannot do switch x.(type) on a value of type T where T is a type parameter. If you write:
func Describe[T any](v T) string {
switch v.(type) {
case string:
return "string"
case int:
return "int"
default:
return "other"
}
}
the compiler reports:
# command-line-arguments
./main.go:NN:NN: cannot use type switch on type parameter value v
Use a type switch on an interface type that holds the value instead. For example, accept interface{} (or an interface that covers the cases you care about) and switch on that, or use reflection. The type parameter itself is not a valid target for a type switch.
No specialization. You cannot provide a different implementation of a generic function for a specific type (e.g. a faster path for int). There is one compiled implementation per GCShape (the underlying memory layout), so the compiler does not generate a faster or different code path for a specific type argument like int. If you need type-specific behavior, use a regular function or a type switch on an interface value instead of a generic.
Recursive type constraints. Some recursive constraint patterns are not supported. For example, trying to define a constraint where a type must contain itself as a type argument fails to compile:
type Node[T Node[T]] interface {
Children() []T
}
The compiler reports:
# command-line-arguments
./main.go:NN:NN: interface type loop involving Node
The workaround is to break the recursion by using a plain interface without a self-referential type parameter, and accept a small loss of type precision at the call site:
type Node interface {
Children() []Node
}
For straightforward containers and algorithms, recursive constraints are rarely needed. Reach for them only when modeling tree or graph structures, and prefer the plain interface pattern if the compiler rejects the recursive form.
When to prefer interfaces. If the function only needs to call methods and never needs to return or store the concrete type, an interface is usually simpler than a generic. For example, to print the name of something that has a Name() string method:
Generic version:
func PrintName[C Card](c C) {
fmt.Println(c.Name())
}
Interface version:
func PrintName(c interface{ Name() string }) {
fmt.Println(c.Name())
}
Both work, but the interface version has no type parameter and no type argument at the call site. Callers pass any value that implements Name() string; the compiler does not need to infer or supply a type parameter. Use the interface when you do not care about the concrete type inside the function.
When to prefer generics. Use generics when you need to preserve the concrete type: return the same type as the input, store a slice of a specific type, or avoid type assertions at call sites. The deck (which stores []C and returns C from RandomCard) and a function that returns the same type it receives are good fits for generics.
Keep constraints as narrow as the operations you use. If you use C any but call card.Name() inside the function, the compiler reports card.Name undefined (type C has no field or method Name). That is the error you get with printCard[C any](card C) in the Creating Generic Functions section. The fix is to constrain C to an interface that declares Name(), e.g. C Card. Use T constraints.Ordered (or a similar constraint) when you use < or other operators; T any does not allow those operations.
Naming type parameters. In public APIs or when the role of the type is not obvious, use meaningful names (e.g. Element, Key, Value) instead of single letters. In short, local generic functions, T, K, V are conventional and fine.
Number of type parameters. Generic functions with more than two or three type parameters become hard to read and use. Consider refactoring or using a small struct to group related type parameters.
When were generics introduced in Go?
Generics were introduced in Go 1.18. The language has supported type parameters and type constraints since then, with later releases stabilizing the implementation.
What does [T any] mean in Go?
[T any] declares a type parameter named T that can be replaced by any Go type. any is an alias for interface{}, so it places no restriction on T. You use it in generic types and functions when you do not need to call methods or use operators on values of type T.
Are Go generics slow?
In practice, generics in Go add little or no measurable overhead for typical use. The compiler uses GCShape stenciling, so pointer types often share one implementation. Benchmark hot paths with go test -bench if you are concerned; avoid unsupported performance claims.
Can I use generics with methods?
You can use generics with methods only indirectly: the receiver type can be generic (e.g. func (d *Deck[C]) AddCard(card C)), but methods cannot declare their own type parameters. So the generic is on the struct (or type), not on the method itself.
What are type constraints in Go?
Type constraints are interfaces that limit which types can be used as type arguments. They can specify a method set (e.g. types that have Name() string) or a type set (e.g. int | string or ~int). The compiler uses them to allow only valid operations on the type parameter.
When should I use interfaces instead of generics?
Use interfaces when you care only about behavior (e.g. “something that can Read”) and the set of types is open-ended. Use generics when you need to preserve or work with a specific type (e.g. “a slice of T” or “return the same type as the input”) and want compile-time type safety.
Do generics replace interfaces in Go?
No. Generics and interfaces solve different problems. Interfaces describe behavior and work with any type that implements the methods; generics let you write type-safe, reusable code that preserves concrete types. Many programs use both.
Can I create generic structs in Go?
Yes. You declare type parameters in square brackets after the struct name, e.g. type Deck[C Card] struct { cards []C }. Then you use the struct with a type argument: Deck[*PlayingCard], Deck[*TradingCard].
What versions of Go support generics?
Go 1.18 and later support generics. Use a current stable release for the best experience; the implementation has been refined in subsequent releases.
Are generics backward compatible?
Yes. Code that does not use type parameters compiles unchanged. Existing APIs that use interface{} or concrete types continue to work; you can migrate to generics gradually by introducing new generic types or functions alongside existing code.
You built a card deck program that started with a slice of interface{} and type assertions, then refactored it to use a generic Deck[C Card] and a generic printCard function. Generics gave you compile-time type safety, no type assertions at call sites, and reusable code for multiple card types while keeping the implementation clear.
This tutorial was originally written by aphistic.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
Go (or GoLang) is a modern programming language originally developed by Google that uses high-level syntax similar to scripting languages. It is popular for its minimal syntax and innovative handling of concurrency, as well as for the tools it provides for building native binaries on foreign platforms.
Browse Series: 53 tutorials
Kristin is a life-long geek and enjoys digging into the lowest levels of computing. She also enjoys learning and tinkering with new technologies.
Building future-ready infrastructure with Linux, Cloud, and DevOps. Full Stack Developer & System Administrator. Technical Writer @ DigitalOcean | GitHub Contributor | Passionate about Docker, PostgreSQL, and Open Source | Exploring NLP & AI-TensorFlow | Nailed over 50+ deployments across production environments.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
This is not a real example. No one will use interface{} instead of already existing object and original code doesn’t have a problem that should be solved with generics.
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.
Full documentation for every DigitalOcean product.
The Wave has everything you need to know about building a business, from raising funding to marketing your product.
Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.
New accounts only. By submitting your email you agree to our Privacy Policy
Scale up as you grow — whether you're running one virtual machine or ten thousand.
Sign up and get $200 in credit for your first 60 days with DigitalOcean.*
*This promotional offer applies to new accounts only.