7

Go 1.18 generics 新套件 constraints, slices 及 maps

 2 years ago
source link: https://blog.wu-boy.com/2022/02/golang-1-18-generics-constraints-slices-maps/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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 寫法,底下來看看例子

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

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

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

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

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

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

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 如下

// 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 整數裡面存在的位置,可以透過底下寫法

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 也是有的

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

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 官方直接內建,開發者直接拿去使用即可。像是上面我們提到的

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 內寫法,打開原始碼可以看到寫法如下:

// 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 範例

// 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 等好用函式,請參考底下

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 之前會這樣寫

// 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 寫法如下

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

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

// 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 寫法就會出錯

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK