【Go】genericsのチュートリアルがあったので試した話

2021/12/23に公開

この記事は Go Advent Calendar 2021 (カレンダー4)23日目の記事です。

Go1.18 beta1がリリースされてgenericsが使えるようになりました!
https://go.dev/blog/go1.18beta1

公式ドキュメントを眺めてたらこんなチュートリアルがあったので試してみました。
https://go.dev/doc/tutorial/generics

Go1.18 beta1 のインストール

チュートリアルにもありますが、以下のように導入します。
goenv を使わずにの切り替えができるので便利です。

$ go install golang.org/dl/go1.18beta1@latest
$ go1.18beta1 download

ダウンロードまでできたらバージョンを確認します。
以下のように表示された成功です。
go1.18beta1と入力するのめんどくさいのでaliasを作っておくのがおすすめです。

$ go1.18beta1 version
go version go1.18beta1 darwin/arm64

// option
$ alias go18="go1.18beta1"

今回は1.18betaを入れましたが、上記の方法を使うと
他のバージョンを入れることができます。

例えば、go1.14.1を入れたい場合このようにします。

$ go install golang.org/dl/go1.14.1@latest
$ go1.14.1 dowload

関数を追加

今回はマップの値を合計して返す関数を作ります。
まずはじめに、ジェネリクスを使わない関数を追加してみます。

package main

import "fmt"

func sumInts(m map[string]int64) int64 {
  var s int64
  for _, v := range m{
    s += v
  }
  return s
}

func sumFloats(m map[string]float64) float64{
  var s float64
  for _, v := range m{
    s += v
  }
  return s
} 

func main(){
  ints := map[string]int64{
    "first": 12,
    "second": 34,
  }
  floats := map[string]float64{
    "first": 12.34,
    "second": 56.78,
  }
  fmt.Printf("SumInt: %v\n",sumInts(ints))
  fmt.Printf("SumFloat: %v\n",sumFloats(floats))
}

実行するのとこのようになります。

$ go18 run main.go
SumInt: 46
SumFloat: 69.12

ジェネリクスを追加

先程作ったsumIntとsumFloatを単一の関数にまとめます。

package main

import "fmt"

func sumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
  var s V
  for _, v := range m{
    s += v
  }
  return s
}

func main(){
  ints := map[string]int64{
    "first": 12,
    "second": 34,
  }
  floats := map[string]float64{
    "first": 12.34,
    "second": 56.78,
  }
  fmt.Printf("sumInt: %v, sumFloat: %v\n",
      sumIntsOrFloats[string, int64](ints),
      sumIntsOrFloats[string, float64](floats))
}

注目していただきたいのは以下の部分です。

func sumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
  var s V
  for _, v := range m{
    s += v
  }
  return s
}

[]内でK,Vの型パラメータを宣言します。引数では型map[K]V。戻り値はVです。
Vでint64とfloat64の2つの型であることを指定します。
つまり、どちらの型も許可するようになります。

fmt.Printf("sumInt: %v, sumFloat: %v\n",
    sumIntsOrFloats[string, int64](ints),
    sumIntsOrFloats[string, float64](floats))

最後に宣言したgenerics関数を呼び出し、作成したマップを渡します。
上記でも記載しているように、呼び出している関数の型パラメータを置き換える必要がある型に明示的に指定することで呼び出しが可能です。

呼び出し時に型引数を削除

先程書いた呼び出しを以下のようにすることもできます。
ただこれが常に可能ではないそうです。(例えば引数のないジェネリクスを呼び出すとき)

fmt.Printf("sumInt: %v, sumFloat: %v\n",
    sumIntsOrFloats(ints),
    sumIntsOrFloats(floats))

型制約宣言

最後に型制約を宣言します。
interface型にint64とfloat64を宣言します。
そして先程記述した、sumIntsOrFloatsのVパラメータをNumberタイプ制約で書くことができます。

type Number interface {
  int64 | float64
}

func sumIntsOrFloats[K comparable, V Number](m map[K]V) V {
  var s V
  for _, v := range m{
    s += v
  }
  return s
}

最後に

今回はgenericsのチュートリアルをやってみました。
やってみた感じ、大変シンプルでわかりやすかったです。
単一の関数にまとめられるので、開発スピードも比較的によくなるかなと感じました。TypeScriptのようにany型なんてあると便利な感じもしますが、type any = interface{}とかで作れそうなので、今後が気になります。

余談ですが、他にもちゅーとりあるがあるので時間があるときやってみたいです。
https://go.dev/doc/tutorial/

Discussion