A Beginner’s Guide to Generics in Go (2024)

A Beginner's Guide to Generics in Go (2024)

Introduction to Generics in Go

Generics are one of the most anticipated features added to Go in recent years. With generics, developers can write reusable functions and types that work with different data types.

Prior to generics, Go developers had to use empty interfaces like interface{} to write generic code. However, this came with run-time costs and lost type safety.

Generics make code:

  • Reusable – Generic functions and types work across different data types.
  • Safe – The compiler checks for type errors during compilation.
  • Performant – Generics avoid reflection and interface costs at run-time.

Overall, generics help you write more flexible and robust Go code with improved type-safety. This beginner’s guide will cover everything you need to start using generics effectively.

Generics Syntax in Go

The syntax for generics looks like this:

func MapKeys[K comparable, V any](m map[K]V) []K {
  r := make([]K, 0, len(m))
  for k := range m {
    r = append(r, k)
  }
  return r
}
  • Square brackets [] after the function name declare type parameters.
  • comparable and any are constraints limiting which types can be used.
  • K and V act as placeholder types in the function body.

When calling a generic function, concrete types are specified inside angle brackets <> like:

keys := MapKeys[string, int](map[string]int{"a": 1, "b": 2}) 
// Keys will be of type []string

This allows MapKeys to work with any map key/value types that meet the constraints.

Type Parameters and Constraints

Type parameters like K and V above act as placeholder types that will be set to concrete types when the function is called.

You can declare multiple type parameters separated by commas:

func Reverse[T any](s []T) {
  // ...
}

By default, type parameters allow any type. But you can limit which types are allowed using a constraint:

func Sort[T constraints.Ordered](data []T) {
  // ...
}

Common constraints include:

  • any – Allows any type. This is the default.
  • comparable – Allows types that support == and != operators.
  • ordered – Allows types that support <<=> operators like integers, strings.
  • numeric – Allows signed/unsigned integers and floats.

Multiple constraints can be combined with the , operator:

func Foo[T comparable, numeric](x T) {
  // ...
}

Generic Types

Along with generic functions, we can now define generic types like List, Map, etc.

type List[T any] struct {
  data []T
}

func (l *List[T]) Add(v T) {
  l.data = append(l.data, v)
}

List acts like a reusable blueprint. We can make concrete versions by specifying a type like List[int] or List[string].

Generic Interfaces

Interfaces can also be parameterized to define generic contracts:

type Serializer[T any] interface {
  Serialize(T) []byte
  Deserialize([]byte) T
}

Structs can then implement the interface for specific types:

type JSONSerializer[T comparable] struct {}

func (j *JSONSerializer[T]) Serialize(obj T) []byte {
  // serialize obj to JSON
}

func (j *JSONSerializer[T]) Deserialize(data []byte) T {
  // deserialize JSON data to T
}

Real World Examples

Let’s look at some realistic use cases where generics really shine.

Search Functions

Searching slices is a common task. With generics, we can write a reusable Search function:

func Search[T comparable](slice []T, value T) int {
  for i, v := range slice {
    if v == value {
      return i 
    }
  }
  return -1
}

Now Search will work with any comparable slice:

idx := Search[string](names, "Jack") // string slice
idx = Search[int]([]int{1,2,3}, 2) // int slice

Caching Layer

A simple cache may look like:

type Cache struct {
  store map[string]interface{}
}

func (c *Cache) Get(key string) interface{} {
  return c.store[key]
}

func (c *Cache) Set(key string, value interface{}) {
  c.store[key] = value
}

But this loses type safety since everything is stored as interface{}.

With generics, we can build a type-safe cache:

type Cache[K comparable, V any] struct {
  store map[K]V
}

func (c *Cache[K, V]) Get(key K) V {
  return c.store[key]
} 

// ...

Binary Tree

Trees are commonly parameterized data structures. A binary tree node may look like:

type TreeNode[T comparable] struct {
  Value T

  Left, Right *TreeNode[T] 
}

We can implement reusable traversals and operations using this generic node definition.

Alternatives to Generics

Prior to generics, Go developers would use:

  • Empty interfaces like interface{} – loses type safety
  • Code generation tools like genny – extra complexity
  • Reflection – runtime overhead

While these approaches work, generics provide the best combination of type-safety, performance and usability.

Conclusion

Generics are a big step forward in Go’s type system. They enable writing reusable functions and data structures that work with different types.

This guide covered the basic syntax, type parameters, constraints, and several realistic use case examples. Overall, generics make Go a more powerful, expressive language.

To learn more, I recommend reading Go’s Generics Draft Design which dives deeper into motivation and technical details.

Now go forth and start building some awesome generic code! Let me know if you have any other generics topics you’d like explained.

FAQ

Here are some frequently asked questions about generics in Go:

Why were generics added recently? Why not in Go 1.0?

The Go team wanted to keep the language simple at first. They prioritized leaving out features like generics that add complexity.

Over time, “missing” features like generics became more requested by developers. The team worked through multiple draft proposals before landing on the current design.

What Go version added generics?

Generics were introduced in Go 1.18, released in March 2022. This was a major release that also added enums and improved error handling.

Are generics available everywhere now?

Yes, generics are usable in all environments since Go 1.18. Previously some features would be draft-only, but generics are fully production-ready.

Will generics make my code slower?

No, generics are just as performant as regular Go code. The compiler handles all the work during build time. There is no reflection overhead at runtime.

Are generics like C++ templates?

Similar, but Go’s generics are simpler and safer. Generic code only gets compiled once in Go, avoiding C++’s issues with code bloat.

When should I use generics?

Use generics when you need reusable and flexible functions, types, or interfaces. Generics shine for data structures, algorithms, and places where type-safety is important.

What if my Go version is still old?

You should upgrade to Go 1.18 or newer to use generics. If that’s not possible yet, you can still use type assertion based approaches. But upgrading is recommended to benefit from generics.

Leave a Reply

Your email address will not be published. Required fields are marked *