2

Hello everyone, I am fried fish.

Go1.18's generics is a lot of trouble, although I have written a lot of design and thinking about generics before. But because the generic proposal has not been finalized before, a complete introduction has not been written.

Now that it has basically taken shape, the fried fish will take everyone to understand Go generics. The content of this article mainly involves three major concepts of generics, which are worthy of your in-depth understanding.

as follows:

  • Type parameters.
  • Type constraints.
  • Type inference.

Type parameter

Type parameters, the noun. The unfamiliar buddies are confused at first glance.

Generic code is written using abstract data types, which we call type parameters. When the program runs the general code, the type parameter will be replaced by the type parameter. That is, the type parameter is the generic abstract data type .

Simple generic example:


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

The code has a Print , which prints out each element of a fragment. The element type of the fragment, here called T, is unknown.

This leads to a point for the design of generic syntax, that is: T's generic type parameter, how should be defined?

In the existing design, it is divided into two parts:

  • Type parameter list: The type parameter list will appear in front of the parameter 161d5219a75e55. In order to distinguish between the type parameter list and the regular parameter list, the type parameter list uses square brackets instead of parentheses.
  • Type parameter constraints: Just like regular parameters have types, type parameters also have meta types, which are called constraints (more on that later).

The complete example is as follows:

// Print 可以打印任何片断的元素。
// Print 有一个类型参数 T,并有一个单一的(非类型)的 s,它是该类型参数的一个片断。
func Print[T any](s []T) {
    // do something...
}

In the above code, we declare a function Print , which has a type parameter T, type constraint is any , expressed as an arbitrary type, the function is the same as interface{} His s variable 061d5219a75eba is a slice of type T.

After the function is declared, we need to specify the type of the type parameter when calling the function. as follows:

    Print[int]([]int{1, 2, 3})

In the above code, we specified the type parameter passed in as int, and passed in []int{1, 2, 3} as the parameter.

Other types, such as float64:

    Print[float64]([]float64{0.1, 0.2, 0.3})

It's a similar way of declaration, just follow the set.

Type constraints

After talking about type parameters, let's talk about "constraints". Type constraints must be specified in all type parameters to be called complete generics.

The following is divided into two parts to explain in detail:

  • Define function constraints.
  • Define operator constraints.

Why type constraints

In order to ensure that the caller can meet the program requirements of the , to ensure that the functions and operators used in the program can operate normally.

Generic type parameters and type constraints complement each other.

Define function constraints

question

Let's take a look at the examples provided by Go official:

func Stringify[T any](s []T) (ret []string) {
    for _, v := range s {
        ret = append(ret, v.String()) // INVALID
    }
    return ret
}

The purpose of this method is: any type of slice can be converted into a corresponding string slice. But there is a problem in the program logic, that is, his any parameter T is of type 061d5219a75ff0, any type can be passed in.

It calls the String method internally, and naturally it will report an error, because only types like int, float64, etc. may not implement this method.

You said that you want to define effective type constraints. It's like the example above. How do you implement it in generics?

If the incoming party is required to have a built-in method, a interface must be defined to restrict him.

Single type

Examples are as follows:

type Stringer interface {
    String() string
}

Application in generic methods:

func Stringify[T Stringer](s []T) (ret []string) {
    for _, v := range s {
        ret = append(ret, v.String())
    }
    return ret
}

Then put the Stringer type to the original any type to achieve the requirements required by the program.

Multiple types

If it is multiple type constraints. Examples are as follows:

type Stringer interface {
    String() string
}

type Plusser interface {
    Plus(string) string
}

func ConcatTo[S Stringer, P Plusser](s []S, p []P) []string {
    r := make([]string, len(s))
    for i, v := range s {
        r[i] = p[i].Plus(v.String())
    }
    return r
}

The rules are the same as the regular input parameter and output parameter type declaration.

Define operator constraints

After completing the definition of the function constraints, the remaining big bone to be gnawed is the "operator" constraint.

question

Let's take a look at the official example of Go:

func Smallest[T any](s []T) T {
    r := s[0] // panic if slice is empty
    for _, v := range s[1:] {
        if v < r { // INVALID
            r = v
        }
    }
    return r
}

After the above function example, we quickly realized that this program simply cannot run successfully.

any parameter is of type 061d5219a76122. Inside the program, the value is obtained according to the slice type, and the operator comparison is performed internally. If it is really a slice, each value type may be different internally.

