Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

How to use generics of Go1.18 's new features

2025-01-17 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)05/31 Report--

This article focuses on "how to use generics of Go1.18 's new features". Interested friends may wish to have a look at it. The method introduced in this paper is simple, fast and practical. Let's let the editor learn how to use generics of Go1.18 's new features.

What are generics in 01 Go

As we all know, Go is a statically typed language. Static type means that when programming in GE language, all variables and function parameters need to specify a specific type, and the compiler will also verify the specified data type during the compilation phase. This also means that the input and return parameters of a function must be strongly related to a specific type and cannot be reused by different types of data structures.

Generics are created to solve the problems of code reuse and type safety checking during compilation. Here is the definition of generics that I understand:

Generics are a way of programming in static languages. This programming method can make the algorithm no longer rely on a specific data type, but by parameterizing the data type to achieve the purpose of algorithm reusability.

Next, let's try it through the traditional way of writing a function and the way of writing generics.

1.1 the traditional way of writing functions

For example, if we have a function, Max, which calculates the largest element in an integer slice, it is traditionally written as follows:

Func Max (s [] int) int {if len (s) = = 0 {return 0} max: = s [0] for _ V: = range s [1:] {if v > max {max = v}} return max} M1: = Max ([] int {4,-8,15})

In this example, the input parameters and return value types of the Max function have been specified as int types, and other types of slices (such as s [] float) cannot be used. If you want to get the largest element in a slice of type float, you need to write another function:

Func MaxFloat (s [] float) float {/ /...}

The disadvantage of the traditional way of writing is that you need to write a function for each type, except for the different types in the parameters of the function, the other logic is exactly the same.

Next, let's take a look at the use of generics.

1.2 how generic functions are written

In order to make the program more reusable, universal programming (Generic programming) also came into being. With generics, a function or type can be defined based on type parameters and instantiated dynamically by specifying a specific type when the function is called, so that the function or type can be used based on a set of defined types. We rewrite the above Max function through generics:

