Hello, friends! Today let’s talk about generics in Go language ~ Haha, of course the title of this article is indeed a bit exaggerated. This article aims to introduce the generics in Go language in detail and let those who have not used generics in Go language quickly master how to use generics.

Okay, no more nonsense, let’s just talk~

In daily development work using Go language, do you often encounter such a troublesome scenario: in order to process different types of data, you have to copy a piece of code with exactly the same logic several times, but in the final analysis, they are just because of theirData types are differentThat’s all.

The best way to learn new features of a language is to start with "what pain points does it solve?"

What would happen without generics?

To understand why we need generics, we first need to know how much trouble we would have without them.

Suppose we now have a very simple requirement to write a very simple function: compare the sizes of two "numeric types" and return the largest one.

If it is an integer ( int ), we will write the following code:

// compare two int type size
func MaxInt(a, b int) int {
    if a > b {
        return a
    }
    return b
}

This is indeed no problem, but if we encounter another scenario, if we need to compare floating point numbers ( float64 ) size?

Then, you can only write another one with tears in your eyes:

// compare two float64 type size
func MaxFloat64(a, b float64) float64 {
    if a > b {
        return a
    }
    return b
}

Gan! Then the question comes, if there are still float32, int32, int64 Woolen cloth? You may have to copy and paste a few times and then change the data type.

Some children's shoes say that copying and pasting is not troublesome. Yes, it is true that copying and pasting is not troublesome, but you can't resist the many methods of the same type. Suppose there is another need: to print out all the elements in a slice.

Then, your code will be written like this:

package main

import "fmt"

func PrintInts(s []int) {
    for _, v := range s {
        fmt.Println(v)
    }
}

If you need to print other types of slices, you have to copy and paste again and again...

So, is there a solution?

Now that we've talked about it, I definitely have it.

A stopgap before Go 1.18: empty interface interface{}

Before Go 1.18, experienced developers usually used empty interfaces interface{} and reflection mechanism to solve this problem.

// Use empty interface to handle multiple types
func MaxInterface(a, b interface{}) interface{} {
    // A lot of type assertions are needed here
    // And if a type that cannot be compared is passed in (such as map ), While the program is running( Runtime )will be direct Panic collapse!
    // The code needs to be written too long and is unsafe. Due to space reasons, it will not be fully demonstrated here.
    return nil
}

Fatal disadvantages of empty interfaces:

  1. Loss of type safety: The compiler cannot help you check for errors during the compilation phase. If the wrong type is passed, the program will report an error and crash when it is run.
  2. Performance loss: Reflection and type assertion will bring additional performance overhead (but it cannot be said that it cannot be used because of performance issues)
  3. The code is extremely bloated: a lot of switch x.(type) Dazzling to see

After having generics

So, what are generics?

Generics, simply put, are "Parameterization of Types", the data type is also passed in as a parameter.

In fact, if we observe the code written at the beginning of the article, you will find a pattern: except for the different data types, the code logic is the same!

Well, now that we have generics, we can integrate this type of method into one method and that's it.

In the past, when we wrote functions, the parameters were specific variables (such as a = 10 ); Now use generics,The data type itself also becomes a parameter(For example, telling the function "I now want to use int type to execute you").

Teach you step by step to write your first generic function

No matter how much text you say, it's not as practical as writing some code. Let’s take the need to compare the sizes of two numbers discussed above.

Now, we use generics to rewrite the previous Max function

Let’s look at the code first, and then we’ll break it down one by one:

package main

import "fmt"

// Customize a type constraint to indicate that these types of numbers can be used
type Number interface {
    int | int32 | int64 | float32 | float64
}

// This is a generic function!
// [T Number] is a type parameter list, indicating T can be Number any type in
func Max[T Number](a, b T) T {
    if a > b {
        return a
    }
    return b
}

func main() {
    // incoming int type
    fmt.Println("largest int yes:", Max(10, 20))         // output: 20
    
    // incoming float64 type
    fmt.Println("largest float64 yes:", Max(3.14, 2.71)) // output: 3.14
}

Dismantling of core grammar (knock on the blackboard, here comes the key point!)

