Golang Generics in Practice

Original link: https://windard.com/blog/2022/05/17/Golang-Generic

Generics

What are generics

Introductory Definitions, Generics, Type Parameters and Type Arguments

In the function definition, the parameters required by the definition are called formal parameters, and the parameters actually passed in are called actual parameters. In a strongly typed programming language, the types of formal parameters and actual parameters are required to be consistent.

There is a problem with this, which causes our function to be very restrictive, especially when Golang has many types of numbers.

 package main import "fmt" func MinInt ( a , b int ) int { if a <= b { return a } else { return b } } func main () { fmt . Println ( MinInt ( 1 , 2 )) }

For a function that compares the size like this, (a,b) is the formal parameter, (1,2) is the actual parameter, we may have to write another function for different types.

If you can not limit the type of formal parameters, and specify the specific type when the function is called, can this problem be solved?

Then we need to introduce the concept of Type Parameter and Type Argument.


Suppose there is such a function, which uses type parameters when the function is defined, and then passes in the parameter type as a parameter to parameterize the type.

When a function is called, not only parameters need to be passed in, but also the type of the parameter. You can pass the type like a method parameter to specify the parameter type that is actually executed.

 func min [ T Numeric ]( a , b T ) T { if a < b { return a } return b } func main () { fmt . Println ( min [ int ]( 45 , 74 )) fmt . Println ( min [ float64 ]( 4.141 , 2.01 )) }

In this way, a function can handle multiple different types of data, and the parameter types can be specified when the function is called. This method is called generic programming.

In fact, it is not that in generic programming, there is no need to care about the parameter type, but the parameter type is determined a posteriori, and the function will still be compiled according to the actual parameter type when Golang compiles.

Therefore, after the introduction of generics in Go1.18, it will have a certain impact on the compilation performance. There will be about 18% performance drop , but it will have no impact on the running performance.

The benefits of introducing generics

  1. Type checking during compilation to improve type safety
  2. Eliminate coercion by specifying a type
  3. It can reduce code duplication and provide more general functions.

However, generics are not a silver bullet. Not all scenarios require generic programming. Mainly in the case of writing the exact same logic code for different types of data, you can consider using generics.

How to use generics

Generic type is the type that refers to the generic type, that is, type parameterization. There is no need to determine the type of the parameter when defining it, and the parameter type can be specified when calling.

Here we removed the function, because the actual use of generics is not only in functions, but also in types, interfaces, functions, etc.

There are three main types of applications

  1. generic type
  2. generic interface
  3. generic function

generic type

It is often used for some collection types, such as the design of generic stacks. Because of Golang’s strongly typed language, general stacks can only be of fixed types, and generic stacks can determine specific element types after instantiation.

For example, when designing a list and dictionary that supports integers and strings, the generic placeholders T , K , and V can be used here, as long as they are consistent in the same definition.

 package main import "fmt" type ListType [ T int | int32 | int64 | string ] [] T type MapType [ K int | int32 , V int64 | string ] map [ K ] V func main () { var intList ListType [ int ] intList = [] int { 1 , 2 , 3 } fmt . Println ( intList ) strList := ListType [ string ]{ "1" , "2" , "3" } fmt . Println ( strList ) intMap := MapType [ int , string ]{ 1 : "1" , 2 : "2" } int32Map := MapType [ int32 , int64 ]{ 1 : 2 , 3 : 4 } fmt . Println ( intMap ) fmt . Println ( int32Map ) }

There is also an example of a simple generic class

 

The generic placeholder T here is the type parameter. In the optional type of T, int | int32 is the type constraint of T (Type Constraint)

That is, when the actual type parameter is passed in, only the type limited in the type parameter list can be used, where any represents any type, same as interface{} .

generic interface

An interface is a further abstraction of a class, Abstracting classes into interfaces is an essential skill , you only need to give the function signature when the interface is defined without completing its specific logic.

Golang will automatically look up the methods defined on the class. If a class implements all the functions defined by the interface, it can be considered that the class implements the interface.

In an object-oriented programming language, when using an interface, there is no need to specify its specific implementation class in the function definition, and it is only necessary to pass in the parameter object that implements the method defined by the interface.

When the function is defined, the interface can also be used as the parameter type, so that any object that implements the interface can be passed in.

Note that <br /> the interface here refers to interface , not interface{} .

Because interface{} as an empty interface is also considered to be a base object type in Golang, similar to Object in Java

Therefore, in order to avoid ambiguity and reduce the writing cost, after Go1.18, the any type is added to replace the interface{} type, which can be fully replaced in the code.

You can use this line of code for full replacement gofmt -w -r 'interface{} -> any' ./...

For a generic interface, the generic type can be declared when the interface is defined without restricting its specific implementation type.

The same as the generic stack mentioned above, the generic interface can be used for abstraction.

 type GenericStackInterface [ T any ] interface { Push ( element T ) Pop () T }

In addition to the generic interface, in Golang 1.18 the interface also adds the concept of type collection, which can add multiple types to the interface definition.

Note that the previous interface is a set of functions, which can be used to declare some functions, but in Go1.18, the set of types has been expanded, and some types can be declared in the interface. In the same interface, there can be both a set of types and a set of functions. .

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

Like this is the type set, the previous interface is the original method set, also known as the basic interface, the basic interface is also an empty type set.

In the type set, multiple types in the same row are connected by | , which means that the types take the union. If it is divided into multiple rows, the intersection between the rows is taken.

If the intersection is empty, it is an empty set. An empty set is not equal to an empty interface. An empty interface means that any type can be used, and an empty set means that no type can be used.

When defining a generic interface, the generic type set and interface set cannot use type parameters and type set at the same time, nor can recursive definition be used in the type set.

generic function