Import ("fmt"golang.org/x/exp/constraints") func main () {M1: = Max [int] ([] int {4,-8,15}) m2: = Max [float64] ([] float64 {4.1,- 8.1,15.1}) fmt.Println (M1) M2)} / / define the generic function func Max [T constraints.Ordered] (s [] T) T {var zero T if len (s) = 0 {return zero} var max T max = s [0] for _ V: = range s [1:] {max = v if v > max {max = v}} return max}

As you can see from the above example, we rewrite the MaxNumber function by using generics, and when we call MaxNumber in the main function, we can reuse the MaxNumber code by passing in a specific type.

Well, here we just have a preliminary study of generics. As for the keywords such as T and any in generic functions, we will explain them in detail later.

Let's start before generics are added in order to better understand the motivation for generics to be added.

02 start before generics are added

To better understand why generics are needed, let's look at how reusable algorithms can be implemented without generics. Take the above function that returns the maximum value of the element in the slice as an example.

In order to reuse different data types in slices, we generally have the following solutions:

Write a repetitive set of code for each type

Pass an empty interface interface {}, using type assertions to determine which data type

Pass an empty interface interface {} and use the reflection mechanism to determine which data type it is

Customize the interface type and implement the specific logic by type inheritance

Let's take a look at the shortcomings of each of the above implementations.

2.1 write a repetitive set of code for each type

We have already implemented this approach in the first section. Implement a function for int slices and float slices respectively, but only the data types of slices are different in the two functions, and other logic is the same.

The main drawback of this approach is that there is a lot of repetitive code. The two functions are the same except for the data type of the slice element. At the same time, a large number of repetitive code also reduces the maintainability of the code.

2.2 use null interfaces and determine specific types through type assertions

Another way is for the function to receive the parameters of an empty interface. Use type assertions and switch statements inside the function to choose which specific type. Finally, the result is then packaged into an empty interface and returned. As follows:

Func Max (s [] interface {}) (interface {}, error) {if len (s) = = 0 {return nil, errors.New ("no values given")} switch first: = s [0]. (type) {case int: max: = first for _ RawV: = range s [1:] {v: = rawV. (int) if v > max {max = v}} return max, nil case float64: max: = first for _ RawV: = range s [1:] {v: = rawV. (float64) if v > max {max = v}} return max, nil default: return nil, fmt.Errorf ("unsupported element type of given slice:% T", first)}} / / Usagem1 Err1: = Max ([] interface {} {4,-8,15}) m2, err2: = Max ([] interface {} {4.1,-8.1,15.1})

This method of writing has two main shortcomings. The first disadvantage is the lack of type safety checks during compilation. If the caller passes an unsupported data type, the implementation of the function should return an error. The second disadvantage is that the usability of this implementation is not very good. Because either the caller handles the return value or the implementation code inside the function needs to wrap the specific type in an empty interface and use type assertions to determine the specific type in the interface.

2.3 pass an empty interface and use reflection to resolve specific types

When parsing a specific type from an empty interface, we can also replace type assertions through reflection. The implementation is as follows:

Func Max (s [] interface {}) (interface {}, error) {if len (s) = = 0 {return nil, errors.New ("no values given")} first: = reflect.ValueOf (s [0]) if first.Type (). Name () = "int" {max: = first.Int () for _ IfV: = range s [1:] {v: = reflect.ValueOf (ifV) if v.Type (). Name () = "int" {intV: = v.Int () if intV > max {max = intV} return max Nil} if first.Type () .Name () = "float64" {max: = first.Float () for _ IfV: = range s [1:] {v: = reflect.ValueOf (ifV) if v.Type (). Name () = "float64" {intV: = v.Float () if intV > max {max = intV} return max, nil} return nil Fmt.Errorf ("unsupported element type of given slice:% T", s [0])} / / Usagem1, err1: = Max ([] interface {} {4,-8,15}) m2, err2: = Max ([] interface {} {4.1,-8.1,15.1})

In this method, there is not only no type security check during compilation, but also poor readability. And when using reflection, performance is usually poor.

2.4 implemented through custom interface types

Another approach can be achieved by passing a specific, predefined interface to the function. The interface should contain the necessary methods for the function to be implemented by the function. This method can be supported as long as the type of the interface is implemented. Let's take the MaxNumber function above as an example, there should be a method to get the number of elements Len, a method to compare the size of the method Less, and a method to get elements Elem. Let's take a look at the specific implementation:

Type ComparableSlice interface {/ / returns the number of elements in the slice. Len () int / / compare whether the element value of index I is smaller than that of index j Less (I, j int) bool / / returns the element Elem (I int) interface {}} func Max (s ComparableSlice) (interface {}, error) {if s.Len () = = 0 {return nil Errors.New ("no values given")} max: = s.Elem (0) for I: = 1 I

< s.Len(); i++ { if s.Less(i-1, i) { max = s.Elem(i) } } return max, nil}type ComparableIntSlice []intfunc (s ComparableIntSlice) Len() int { return len(s) }func (s ComparableIntSlice) Less(i, j int) bool { return s[i] < s[j] }func (s ComparableIntSlice) Elem(i int) interface{} { return s[i] }type ComparableFloat64Slice []float64func (s ComparableFloat64Slice) Len() int { return len(s) }func (s ComparableFloat64Slice) Less(i, j int) bool { return s[i] < s[j] }func (s ComparableFloat64Slice) Elem(i int) interface{} {return s[i]}// Usagem1, err1 := Max(ComparableIntSlice([]int{4, -8, 15}))m2, err2 := Max(ComparableFloat64Slice([]float64{4.1, -8.1, 15.1})) 在该实现中,我们定义了一个ComparableSlice接口,其中ComparableIntSlice和ComparableFloat64Slice两个具体的类型都实现了该接口,分别对应int类型切片和float64类型切片。 该实现的一个明显的缺点是难以使用。因为调用者必须将数据封装到一个自定义的类型中(在该示例中是ComparableIntSlice和ComparableFloat64Slice),并且该自定义类型要实现已定义的接口ComparableSlice。 由以上示例可知,在有泛型功能之前,要想在Go中实现处理多种类型的可复用的函数,都会带来一些问题。而泛型机制正是避免上述各种问题的解决方法。 03 深入理解泛型--泛型使用"三步曲" 在文章第一节处我们已经提到过泛型要解决的问题--程序针对一组类型可进行复用。下面我们给出泛型函数的一般形式,如下图: 由上图的泛型函数的一般定义形式可知,使用泛型可以分三步,我将其称之为"泛型使用三步曲"。 3.1 第一步:类型参数化 在定义泛型函数时,使用中括号给出类型参数类型,并在函数所接收的参数中使用该类型参数,而非具体类型,就是所谓的类型参数化。还是以上面的泛型函数为例: func Max[T constraints.Ordered](s []T) T { var zero T if len(s) == 0 { return zero } var max T max = s[0] for _, v := range s[1:] { max = v if v >

Max {max = v}} return max}

Where T is called a type parameter, that is, it is no longer a specific type value, but needs to be dynamically passed in a type value (such as int,float64) when the function is called to instantiate T. For example: Max [int] (s [] int {4 int 8je 15}), then T stands for Max.

Of course, there can be multiple type parameters in the list of type parameters, separated by commas. Type parameter names do not have to be T, and any name that conforms to the rules of variables is fine.

3.2 step 2: add constraints to the type

In the figure above, any is called a type constraint, which is used to describe what conditions the type value passed to T should meet. Types that do not meet the constraint will be reported a compilation error when they are passed to T, thus implementing the type security mechanism. Of course, type constraints are not as simple as any.

There are two types of type constraints in Go, which are built-in type constraints (including built-in type constraints any, comparable and type constraints defined in the golang.org/x/exp/constraints package) and custom type constraints that are officially supported by Go. Because generic constraints in Go are implemented through interfaces, we can customize type constraints by defining interfaces.

3.2.1 built-in type constraints officially supported by Go

Among them, the built-in type constraints of Go and the type constraints defined by constraints package are unified into the type constraints officially defined by Go. It is in the golang.org/x/exp/constraints package because the constraint is experimental.

Here's a list of the predefined type constraints officially supported by Go:

Constraint description location any any type Can be seen as the alias of the empty interface interface {} go built-in comparable comparable value type That is, values of this type can be compared using the = = and! = operators (for example, bool, numeric type, string, pointer, channel, interface, array where values are comparable types, All fields are comparable structures, etc.) go built-in Signed-signed integer ~ int | ~ int8 | ~ int16 | ~ int32 | ~ int64golang.org/x/exp/constraintsUnsigned-signed integer ~ uint | ~ uint8 | ~ uint16 | ~ uint32 | ~ uint64 | ~ uintptrgolang.org/x/exp/constraintsInteger-integer Signed | Unsignedgolang.org/x/exp/constraintsFloat-floating point ~ float32 | ~ float64golang.org/x/exp/constraintsComplex-plural ~ complex64 | ~ complex128golang.org/x/exp/constraintsOrderedInteger | Float | ~ string (any type of operator supported) golang.org/x/exp/constraints

In the above table, we see the symbol ~. ~ T means that the underlying type is the type of T. For example, ~ int represents a type whose underlying type is int. We have a detailed introduction and examples of this in the next section, Custom Type constraints.

3.2.2 Custom type constraints

As you can see from the above, the constraint of a type is essentially an interface. Therefore, if the officially provided type constraints do not meet your business scenarios, you can customize the type constraints according to the syntax rules of generics in Go. Type constraints are generally defined in two forms:

Define in the form of interface

Directly defined in the type parameter list

Let's take a look at their respective methods of use.

Define in the form of interface

The following is an example of a type constraint defined as an interface:

/ / Custom type constraint API StringableFloattype StringableFloat interface {~ float32 | ~ float64 / / A type whose underlying layer is float32 or float64 can satisfy the constraint String () string} / / MyFloat is a float type that satisfies the StringableFloat type constraint. Type MyFloat float64 / / implements the String method func (m MyFloat) String () string {return fmt.Sprintf ("% e", m)} / / generic function in the type constraint, using the StringableFloat constraint func StringifyFloat [T StringableFloat] (f T) string {return f.String ()} / / Usagevar f MyFloat = 48151623.42 / instantiate T using MyFloat type s: = StringifyFloat [MyFloat] (f)

In this example, the function StringifyFloat is a generic function and uses the StringableFloat interface to constrain T. The MyFloat type is a concrete type that satisfies StringableFloat constraints.

In generics, type constraints are defined as interfaces that can contain collections and methods of a specific type. In this example, the StringfyFloat type constraint contains two types, float32 and float64, as well as a String () method. This constraint allows any specific type that satisfies the interface to instantiate the parameter T.

In the above example, we also see a new key symbol: ~. ~ T represents that the underlying type of all types must be type T. Here the type MyFloat is a custom type, but its underlying type, or base type, is float64. Therefore, MyFloat satisfies the StringifyFloat constraint.

In addition, type parameters can also be introduced in defining type constraint interfaces. In the following example, the slice type in the type constraint SliceConstraints introduces the type parameter E so that the constraint can constrain any type of slice.

Package mainimport ("fmt"golang.org/x/exp/constraints") func main () {R1: = FirstElem1 [[] string, string] ([] string {"Go", "rocks"}) R2: = FirstElem1 [[] int, int] ([] int {1,2}) fmt.Println (R1, R2)} / / define type constraints The type parameter Etype SliceConstraint [E any] interface {~ [] E} / / generic function func FirstElem1 [S SliceConstraint [E], E any] (s S) E {return s [0]} is introduced.

Define constraints directly in the type parameter list

In the following example, the FirstElem2 and FirstElem3 generic functions define the type constraint directly in the type parameter list, which I call the anonymous type constraint interface, similar to anonymous functions. In the following sample code, three generic functions are equivalent:

Package mainimport ("fmt"golang.org/x/exp/constraints") func main () {s: = [] string {"Go", "rocks"} R1: = FirstElem1 [[] string, string] (s) R2: = FirstElem2 [[] string, string] (s) R3: = FirstElem3 [[] string, string] (s) fmt.Println (R1, R2, R3)} type SliceConstraint [E any] interface {~ [] E} func FirstElem1 [S SliceConstraint [E] E any] (s S) E {return s [0]} func FirstElem2 [S interface {[] E}, E any] (s S) E {return s [0]} func FirstElem3 [S [] E, E any] (s S) E {return s [0]} 3.3 third step: instantiation of type parameters

When calling a generic function, you need to specify a specific type for the type parameter of the function, which is called type instantiation. In the process of type instantiation, sometimes there is no need to specify a specific type, when in the compilation phase, the compiler will automatically derive the actual parameter value of T according to the parameters of the function. As follows:

Type parameter instantiation is relatively simple, that is, when calling a generic function, you pass a specific type to the type parameter of the generic function. As specified in the first step when calling the Max function: R2: = Max [int] ([] int {4,8,15}), where the int in parentheses after the Max is the type argument, so the Max function can know the specific type of the slice element being processed.

It is also important to note that when type parameters are instantiated, there is a way that you do not need to specify a specific type. At this time, during the compilation phase, the compiler will automatically derive the actual parameter value of T from the parameters of the function: R3: = Max ([] float64 {4.1,8.1,15.1}). The square brackets and the corresponding specific types are not given after the Max, but the Go compiler can automatically infer that it is the float64 type based on the slice element type.

04 differences between generic type constraints and normal interfaces

First of all, both are interfaces, and both can define methods. However, specific types can be defined in the type constraint API, such as the type constraint in the custom StringableFloat type constraint API defined above: ~ float32 | ~ float64

Type StringableFloat interface {~ float32 | ~ float64 / / the type whose underlying layer is float32 or float64 can satisfy the constraint String () string}

When there are type constraints in an interface, the interface can only be used for constraints on generic type parameters.

At this point, I believe you have a deeper understanding of "how to use generics of Go1.18 's new features". You might as well do it in practice. Here is the website, more related content can enter the relevant channels to inquire, follow us, continue to learn!

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report