Original: A gentle introduction to generics in Go by Dominik Braun

Wan Junfeng Kevin: I found the article to be very simple and easy to understand, so I asked the author for permission to translate it and share it with everyone.

This article is a relatively accessible introduction to the basic idea of generics and their implementation in Go, as well as a brief summary of the various performance discussions surrounding generics. First, let's look at the core problem that generics solve.

question

Suppose we want to implement a simple tree data structure. Each node holds a value. Before Go 1.18, a typical way to implement this structure was as follows.

 type Node struct {
    value interface{}
}

This works fine for the most part, but it has some drawbacks.

First, interface{} can be anything. If we want to limit value types that may be held, such as integers and floats, we can only check this limit at runtime.

 func (n Node) IsValid() bool {
    switch n.value.(type) {
        case int, float32, float64:
            return true
        default:
            return false
    }
}

This makes it impossible to restrict types at compile time, and type checking like the above is common practice in many Go libraries. Here are examples from the go-zero project .

Second, working with values in Node is very tedious and error-prone. Doing anything with a value involves some type of assertion, even though you can safely assume that the value holds a int value.

 number, ok := node.value.(int)
if !ok {
    // ...
}

double := number * 2

These are just some of the inconveniences of using interface{} which provides no type safety and has the potential to cause hard-to-recover runtime errors.

Solution

We're not going to accept arbitrary data types or concrete types, but rather define a ---1a1644a1eb445db6a1243ac5dc723bc5--- called T 占位符类型 as a type of value. Note that this code will not compile yet.

 type Node[T] struct {
    value T
}

First you need to declare the generic type T , which is used inside the square brackets after the structure or function name.

T can be any type, T will only be deduced when instantiating a Node with an explicit type.

 n := Node[int]{
    value: 5,
}

The generic Node is instantiated as Node[int] (integer node), so T is a int .

type constraints

In the above implementation, the declaration of T is missing a necessary piece of information: type constraints.

Type constraints are used to further limit the possible types that can be T . Go itself provides some predefined type constraints, but custom type constraints can also be used.

 type Node[T any] struct {
    value T
}

The any type (any) constraint allows T actually any type. If node values need to be compared, there is a comparable type constraint, and types that satisfy this predefined constraint can be compared using == .

 type Node[T comparable] struct {
    value T
}

Any type can be used as a type constraint. Go 1.18 introduced a new interface syntax that can embed other data types.

 type Numeric interface {
    int | float32 | float64
}

This means that an interface can define not only a set of methods, but also a set of types. Use the Numeric interface as a type constraint, meaning that the value can be either an integer or a float.

 type Node[T Numeric] struct {
    value T
}

regain type safety

The huge advantage of using generic type parameters over using interface{} is that the final type of T is deduced at compile time. Define a type constraint for T which completely eliminates the runtime check. If the type used as T does not satisfy the type constraints, the code will not compile.

When writing generic code, you can write code as if you already know the final type of T .

 func (n Node[T]) Value() T {
    return n.value
}

The above function returns n.Value which is of type T . So the return value is T and if T is an integer, then the return type is known to be int . Therefore, the return value can be used directly as an integer without any type assertion.

 n := Node[int]{
    value: 5,
}

double := n.Value() * 2

Restoring type safety at compile time makes Go code more reliable and less error-prone.

Generic usage scenarios

Typical usage scenarios for generics are listed in Ian Lance Taylor 's When To Use Generics , which boil down to three main cases:

  1. Use built-in container types such as slices , maps and channels
  2. Implement common data structures like linked list or tree
  3. Write a function whose implementation is the same for many types, such as a sort function

In general, use generics when you don't want to make assumptions about the contents of the values you're manipulating. The one in our example Node doesn't care much about the value it holds.

Generics are not a good choice when different types have different implementations. In addition, do not change the interface function signature such as Read(r io.Reader) Read[T io.Reader](r T) to a generic signature such as ---0941b51b6ee742c3183a9335c0c93b0b---.

performance

To understand the performance of generics and their implementation in Go, it is first necessary to understand the two most common ways of implementing generics in general.

This is an in-depth look at various properties and a brief introduction to the discussions surrounding them. You probably don't need to care much about the performance of generics in Go.

virtual method table

One way to implement generics in the compiler is to use Virtual Method Table . Generic functions are modified to accept only pointers as parameters. These values are then allocated on the heap and pointers to these values are passed to the generic function. This is done because the pointer always looks the same, no matter what type it points to.