In addition to being used in class definitions and interface definitions, generics can be used in function definitions as input or output parameter types.

For example, in the design of the generic stack above, the definition of the generic function has been used for the operations of pushing and popping the stack.

For another example, in the size comparison scenario mentioned at the beginning, we can define type parameters to allow multiple different types of shaping comparisons

 func minInt [ T int | int8 | int16 | int32 ]( a , b T ) T { if a < b { return a } return b } func maxInt [ T int | int8 | int16 | int32 ]( a , b T ) T { if a > b { return a } return b }

But in this case, each function needs to arrange the various digital types. We can use the type set to define all the digital types first.

For example, the optimized ratio size operation

 package main type Numeric interface { int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64 } func min [ T Numeric ]( a , b T ) T { if a < b { return a } return b } func max [ T Numeric ]( a , b T ) T { if a > b { return a } return b }

All the numeric types here are included in Numric , so that the size can be compared in a generic function, such a function with type parameters is called a generic function.

As mentioned earlier, generic functions need to specify their specific types when they are called, but some simple types will also be automatically deduced and can be omitted.

 func main () { maths . MinInt ( 1 , 2 ) fmt . Println ( min ( 45 , 74 )) fmt . Println ( min [ int ]( 45 , 74 )) fmt . Println ( min [ int32 ]( 45 , 74 )) fmt . Println ( max ( 4.141 , 2.01 )) fmt . Println ( max [ float64 ]( 4.141 , 2.01 )) // 编译报错:cannot use 4.141 (untyped float constant) as int64 value in argument to max[int64] (truncated) fmt . Println ( max [ int64 ]( 4.141 , 2.01 )) // IDE报错:Cannot use string as the type Numeric // 编译报错:string does not implement Numeric fmt . Println ( max [ string ]( 4.141 , 2.01 )) }

But notice two things

  1. Executable type, if the specified type is inconsistent with the actual incoming parameter type, there will be a compilation error
  2. If the type is not specified, the compiler will automatically identify the type. If the type is inconsistent, a compilation error will be reported.

Because Golang’s generics are determined at compile time according to the specified specific type, there are still strong type restrictions at runtime.

For general numeric types, constraints.Ordered is also built-in in Golang 1.18 to represent all built-in types that can be sorted, so the above generic function can be rewritten as this

 package main import ( "golang.org/x/exp/constraints" ) func minType [ T constraints . Ordered ]( a , b T ) T { if a < b { return a } return b } func maxType [ T constraints . Ordered ]( a , b T ) T { if a > b { return a } return b }

If you enter the source code to view its specific code, you will find that there are also various digital types combined in the Ordered type.

But there is a bit of strangeness in the type set here, a wavy line ~ is added before each type, indicating a derived type, even the type customized by type can be recognized, as long as the underlying type is the same.

For example, ~int can contain multiple types such as int and type MyInt int

 // Signed is a constraint that permits any signed integer type. // If future releases of Go add new predeclared signed integer types, // this constraint will be modified to include them. type Signed interface { ~ int | ~ int8 | ~ int16 | ~ int32 | ~ int64 } // Unsigned is a constraint that permits any unsigned integer type. // If future releases of Go add new predeclared unsigned integer types, // this constraint will be modified to include them. type Unsigned interface { ~ uint | ~ uint8 | ~ uint16 | ~ uint32 | ~ uint64 | ~ uintptr } // Integer is a constraint that permits any integer type. // If future releases of Go add new predeclared integer types, // this constraint will be modified to include them. type Integer interface { Signed | Unsigned } // Float is a constraint that permits any floating-point type. // If future releases of Go add new predeclared floating-point types, // this constraint will be modified to include them. type Float interface { ~ float32 | ~ float64 } // Complex is a constraint that permits any complex numeric type. // If future releases of Go add new predeclared complex numeric types, // this constraint will be modified to include them. type Complex interface { ~ complex64 | ~ complex128 } // Ordered is a constraint that permits any ordered type: any type // that supports the operators < <= >= >. // If future releases of Go add new ordered types, // this constraint will be modified to include them. type Ordered interface { Integer | Float | ~ string }

In addition to the Ordered type, it also provides a built-in interface type, comparable , where comparable means that == and != can be used to compare whether they are the same, not > and < for size comparison, but note that in the interface In a collection, the comparable interface and other types of unions cannot be used.

In the use of generics, it can be used in type definitions and interface definitions, as type constraints and type collections, and can also be used in channels (Channel).

But there are some times when it can’t be used, such as

  1. Generics cannot be defined individually,
  2. For example, anonymous structs cannot use generics,
  3. For example, anonymous functions cannot define their own type parameters.
  4. One is that you can only use generic methods in generic classes, and you cannot use generic methods in non-generic classes.

A type parameter can only be defined once, and a type parameter can only be passed in and confirmed once, and cannot be confirmed multiple times with a type parameter.

Reference link

A Comprehensive Explanation of Generics in Go 1.18: An Introduction to Generics

The role and definition of generics

Are generics a double-edged sword? Go1.18 compilation will be nearly 20% slower

Go 1.18 is officially released! Generics support, performance optimization…

Talking about generic programming in Go1.18

Golang Generics Analysis

Compile-time type checking of the advantages of Java generics

Why use generics?

Understand Java’s generics in seconds

Go language generic design

Practical exploration of functional programming under Go generics

Quick Start with Go Generics

Go Generics Concise Tutorial

Go Generics Preliminary

Go 1.18 New Features Study Notes 04: Basic Grammar of Go Generics

Do you know these 3 core designs of Go generics?

Introduction to Go Generics

Tutorial: Getting started with generics

An Introduction To Generics

This article is reprinted from: https://windard.com/blog/2022/05/17/Golang-Generic
This site is for inclusion only, and the copyright belongs to the original author.

Leave a Comment