Let’s take a closer look at the code above: func Max[T Number](a, b T) T

  1. [T Number] : This is called Type parameter list. It follows the function name, in square brackets [] bracketed.
  • T is the "code name" we give the type (you call it A , MyType Either way, you can just think of it as a placeholder, but the convention is T stands for Type ).
  • Number calledtype constraints, which tells the compiler: "This T It can't be a cat or a dog, it must be Number One of the several number types defined in ."
  1. (a, b T) : Ordinary function parameter list. Indicates formal parameters a and b All types are that T data type .
  2. final T : Indicates that the return value type of the function is also T .

when you call Max(10, 20) When , the Go compiler will automatically infer that what you pass in is int , so it will automatically put T Replace with int , and perform comparison logic.

Okay, through the above simple understanding, do you already have a preliminary understanding of generics?

The most commonly used built-in constraints: any and comparable

In fact, in actual development, you may not need to customize one as above every time Number . The Go language already has some commonly used type constraints built in.

1. any Constraints: Any data type can be received

any Actually it is interface{} alias.

// Print any type of slice( Slice )
// [T any] express T Can be of any type
func PrintSlice[T any](s []T) {
    for _, v := range s {
        fmt.Printf("%v ", v)
    }
    fmt.Println()
}

func main() {
    PrintSlice([]int{1, 2, 3})          // output: 1 2 3
    PrintSlice([]string{"Go", "Generics"})  // output: Go Generics
}

2. comparable Constraints: Only accept those that can be used == and != type of comparison

If you need to determine whether two values ​​are equal (such as finding the corresponding Key in a Map), you must use comparable .

It supports numbers, strings, booleans, etc., butSlices (Slice) and dictionaries (Map) are not supported, because they cannot be used == Direct comparison.

// Find the index of the element in the slice, return if not found -1
// [T comparable] express T must be able to use == type of comparison
func FindIndex[T comparable](slice []T, target T) int {
    for i, v := range slice {
        if v == target { // Because there is comparable Constraints can only be used here ==
            return i
        }
    }
    return -1
}

If you can understand the above code, congratulations, you have learned it slices in the bag slices.Index() method.

Not just functions, but also generic types!

Generics can be used not only on functions, but also on struct(structure) on. This is very useful when defining general data containers (such as stacks, queues, linked lists).

For example, if we now want to implement a general "Stack" that can store various types of data, we can write the code as follows:

// Define a generic structure
// [T any] Indicates that this stack can store any type of data
type Stack[T any] struct {
    elements []T
}

// Add to the generic stack Push method (push to stack)
// Note that the recipient should be written as *Stack[T]
func (s *Stack[T]) Push(value T) {
    s.elements = append(s.elements, value)
}

// Add to the generic stack Pop method (pop)
func (s *Stack[T]) Pop() (T, bool) {
    if len(s.elements) == 0 {
        var zero T // declare a T zero value of type
        return zero, false
    }
    // Get the last element
    lastIndex := len(s.elements) - 1
    value := s.elements[lastIndex]
    // shrink slice
    s.elements = s.elements[:lastIndex]
    return value, true
}

func main() {
    // Instantiate a dedicated storage int stack
    var intStack Stack[int]
    intStack.Push(100)
    
    // Instantiate a dedicated storage string stack
    var stringStack Stack[string]
    stringStack.Push("Hello")
}

Through the above code, we use a piece of logic to easily create different types of stack data structures. Doesn’t the code look very nice now?

Summarize

  1. Why are generics needed?: In order to reduce duplicate code and improve type safety, farewell interface{} runtime risks
  2. basic grammar: Add after the function name or type name [T constraint type]
  3. constraint type: can be used any (Any type, actually interface{}), comparable (can be judged as other types), or use interface use | Collection of symbol custom types.

When you need to deal with common logic, write underlying tool libraries, or common data structures, you can use generics to write more elegant code.

Now you should have a certain understanding of generics, right? If this article is helpful to you, don’t forget to clickpraiseandlook inHa~

Finally, I raise a question for everyone to discuss:

type Number interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
       ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
       ~float32 | ~float64
}

Why sometimes defining a generic constraint can be defined like this? ~int front ~ What does it mean? Why is it not necessary sometimes?

Here I’m just going to throw some light on things and welcome all the handsome and graceful handsome guys to chat with us~