こちらはzennの記事を移行したものです

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

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

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

:::message これはベータコンテンツなので変更があるかもしれません。 :::

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/