golang

Let’s take a look at the three new Packages introduced in go1.18: constraints, slices and maps. Currently, these three packages are unified in golang.org/x/exp. The code can be found here.

New any and comparable

Go1.18 adds two syntax types, any and comparable, where any can be compared to the original interface, and developers can replace the original interface writing according to the context. Let’s take a look at the following example.

1
2
3
4
func splitStringSlice(s []string) ([]string, []string) {
  mid := len(s) / 2
  return s[:mid], s[mid:]
}

If it is int64, another func will be written.

1
2
3
4
func splitInt64Slice(s []int64) ([]int64, []int64) {
  mid := len(s) / 2
  return s[:mid], s[mid:]
}

The any syntax can be used instead of the above in Go1.18.

1
2
3
4
func splitAnySlice[T any](s []T) ([]T, []T) {
  mid := len(s) / 2
  return s[:mid], s[mid:]
}

At this point you will find that if you want to use == or ! =, use comparable instead, and see the example below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func indexOf[T comparable](s []T, x T) (int, error) {
  for i, v := range s {
    // v and x are type T, which has the comparable
    // constraint, so we can use == here.
    if v == x {
      return i, nil
    }
  }
  return 0, errors.New("not found")
}

Change comparable above to any and you will find a compiler error.

1
cannot compare v == x (T is not comparable)

constraints

Go1.18 has a new constraints package. If you open the code and look at it, you will see that it provides a lot of easy generics interface writing, such as Integer interface as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 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
}

In this way, we can find out where the slice integer exists by writing the following method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func indexOfInteger[T constraints.Integer](s []T, x T) (int, error) {
  for i, v := range s {
    // v and x are type T, which has the comparable
    // constraint, so we can use == here.
    if v == x {
      return i, nil
    }
  }
  return 0, errors.New("not found")
}

We do not need to declare additional custom interfaces, but of course if it is a floating point Float is also available.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func indexOfFloat[T constraints.Float](s []T, x T) (int, error) {
  for i, v := range s {
    // v and x are type T, which has the comparable
    // constraint, so we can use == here.
    if v == x {
      return i, nil
    }
  }
  return 0, errors.New("not found")
}

constraints.Ordered can be used to add up all the numbers, whether they are floating point or integers.

1
2
3
4
5
6
7
func sum[T constraints.Ordered](s []T) T {
  var total T
  for _, v := range s {
    total += v
  }
  return total
}

slices

Developers used to have to write a bunch of useful func’s like BinarySearch, Compare or Contains and many other Slice functions, but now Go has them built in directly and developers can just take them and use them. Like we mentioned above.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
  "errors"
  "fmt"

  "golang.org/x/exp/slices"
)

func indexOf[T comparable](s []T, x T) (int, error) {
  for i, v := range s {
    // v and x are type T, which has the comparable
    // constraint, so we can use == here.
    if v == x {
      return i, nil
    }
  }
  return 0, errors.New("not found")
}

func main() {
  i, err := indexOf([]string{"apple", "banana", "pear"}, "banana")
  fmt.Println(i, err)
  i, err = indexOf([]int{1, 2, 3}, 3)
  fmt.Println(i, err)

  fmt.Println(slices.Index([]string{"apple", "banana", "pear"}, "banana"))
}

To compare the way the main func is written, open the source code and see how it is written as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Index returns the index of the first occurrence of v in s,
// or -1 if not present.
func Index[E comparable](s []E, v E) int {
  for i, vs := range s {
    if v == vs {
      return i
    }
  }
  return -1
}

So the official is also for all developers to write a bunch of common Slice operation syntax, I believe we are very common to use. Here is another official Binary Search example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// BinarySearch searches for target in a sorted slice and returns the smallest
// index at which target is found. If the target is not found, the index at
// which it could be inserted into the slice is returned; therefore, if the
// intention is to find target itself a separate check for equality with the
// element at the returned index is required.
func BinarySearch[Elem constraints.Ordered](x []Elem, target Elem) int {
  return search(len(x), func(i int) bool { return x[i] >= target })
}

func search(n int, f func(int) bool) int {
  // Define f(-1) == false and f(n) == true.
  // Invariant: f(i-1) == false, f(j) == true.
  i, j := 0, n
  for i < j {
    h := int(uint(i+j) >> 1) // avoid overflow when computing h
    // i ≤ h < j
    if !f(h) {
      i = h + 1 // preserves f(i-1) == false
    } else {
      j = h // preserves f(j) == true
    }
  }
  // i == j, f(i-1) == false, and f(j) (= f(i)) == true  =>  answer is i.
  return i
}

maps

In addition to the common slice, the map syntax is also very common, and the official functions such as Copy, Clone, Keys, Values or Equal are also available. Please see below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
  "fmt"

  "golang.org/x/exp/maps"
)

var (
  m1 = map[int]int{1: 2, 2: 4, 4: 8, 8: 16}
  m2 = map[int]string{1: "2", 2: "4", 4: "8", 8: "16"}
)

func main() {
  fmt.Println(maps.Keys(m1))
  fmt.Println(maps.Keys(m2))

  fmt.Println(maps.Values(m1))
  fmt.Println(maps.Values(m2))

  fmt.Println(maps.Equal(m1, map[int]int{1: 2, 2: 4, 4: 8, 8: 16}))

  maps.Clear(m1)
  fmt.Println(m1)
  m3 := maps.Clone(m2)
  fmt.Println(m3)
}

Limits of generics

Not all interface{} can be replaced, and there are still specific cases where generics cannot be used. see the example first, write a conversion of all type to string, as it was written before go1.18.

1
2
3
4
5
6
7
// ToString convert any type to string
func ToString(value interface{}) string {
  if v, ok := value.(*string); ok {
    return *v
  }
  return fmt.Sprintf("%v", value)
}

The conversion to go1.18 is written as follows.

1
2
3
func toString[T constraints.Ordered](value T) string {
  return fmt.Sprintf("%v", value)
}

The above example is fine, but when converted to ToBool, there will be problems.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// ToBool convert any type to boolean
func ToBool(value interface{}) bool {
  switch value := value.(type) {
  case bool:
    return value
  case int:
    if value != 0 {
      return true
    }
    return false
  }
  return false
}

To rewrite to go1.18 writing will be wrong.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func toBool[T constraints.Ordered](value T) bool {
  switch value := value.(type) {
  case bool:
    return value
  case int:
    if value != 0 {
      return true
    }
    return false
  }
  return false
}

The error message is as follows:

1
cannot use type switch on type parameter value value (variable of type T constrained by constraints.Ordered)

So generics is not a panacea, and it depends on the context in which it is used.

Generics vs Interfaces vs code generation

Interfaces in the Go language allows developers to design the same API for different types, and any type can write a very beautiful abstraction layer by implementing the same methods, but we will find that there are only a few differences in the methods implemented in different types, and the logic is the same, resulting in a lot of duplicate code. This results in a lot of repetitive code.

To solve this problem, many developers have written code generation tools through go generate, which is built into the Go language, to generate code and reduce the duplication of code by hand. Generics was created to solve this problem and really implement DRY.