To be clear, I’ll provide general guidelines, not hard and fast rules. Use your own judgement. But if you aren’t sure, I recommend using the guidelines discussed here.To be clear, I’ll provide general guidelines, not hard and fast rules. Use your own judgement. But if you aren’t sure, I recommend using the guidelines discussed here.

Go: When Should You Use Generics? When Shouldn't You?

2025/10/04 23:00

Introduction

This is the blog post version of my talks at Google Open Source Live:

https://youtu.be/nr8EpUO9jhw?si=jlWTapr6NM6isLgt&embedable=true

and GopherCon 2021:

https://youtu.be/Pae9EeCdy8?si=M-87Eisb2nU1qmJ&embedable=true

\ The Go 1.18 release adds a major new language feature: support for generic programming. In this article I’m not going to describe what generics are nor how to use them. This article is about when to use generics in Go code, and when not to use them.

\ To be clear, I’ll provide general guidelines, not hard and fast rules. Use your own judgement. But if you aren’t sure, I recommend using the guidelines discussed here.

Write code

Let’s start with a general guideline for programming Go: write Go programs by writing code, not by defining types. When it comes to generics, if you start writing your program by defining type parameter constraints, you are probably on the wrong path. Start by writing functions. It’s easy to add type parameters later when it’s clear that they will be useful.

When are type parameters useful?

That said, let’s look at cases for which type parameters can be useful.

When using language-defined container types

One case is when writing functions that operate on the special container types that are defined by the language: slices, maps, and channels. If a function has parameters with those types, and the function code doesn’t make any particular assumptions about the element types, then it may be useful to use a type parameter.

\ For example, here is a function that returns a slice of all the keys in a map of any type:

// MapKeys returns a slice of all the keys in m. // The keys are not returned in any particular order. func MapKeys[Key comparable, Val any](m map[Key]Val) []Key {     s := make([]Key, 0, len(m))     for k := range m {         s = append(s, k)     }     return s } 

\ This code doesn’t assume anything about the map key type, and it doesn’t use the map value type at all. It works for any map type. That makes it a good candidate for using type parameters.

\ The alternative to type parameters for this kind of function is typically to use reflection, but that is a more awkward programming model, is not statically typechecked at build time, and is often slower at run time.

General purpose data structures

Another case where type parameters can be useful is for general purpose data structures. A general purpose data structure is something like a slice or map, but one that is not built into the language, such as a linked list, or a binary tree.

\ Today, programs that need such data structures typically do one of two things: write them with a specific element type, or use an interface type. Replacing a specific element type with a type parameter can produce a more general data structure that can be used in other parts of the program, or by other programs. Replacing an interface type with a type parameter can permit data to be stored more efficiently, saving memory resources; it can also permit the code to avoid type assertions, and to be fully type checked at build time.

\ For example, here is part of what a binary tree data structure might look like using type parameters:

// Tree is a binary tree. type Tree[T any] struct {     cmp  func(T, T) int     root *node[T] }  // A node in a Tree. type node[T any] struct {     left, right  *node[T]     val          T }  // find returns a pointer to the node containing val, // or, if val is not present, a pointer to where it // would be placed if added. func (bt *Tree[T]) find(val T) **node[T] {     pl := &bt.root     for *pl != nil {         switch cmp := bt.cmp(val, (*pl).val); {         case cmp < 0:             pl = &(*pl).left         case cmp > 0:             pl = &(*pl).right         default:             return pl         }     }     return pl }  // Insert inserts val into bt if not already there, // and reports whether it was inserted. func (bt *Tree[T]) Insert(val T) bool {     pl := bt.find(val)     if *pl != nil {         return false     }     *pl = &node[T]{val: val}     return true } 

\ Each node in the tree contains a value of the type parameter T. When the tree is instantiated with a particular type argument, values of that type will be stored directly in the nodes. They will not be stored as interface types.

\ This is a reasonable use of type parameters because the Tree data structure, including the code in the methods, is largely independent of the element type T.

\ The Tree data structure does need to know how to compare values of the element type T; it uses a passed-in comparison function for that. You can see this on the fourth line of the find method, in the call to bt.cmp. Other than that, the type parameter doesn’t matter at all.

For type parameters, prefer functions to methods

The Tree example illustrates another general guideline: when you need something like a comparison function, prefer a function to a method.

\ We could have defined the Tree type such that the element type is required to have a Compare or Less method. This would be done by writing a constraint that requires the method, meaning that any type argument used to instantiate the Tree type would need to have that method.