If the values are objects, and the generic function needs to call methods on those objects, it can no longer do so. The function just has a pointer to the object and doesn't know where their methods are. Therefore, it needs a table where the memory addresses of methods can be queried: Virtual Method Table . This so-called dynamic dispatch is already used by interfaces in languages like Go and Java.

Virtual Method Table can be used not only to implement generics, but also to implement other types of polymorphism. However, deducing these pointers and calling virtual functions is slower than calling functions directly, and using Virtual Method Table prevents the compiler from optimizing.

Monomorphization

A simpler approach is monomorphization ( Monomorphization ), where the compiler generates a copy of the generic function for each called data type.

 func max[T Numeric](a, b T) T {
    // ...
}

larger := max(3, 5)

Since the max function shown above is called with two integers, the compiler will generate a copy of max for int when it monomorphizes the code.

 func maxInt(a, b int) int {
    // ...
}

larger := maxInt(3, 5)

The biggest advantage is that Monomorphization brings significantly better runtime performance than using Virtual Method Table . Direct method calls are not only more efficient, but also apply the entire compiler's optimization chain. However, this comes at a cost of compile time, and generating copies of generic functions for all relevant types is very time consuming.

Implementation in Go

Which of these two approaches is best for Go? Fast compilation is important, but so is runtime performance. To meet these requirements, the Go team decided to mix the two approaches when implementing generics.

Go uses Monomorphization , but tries to reduce the number of function copies that need to be generated. Instead of making a copy for each type, it makes a copy for each layout in memory: int , float64 , Node and other so called "值类型" will all look different in memory, so the generic function will make a copy for all of these types.

In contrast to value types, pointers and interfaces always have the same layout in memory. The compiler will generate a copy of the generic function for pointer and interface calls. Like Virtual Method Table , generic functions receive pointers, so a table is needed to dynamically look up method addresses. Dictionaries in the Go implementation have the same performance characteristics as virtual method tables.

in conclusion

The benefit of this hybrid approach is that you get the performance benefit of Monomorphization in calls using value types, and only pay the cost of Virtual Method Table in calls using pointers or interfaces .

What is often overlooked in performance discussions is that all these benefits and costs relate only to function calls. Typically, most of the execution time is spent inside functions. The performance overhead of calling a method may not be a performance bottleneck, and even if it is, consider optimizing the function implementation first, and then consider the calling overhead.

read more

Vicent Marti: Generics can make your Go code slower (PlanetScale)

Andy Arthur: Generics and Value Types in Golang (Dolthub)

Virtual method table (Wikipedia)

Monomorphization (Wikipedia)

Dynamic dispatch (Wikipedia)

Impact on the standard library

As part of Go 1.18, 不改变标准库 was a prudent decision. The current plan is to gather experience with generics, learn how to use them appropriately, and figure out reasonable use cases in the standard library.

Go has some proposals for generic packages, functions and data structures:

  • constraints , providing type constraints ( #47319 )
  • maps , providing generic map functions ( #47330 )
  • slices , providing generic slice functions ( #47203 )
  • sort.SliceOf , a generic sort implementation ( #47619 )
  • sync.PoolOf and other generic concurrent data structures ( #47657 )

Plans for go-zero generics

We are cautious about rewriting go-zero support with generics, because once generics are used, the Go version must be upgraded from 1.15 to 1.18. Many users' online services have not been upgraded to the latest version, so go-zero's The generic rewrite will delay Go by two or three versions to ensure that most of the online services of users have been upgraded to Go 1.18

go-zero is also doing sufficient research and attempts on generics.

Among them, the mr package has been newly opened to support generics:

https://github.com/kevwan/mapreduce

Among them, the fx package has also been opened in a new warehouse to try to support generics, but due to the lack of template method , it has not been completed, and I look forward to the improvement of Go generics in the future

https://github.com/kevwan/stream

When the follow-up go-zero supports generics, we will incorporate these well-tested generic implementations.

project address

https://github.com/zeromicro/go-zero

Welcome go-zero and star support us!

WeChat exchange group

Follow the official account of " Microservice Practice " and click on the exchange group to get the QR code of the community group.

If you have go-zero use experience articles, or source code study notes, please contact the public account to contribute!


kevinwan
931 声望3.5k 粉丝

go-zero作者