If one is slice and the other is int type, how to compare the values of operators?

Approximate element

Some students may think of overloaded operators, but...too much thinking, Go language has no plan to support it. To this end, a new design was made, which allowed to limit the type range of type parameters.

The syntax is as follows:

InterfaceType  = "interface" "{" {(MethodSpec | InterfaceTypeName | ConstraintElem) ";" } "}" .
ConstraintElem = ConstraintTerm { "|" ConstraintTerm } .
ConstraintTerm = ["~"] Type .

Examples are as follows:

type AnyInt interface{ ~int }

The set of types declared above is ~int , that is, all types of int (such as: int, int8, int16, int32, int64) can satisfy the condition of this type constraint.

Including the underlying type is int8 type, for example:

type AnyInt8 int8

That is, within the matching range.

Joint element

If you want to further narrow the limited type, you can use it in combination with the separator, the usage is:

type AnyInt interface{
 ~int8 | ~int64
}

You can limit the type set to int8 and int64.

Implement operator constraints

Based on the new grammar, combined with the new concept union and approximate elements, the program can be transformed to realize the matching of operators in generics.

The declaration of type constraints is as follows:

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

The application procedures are as follows:

func Smallest[T Ordered](s []T) T {
    r := s[0] // panics if slice is empty
    for _, v := range s[1:] {
        if v < r {
            r = v
        }
    }
    return r
}

After ensuring that the values are all basic data types, the program can run normally.

Type inference

When programmers write code, a certain degree of laziness is inevitable.

In certain scenarios, type inference can be used to avoid explicitly writing some or all of the type parameters, and the compiler will automatically recognize them.

It is recommended that complex functions and parameters are clear, otherwise it will be more troublesome for students who read the code. The guarantee of readability and maintainability is also an important point in the work.

Parameter derivation

Function example. as follows:

func Map[F, T any](s []F, f func(F) T) []T { ... }

Common code snippets. as follows:

var s []int
f := func(i int) int64 { return int64(i) }
var r []int64

Explicitly specify two type parameters. as follows:

r = Map[int, int64](s, f)

Only the first type parameter is specified, and the variable f is inferred. as follows:

r = Map[int](s, f)

Do not specify any type parameters, let both be inferred. as follows:

r = Map(s, f)

Constraint Derivation

The magic is that type derivation is not limited to this, even constraints can be deduced.

Examples of functions are as follows:

func Double[E constraints.Number](s []E) []E {
    r := make([]E, len(s))
    for i, v := range s {
        r[i] = v + v
    }
    return r
}

The derivation case based on this is as follows:

type MySlice []int

var V1 = Double(MySlice{1})

MySlice is an alias for the slice type of int. The type of variable V1 is deduced by the compiler []int, which is not MySlice.

The reason is that when the compiler compares the types of the two, it will recognize the MySlice type as []int, which is the int type.

To achieve the "correct" derivation, the following definitions are needed:

type SC[E any] interface {
    []E 
}

func DoubleDefined[S SC[E], E constraints.Number](s S) S {
    r := make(S, len(s))
    for i, v := range s {
        r[i] = v + v
    }
    return r
}

Derive a case based on this. as follows:

var V2 = DoubleDefined[MySlice, int](MySlice{1})

As long as the explicit type parameters are defined, the correct type can be obtained, and the type of the variable V2 will be MySlice.

What if the constraints are not declared? as follows:

var V3 = DoubleDefined(MySlice{1})

The compiler derives through function parameters, and it can also make it clear that the variable V3 type is MySlice.

Summarize

Today we introduced three important concepts of generics in this article, namely:

  • Type parameters: generic abstract data types.
  • Type constraints: to ensure that the caller can meet the program requirements of the recipient.
  • Type inference: Avoid explicitly writing some or all type parameters.

New concepts such as joint elements, approximate elements, function constraints, and operator constraints are also involved in the content. In essence, they are all new solutions based on the extension of three major concepts, one after another.

Have you learned Go generics? How about the design? Welcome to discuss together :)

If you have any questions, welcome feedback and exchanges in the comment area. The best relationship between , and your likes is the biggest motivation for the creation of fried fish

The article is continuously updated, and you can read it on search [the brain is fried fish], this article 161d5219a764df GitHub github.com/eddycjy/blog has been included, learn the Go language can see Go learning map and route more. Welcome to

refer to


煎鱼
8.4k 声望12.8k 粉丝