Go 1.18 generics 新套件 constraints, slices 及 maps

logo

今天看到 Go1.18 終於推出 RC1 版本了,離正式 Release 又跨出一大步了。繼上一篇『初探 golang 1.18 generics 功能』教學後,本次來看看 go1.18 推出三個新的 Package: constraints, slicesmaps 使用方式。目前這三個 Package 會統一放在 golang.org/x/exp 內。本篇程式碼都可以在這邊找到

影片教學

影片視頻會同步放到底下課程內

新增 any 及 comparable

Go1.18 新增 anycomparable 兩種語法型態,其中 any 可以對比原本的 interface,開發者可以根據情境來取代原本 interface 寫法,底下來看看例子

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

如果是 int64,又會另外寫一個 func

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

在 Go1.18 可以透過 any 語法取代上述寫法

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

這時候你會發現,如果想在邏輯運算內使用 ==!=,請改用 comparable,直接看底下範例

 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")
}

把上述的 comparable 改成 any,你會發現出現 compiler 錯誤。

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

constraints 套件

Go1.18 會新增 constraints package,打開代碼來看,你會看到提供蠻多簡易的 generics interface 寫法,像是 Integer interface 如下

 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
}

這樣我們要找出 slice 整數裡面存在的位置,可以透過底下寫法

 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")
}

我們不用額外在宣告自定義的 interface,當然如果是浮點數 Float 也是有的

 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 方式

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 套件

開發者以前自己要寫一堆好用的 func 像是 BinarySearch, Compare 或 Contains 等眾多的 Slice 函式,現在 Go 官方直接內建,開發者直接拿去使用即可。像是上面我們提到的

 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"))
}

比較一下在 main func 內寫法,打開原始碼可以看到寫法如下:

 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
}

所以官方也是替所有開發者寫好一堆常用的 Slice 操作語法,相信大家很常用到。這邊再看一個官方 Binary Search 範例

 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 套件

除了常用 slice 之外,map 語法也是大家很常見的,官方也是提供 Copy, Clone, Keys, Values 或 Equal 等好用函式,請參考底下

 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)
}

generics 限制

不是所有的 interface{} 都可以取代,還是有特定的狀況無法使用 generics,先看範例,寫一個轉換全部 typestring,在 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)
}

換成 go1.18 寫法如下

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

上面例子沒問題,但是換成轉換 ToBool 就會出問題

 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
}

要改寫成 go1.18 寫法就會出錯

 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
}

錯誤訊息如下

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

所以 generics 不是萬能,還是要看看使用的情境。

Generics vs Interfaces vs code generation

Interfaces 在 Go 語言內讓開發者針對不同型態設計相同的 API,任何型態只要去實現相同的 methods,就可以寫出非常漂亮的 abstraction layer,但是大家會發現在不同的型態所實現的 methods 只有少數差異,而邏輯上面都是相同的,造成很多重複性的代碼。

為了解決這問題,很多開發者透過 Go 語言內建的 go generate 撰寫了 code generation 工具,讓代碼產生代碼,進而減少手動撰寫重複性代碼。而 Generics 的出現就是要解決這問題,真正實現 DRY


See also