\ A consequence would be that anybody who wants to use Tree with a simple data type like int would have to define their own integer type and write their own comparison method. If we define Tree to take a comparison function, as in the code shown above, then it is easy to pass in the desired function. It’s just as easy to write that comparison function as it is to write a method.

\ If the Tree element type happens to already have a Compare method, then we can simply use a method expression like ElementType.Compare as the comparison function.

\ To put it another way, it is much simpler to turn a method into a function than it is to add a method to a type. So for general purpose data types, prefer a function rather than writing a constraint that requires a method.

Implementing a common method

Another case where type parameters can be useful is when different types need to implement some common method, and the implementations for the different types all look the same.

\ For example, consider the standard library’s sort.Interface. It requires that a type implement three methods: Len, Swap, and Less.

\ Here is an example of a generic type SliceFn that implements sort.Interface for any slice type:

// 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]) } 

\ For any slice type, the Len and Swap methods are exactly the same. The Less method requires a comparison, which is the Fn part of the name SliceFn. As with the earlier Tree example, we will pass in a function when we create a SliceFn.

\ Here is how to use SliceFn to sort any slice using a comparison function:

// 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}) } 

\ This is similar to the standard library function sort.Slice, but the comparison function is written using values rather than slice indexes.

\ Using type parameters for this kind of code is appropriate because the methods look exactly the same for all slice types.

