グローバルナビゲーションへ

本文へ

フッターへ

お役立ち情報Blog



Go1.18から実装されたジェネリクスの基本的な使い方をまとめてみた

業務でGoのジェネリクスに触れる機会があったので、自身の学習も兼ねてまとめてみました。

Goのジェネリクスの概要

Go1.18にジェネリクスの機能が搭載されました。この機能により、複数のデータ型に対して動作する関数や型が作成可能になりました。
ジェネリックな型や関数を使うには、対象に対して型パラメータを渡す必要があります。

それでは早速どういった使い方をするか見ていきましょう。

Goのジェネリクスの書き方

ジェネリックな型

型パラメータにanyを渡し、任意の型を扱う構造体Queueを定義してみます。

// 任意の型を格納するスライスをもつ
type Queue[T any] struct {
	data []T
}

// qと同じ型のvalをqのスライスに格納する
func (q *Queue[T]) Enqueue(val T) {
	q.data = append(q.data, val)
}

// qのスライスの先頭のデータを取り出す
func (q *Queue[T]) Dequeue() (T, bool) {
	if len(q.data) == 0 {
		var zeroValue T
		return zeroValue, false
	}

	head := q.data[0]
	q.data = q.data[1:]
	return head, true
}

func main() {
	qi := Queue[int]{}
	qi.Enqueue(1)
	qi.Enqueue(2)
	qi.Enqueue(3)

	// これは文字列のためエラーになる
	// qi.Enqueue("よん")

	num, _ := qi.Dequeue()
	fmt.Printf("num = %d\n", num)		// num = 1
	num, _ = qi.Dequeue()
	fmt.Printf("num = %d\n", num)		// num = 2
	num, _ = qi.Dequeue()
	fmt.Printf("num = %d\n", num)		// num = 3

	qs := Queue[string]{}
	qs.Enqueue("春はあけぼの")
	qs.Enqueue("夏は夜")

	// これは数値なのでエラーになる
	// qs.Enqueue(12345)
}

ジェネリックな関数

関数も以下のように実装することができます。

// 渡された任意の型のスライスの先頭を削除したものを返却する
func DeleteHead[T any](slice []T) []T {
	if len(slice) == 0 {
		return []T{}
	}
	return slice[1:]
}

func main() {
	is := []int{1, 2, 3, 4}
	fmt.Println(is)             // [1 2 3 4]
	fmt.Println(DeleteHead(is)) // [2 3 4]

	ss := []string{"a", "b", "c", "d"}
	fmt.Println(ss)             // [a b c d]
	fmt.Println(DeleteHead(ss)) // [b c d]
}

型制約について

型パラメータにanyではなく、interfaceを渡すことで使用する型を制限することができます。
例として、 fmt.Stringer インタフェースを使って実装してみます。

// 渡された引数を文字列化して三回出力する
func TripleEcho[T fmt.Stringer](x T) {
	for i := 0; i < 3; i++ {
		fmt.Println(x.String())
	}
}

type MyIntA int

// fmt.Stringerを満たすためのメソッド
func (i MyIntA) String() string {
	return strconv.Itoa(int(i))
}

type MyIntB int

func main() {
	var x MyIntA = 100
	TripleEcho(x) // 100, 100, 100

	// MyIntB は、fmt.Stringer インタフェースを満たしていないためエラーになる
	// var y MyIntB = 100
	// TripleEcho(y)
}

int型とstring型のみを型の制約とするジェネリックな関数を定義する場合は、以下のように実装することができます。

// intまたはstringを足して返却する
func Plus[T int | string](x T, y T) T {
	return x + y
}

func main() {
	s1 := "123"
	s2 := "456"
	fmt.Println(Plus(s1, s2))	// 123456

	i1 := 123
	i2 := 456
	fmt.Println(Plus(i1, i2))	// 579

	// stringとintを混ぜて使うことはできない
	// fmt.Println(Plus(s1, i1))
}

また、上と同じようにint型とstring型のみを制約とするinterfaceを宣言して使用することもできます。こちらの方法を利用すれば型制約を再利用でき、型パラメータを渡す際も簡潔に書くことができます。

// intまたはstringのみ満たすことができるinterface
type IntString interface {
	int | string
}

// intまたはstringを足して返却する
func Plus[T IntString](x T, y T) T {
	return x + y
}

func main() {
	s1 := "123"
	s2 := "456"
	fmt.Println(Plus(s1, s2)) // 123456

	i1 := 123
	i2 := 456
	fmt.Println(Plus(i1, i2)) // 579

	// stringとintを混ぜて使うことはできない
	// fmt.Println(Plus(s1, i1))
}

Goのジェネリクスを使うべきシチュエーション

ここまではジェネリクスの書き方について説明してきました。それでは、実際にどういった場合にジェネリクスを使えば良いでしょうか。

代表的な場合として、複数の型で同じようなデータ構造を定義したい場合です。今までも interface{} 等を使って実装することは可能でしたが、型安全性が低くなりがちで、コンパイル時にエラーを発見できずに実行時にパニックが発生してしまう可能性が高くなってしまいました。

Goの静的型付け言語の特徴を活かしたコードを書くには、ジェネリクスを使っていくべきでしょう。

さいごに

今回はGoのジェネリクスの基本的な使い方についてご紹介しました。 使いこなすことができれば、型安全性を維持しつつコードを書く量を減らせる素晴らしい機能です。
今回紹介しきれなかった機能もあるので、機会があれば紹介できればと思います。

この記事を書いた人

wanderlust
wanderlust事業開発部 web application engineer
これまで農業、士業と経験し、まったく異業種のエンジニアとしてアーティスに入社。
現在は事業開発部でバックエンドエンジニアとして仕事に従事。可読性の高いコードが書けるよう日々勉強中。趣味は一人旅。
この記事のカテゴリ

FOLLOW US

最新の情報をお届けします