To be honest, no matter what other Go fans think, for me, I’ve been holding out hope for the Go generics since the 1.18 proposal. Because generics are something you really want. With the official release of Go 1.18, generics have really arrived. I’ll then use this new feature to refactor the tool libraries currently on my team.

Since the library has a lot of utility functions, here are two generics-related functions to share the pain points that generics address:

// Check whether an element exists in a data group
array.In(elem string, arr []string) = >bool

// Array elements are de-duplicated
array.Unique(arr []string) = > []string
Copy the code

Array.in () is used to check whether an array of strings contains an element and returns a bool variable; Array.unique () is used to unduplicate elements of an array (or slice) of strings. General-purpose functions such as these are used frequently in team practice, but are not limited to the above string data, and may actually require Int, array of float64 types, etc., and other types.

Now, if you’re playing other languages, you might wonder, why do these basic functions in Go encapsulate themselves? Indeed, unlike other languages, such as PHP, Go has a lot of handy base functions built into it that you need to implement yourself.

Implementation before generics

These functions are very basic but very general, so we tend to design them as libraries. Before generics came along, we implemented them in one of two ways to satisfy all the needs of our developers:

In(elem interface{}, arr interface{}) to determine what type to pass.

// The element is defined as interface{}
func In(elem interface{}, arr interface{}) bool {
   switch val := elem.(type) { // Internal assertion judgment
   case string:
       tmpArr := arr.([]string)
      for _, item := range tmpArr {
         if val == item {
            return true}}case int:
      // ... 
   default:
      panic(' ')}return false
}
Copy the code

The above implementation works for In(), but Unique() is a bit more difficult because Uninue() has different types of returns, which we expect: When []string is passed in, the []string is returned. When []int is passed in, the corresponding []int is returned:

// Pass string array, return string array
array.Unique([]string{"A"."B"."B"}) = = = > []string{"A"."B"}

// Pass an integer array returns an integer array
array.Unique([]int{1.1.2}) = = = > []int{1.2}
Copy the code

If you also want to return interface{}, then the user needs a cast, which is very uncomfortable:

func Unique(interface{}) interface{} { // Return interface{} multipurpose type
    // ...
}
Copy the code

Users will need to turn:

val arrStr := array.Unique([]int{1.2.3.1([]}).int) // It is not graceful to turn too hard.
Copy the code

Method 2: Define several functions of the same type, and then use different methods to satisfy different types. As in the official Go generics example, the final code looks like this:

func UniqueInt(arr []int) []int{... }func UniqueInt8(arr []int8) []int8{... }func UniqueInt16(arr []int16) []int16{... }func UniqueInt32(arr []int32) []int32{... }func UniqueInt64(arr []int64) []int64{... }func UniqueString(arr []string) []string{... }// ...
Copy the code

The above code implementation is not very elegant anyway, so wait for generics…

With generics

The pain points above are resolved when generics come in, and again, we use generic refactoring.

First we define a new element type, Element (whatever it is called), and specify which types of data constraints it can dynamically support.

// Define a new type: element
type element interface {
   // Element supports the following types
   string | int8 | int16 | int32 | int64 | int | float32 | float64 | uint | uint8 | uint16 | uint32 | uint64
}
Copy the code

We then redesign the generic function with new type parameters:

func Unique[T element](arr []T) []T {}
Copy the code

The difference with ordinary functions is that the function name is followed by a parenthesis content, and the parameter and return value are unknown type T

The bracketed [T elemet] is the design implementation of the Go language’s generic type parameters, which place the constraint’s type parameters in brackets [] (unlike Java’s Angle brackets <>).

The above function we declare a type parameter T (T is also named casually, you can play any), and its type constraints for the element type, also known as (string | int. J.) Type. You can think of Element as an alias for the group of types above. The return value is also of type []T.

We define a Unique function whose input type is the type of the constraint element and whose return value is the same. The following is a concrete function body code implementation, in fact, the original concrete type is replaced by an uncertain type T, other and ordinary functions are no different:

// Unloadable array or slice edge
func Unique[T element](arr []T) []T {
   tmp := make(map[T]struct{})
   l := len(arr)
   if l == 0 {
      return arr
   }

   rel := make([]T, 0, l)
   for _, item := range arr {
      _, ok := tmp[item]
      if ok {
         continue
      }
      tmp[item] = struct{}{}
      rel = append(rel, item)
   }

   return rel[:len(tmp)]
}
Copy the code

test

func TestUnique(t *testing.T) {
   arrayInts := []int{1.2.2.3.4}
   arrayStrs := []string{"A"."B"."C"."D"."C"}
    
   // Test integers
   t.Log(array.Unique(arrayInts))
   
   // Test string
   t.Log(array.Unique(arrayStrs))
}
Copy the code

The results are as follows:

main_test.go:22: [1 2 3 4]
main_test.go:23: [A B C D]
Copy the code

On performance

Since generics are equivalent to a universal function, is there a performance difference between using a generic function and a generic function? The answer is: there is no difference, it can be used safely.

For testing purposes, a normal UniqueString function is encapsulated here for comparison:

func UniqueString(arr []string) []string {
   tmp := make(map[string]struct{})
   // ...

   rel := make([]string.0, l)
   for _, item := range arr {
     // ..
   }

   return rel[:len(tmp)]
}

func BenchmarkUniqueNotGeneric(b *testing.B) {
   arrayStrs := []string{"A"."B"."C"."D"."C"}
   for i := 0; i < b.N; i++ {
      _ = array.UniqueString(arrayStrs)
   }
}

func BenchmarkUniqueGeneric(b *testing.B) {
   arrayStrs := []string{"A"."B"."C"."D"."C"}
   for i := 0; i < b.N; i++ {
      _ = array.Unique(arrayStrs)
   }
}

Copy the code

Normal functions are designed exactly like generic parameters, with support for replacing the type with a normal string type. The results of the benchmark test show that the average execution time is around 116 nanoseconds, not much different.

go test -bench=. goos: darwin goarch: arm64 pkg: Generics_type BenchmarkUniqueNotGeneric - 10 8855962 116.7 ns/op BenchmarkUniqueGeneric - 10 10200286 115.6 ns/op PASS ok Generics_type 4.008 sCopy the code

other

Since generics didn’t exist until Go 1.18, when developing with the Go Module, your go.mod file should be no lower than 1.18, meaning that once you use generics, you don’t want to compile code that is lower than 1.18.

module generics_type

go 1.18

require (...)
Copy the code

Besides, at present, idea + Go plug-in and Goland support generic development, so the development is relatively smooth, and we will do more project practice based on generics in the future.