カテゴリー: BackEnd

Go 1.18で追加されるGenericsの紹介

はじめに

まもなくリリース予定(2022年2月)のGo 1.18では、待望?のGenericsが導入されます。そこで、現在利用できるGo 1.18beta1で、Genericsの使い方を紹介します。

GoのGenerics

まず始めに、現時点でのGenericsの仕様や制限について紹介します。

Genericsの主な仕様

Genericsの主な仕様は以下のとおりです。

  • 関数や型宣言の構文に型パラメータが使えるようになった
  • パラメータ化された関数や型は、その後に[]で型引数のリストを記述することでインスタンス化することができる
  • インタフェース型の構文では、任意の型、union、T型要素を埋め込む事ができるようになった
  • any識別子(空のインタフェースのエイリアス)が追加された
  • comparable識別子(==や!=を使って比較できる全ての型の集合を表すインタフェース)が追加された

より詳しい言語仕様はproposalをご覧ください。

Genericsの制限

現在のGenericsの実装には以下の制限があります。

  • GoのコンパイラはGenerics関数やメソッドの内部での型宣言ができない(Go 1.19でサポートされる予定)
  • Goのコンパイラは予め宣言されたreal関数、imag関数、complex関数でのパラメータ型の引数を受け入れない(Go 1.19でサポートされる予定)
  • 型パラメータや型パラメータへのポインタを構造体の無名フィールドとして埋め込む事はできない。また、インタフェース型に型パラメータを埋め込む事もできない
  • 2つ以上の項を持つunion要素は、空でないメソッドセットをもつインタフェース型を含むことができない

Genericsを使ってみる

基本形

では、実際にGenericsを使ったメソッドを書いてみます。比較のために、まずはGenericsを使わない場合のメソッドを定義します。

package main

import "fmt"

func main() {
 intFruits := map[string]int64{
  "apple":  10,
  "orange": 8,
 }

 floatFruits := map[string]float64{
  "first":  11.0,
  "second": 8.8,
 }

 fmt.Printf("Sums: %v and %v\n",
  SumIntFruits(intFruits),
  SumFloatFruits(floatFruits))
}

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

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

int64型のマップとfloat64型のマップの合計値を返す2つの関数が定義されています。Genericsを使うと、これら2つの関数をまとめる事ができます。

package main

import "fmt"

func main() {
 intFruits := map[string]int64{
  "apple":  10,
  "orange": 8,
 }

 floatFruits := map[string]float64{
  "first":  11.0,
  "second": 8.8,
 }

 fmt.Printf("Sums: %v and %v\n",
  SumFruits[string, int64](intFruits),
  SumFruits[string, float64](floatFruits))
}

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

SumIntFruits関数SumFloatFruits関数のをまとめて、SumFruits関数を定義しました。この関数では、comparableを満たす型のキーを持ち、int64型もしくはfloat64型の値を持つマップを引数として受けとることができます。

型パラメータ(SumFruits関数のKやV)の制約は通常、型のセット(int64 | float64のように)を表しますが、コンパイル時には単一の型(呼び出し元のコードで渡している値の型)を表します。

なお、型パラメータとして許容していない、int32型の値を持つマップを引数として渡した場合、コンパイルエラーになります。

cannot use intFruits (variable of type map[string]int32) as type map[string]int64 in argument to SumFruits[string, int64]

注意点として、型パラメータはGenericsコードがそのパラメータに対して実行する全ての操作をサポートしなければなりません。例えば、型パラメータに数値型が含まれているGenerics関数で文字列操作を実行しようとすると、コードはコンパイルされません。

呼び出しの際の型引数を省略する

多くの場合、呼び出し元のコードから型を推論することができるため、呼び出し時に型引数を省略することができます。先程のコードを修正すると、以下のようになります。

package main

import "fmt"

func main() {
 intFruits := map[string]int64{
  "apple":  10,
  "orange": 8,
 }

 floatFruits := map[string]float64{
  "first":  11.0,
  "second": 8.8,
 }

 fmt.Printf("Sums: %v and %v\n",
  SumFruits(intFruits),
  SumFruits(floatFruits))
}

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

このように、SumFruits関数の呼び出し時に型引数を省略することができます。

型制約をインタフェースとして定義する

先程のコードでは、V int64 | float64のように冗長な記述になっています。そこで、これらを集約した型を定義することで、様々なところで利用できるようになるので、制約がより複雑になった場合などに、コードの効率化に役立ちます。

修正したコードは以下の通りです。

package main

import "fmt"

type Number interface {
 int64 | float64
}

func main() {
 intFruits := map[string]int64{
  "apple":  10,
  "orange": 8,
 }

 floatFruits := map[string]float64{
  "first":  11.0,
  "second": 8.8,
 }

 fmt.Printf("Sums: %v and %v\n",
  SumFruits(intFruits),
  SumFruits(floatFruits))
}

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

int64float64unionNumberと定義することで、よりコードがシンプルになります。

さいごに

Go 1.18で導入されるGenericsについて紹介しました。Generics自体は何年も前から議論されてしましたが、ついに導入されることとなり、実アプリケーションでGenericsの力を実感するのが楽しみです。

おすすめ書籍

Hiroki Ono

シェア
執筆者:
Hiroki Ono

最近の投稿

フロントエンドで動画デコレーション&レンダリング

はじめに 今回は、以下のように…

3週間 前

Goのクエリビルダー goqu を使ってみる

はじめに 最近携わっているとあ…

1か月 前

【Xcode15】プライバシーマニフェスト対応に備えて

はじめに こんにちは、suzu…

2か月 前

FSMを使った状態管理をGoで実装する

はじめに 一般的なアプリケーシ…

3か月 前