\ (I should mention that Go 1.19–not 1.18–will most likely include a generic function to sort a slice using a comparison function, and that generic function will most likely not use sort.Interface. See proposal #47619. But the general point is still true even if this specific example will most likely not be useful: it’s reasonable to use type parameters when you need to implement methods that look the same for all the relevant types.)

When are type parameters not useful?

Now let’s talk about the other side of the question: when not to use type parameters.

Don’t replace interface types with type parameters

As we all know, Go has interface types. Interface types permit a kind of generic programming.

\ For example, the widely used io.Reader interface provides a generic mechanism for reading data from any value that contains information (for example, a file) or that produces information (for example, a random number generator). If all you need to do with a value of some type is call a method on that value, use an interface type, not a type parameter. io.Reader is easy to read, efficient, and effective. There is no need to use a type parameter to read data from a value by calling the Read method.

\ For example, it might be tempting to change the first function signature here, which uses just an interface type, into the second version, which uses a type parameter.

func ReadSome(r io.Reader) ([]byte, error)  func ReadSome[T io.Reader](r T) ([]byte, error) 

\ Don’t make that kind of change. Omitting the type parameter makes the function easier to write, easier to read, and the execution time will likely be the same.

\ It’s worth emphasizing the last point. While it’s possible to implement generics in several different ways, and implementations will change and improve over time, the implementation used in Go 1.18 will in many cases treat values whose type is a type parameter much like values whose type is an interface type. What this means is that using a type parameter will generally not be faster than using an interface type. So don’t change from interface types to type parameters just for speed, because it probably won’t run any faster.

\

Don’t use type parameters if method implementations differ

When deciding whether to use a type parameter or an interface type, consider the implementation of the methods. Earlier we said that if the implementation of a method is the same for all types, use a type parameter. Inversely, if the implementation is different for each type, then use an interface type and write different method implementations, don’t use a type parameter.

\ For example, the implementation of Read from a file is nothing like the implementation of Read from a random number generator. That means that we should write two different Read methods, and use an interface type like io.Reader.

Use reflection where appropriate

Go has run time reflection. Reflection permits a kind of generic programming, in that it permits you to write code that works with any type.

\ If some operation has to support even types that don’t have methods (so that interface types don’t help), and if the operation is different for each type (so that type parameters aren’t appropriate), use reflection.

\ An example of this is the encoding/json package. We don’t want to require that every type that we encode have a MarshalJSON method, so we can’t use interface types. But encoding an interface type is nothing like encoding a struct type, so we shouldn’t use type parameters. Instead, the package uses reflection. The code is not simple, but it works. For details, see the source code.

One simple guideline

In closing, this discussion of when to use generics can be reduced to one simple guideline.

\ If you find yourself writing the exact same code multiple times, where the only difference between the copies is that the code uses different types, consider whether you can use a type parameter.

\ Another way to say this is that you should avoid type parameters until you notice that you are about to write the exact same code multiple times.


Ian Lance Taylor

\ This article is available on The Go Blog under a CC BY 4.0 DEED license.

\ Photo by Michael Dziedzic on Unsplash

Disclaimer: The articles reposted on this site are sourced from public platforms and are provided for informational purposes only. They do not necessarily reflect the views of MEXC. All rights remain with the original authors. If you believe any content infringes on third-party rights, please contact [email protected] for removal. MEXC makes no guarantees regarding the accuracy, completeness, or timeliness of the content and is not responsible for any actions taken based on the information provided. The content does not constitute financial, legal, or other professional advice, nor should it be considered a recommendation or endorsement by MEXC.

You May Also Like

Why Digitap ($TAP) is the Best Crypto Presale December Follow-Up

Why Digitap ($TAP) is the Best Crypto Presale December Follow-Up

The post Why Digitap ($TAP) is the Best Crypto Presale December Follow-Up appeared on BitcoinEthereumNews.com. Crypto Projects Hyperliquid’s HYPE has seen another disappointing week. The token struggled to hold the $30-$32 price range after 9.9M tokens were unlocked and added to the circulating supply. Many traders are now watching whether HYPE will reclaim the $35 area as support or break down further towards the high $20s. Unlike Hyperliquid, whose trading volume is shrinking, Digitap ($TAP), a rising crypto presale project, has already raised over $2 million in just weeks. This is all thanks to its live omnibank app that combines crypto and fiat tools in a single, seamless account. While popular altcoins stall, whales are channeling capital into early-stage opportunities. This shift is shaping discussions on the best altcoins to buy now in the current market dynamics. Hyperliquid Spot Trades Clustered Between the Low and Mid $30s HYPE price closed the week with an 11% loss. This is because a significant portion of its spot trades are clustered between the low and mid $30s. This leaves the token with a multi-billion-dollar fully diluted valuation on its daily trading volume. Source: CoinMarketCap Moreover, HYPE’s daily RSI is still stuck above $40s, while the short-term averages are continually dropping. This shows an indecisiveness, where the bears and the bulls don’t have clear control of the market. Additionally, roughly 2.6% of the circulating supply is in circulation. After unlocking 9.9M tokens, the Hyperliquid team spent over $600 million on buybacks. This amount often buys only a few million tokens a day. That steady demand is quite small compared to the 9.9 million tokens that were released. This has left the HYPE market with an oversupply. Many HYPE holders are now rotating capital into crypto presale projects, like Digitap, that offer immediate upside. HYPE Market Sentiments Shows Mixed Signals Traders are now projecting mixed sentiments for the token. Some…
Share
BitcoinEthereumNews2025/12/08 22:17
EUR/CHF slides as Euro struggles post-inflation data

EUR/CHF slides as Euro struggles post-inflation data

The post EUR/CHF slides as Euro struggles post-inflation data appeared on BitcoinEthereumNews.com. EUR/CHF weakens for a second straight session as the euro struggles to recover post-Eurozone inflation data. Eurozone core inflation steady at 2.3%, headline CPI eases to 2.0% in August. SNB maintains a flexible policy outlook ahead of its September 25 decision, with no immediate need for easing. The Euro (EUR) trades under pressure against the Swiss Franc (CHF) on Wednesday, with EUR/CHF extending losses for the second straight session as the common currency struggles to gain traction following Eurozone inflation data. At the time of writing, the cross is trading around 0.9320 during the American session. The latest inflation data from Eurostat showed that Eurozone price growth remained broadly stable in August, reinforcing the European Central Bank’s (ECB) cautious stance on monetary policy. The Core Harmonized Index of Consumer Prices (HICP), which excludes volatile items such as food and energy, rose 2.3% YoY, in line with both forecasts and the previous month’s reading. On a monthly basis, core inflation increased by 0.3%, unchanged from July, highlighting persistent underlying price pressures in the bloc. Meanwhile, headline inflation eased to 2.0% YoY in August, down from 2.1% in July and slightly below expectations. On a monthly basis, prices rose just 0.1%, missing forecasts for a 0.2% increase and decelerating from July’s 0.2% rise. The inflation release follows last week’s ECB policy decision, where the central bank kept all three key interest rates unchanged and signaled that policy is likely at its terminal level. While officials acknowledged progress in bringing inflation down, they reiterated a cautious, data-dependent approach going forward, emphasizing the need to maintain restrictive conditions for an extended period to ensure price stability. On the Swiss side, disinflation appears to be deepening. The Producer and Import Price Index dropped 0.6% in August, marking a sharp 1.8% annual decline. Broader inflation remains…
Share
BitcoinEthereumNews2025/09/18 03:08