Go言語ジェネリクス実践ガイド
Go 1.18で導入されたジェネリクスは、型安全性を保ちながらコードの再利用性を大幅に向上させる機能です。本記事では、基本的な型パラメータから実践的なパターン、パフォーマンスへの影響まで、Goのジェネリクスを完全に解説します。
Goジェネリクスの基本
ジェネリクスを使用することで、複数の型に対して同じロジックを適用できます。
型パラメータの基本構文
// 基本的な型パラメータ
func Print[T any](value T) {
fmt.Println(value)
}
// 使用例
func main() {
Print[int](42) // 明示的な型指定
Print[string]("Hello") // 明示的な型指定
Print(3.14) // 型推論
Print(true) // 型推論
}
ジェネリック型の定義
// ジェネリックなスライス型
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
index := len(s.items) - 1
item := s.items[index]
s.items = s.items[:index]
return item, true
}
func (s *Stack[T]) Peek() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
return s.items[len(s.items)-1], true
}
func (s *Stack[T]) IsEmpty() bool {
return len(s.items) == 0
}
func (s *Stack[T]) Size() int {
return len(s.items)
}
// 使用例
func main() {
intStack := &Stack[int]{}
intStack.Push(1)
intStack.Push(2)
intStack.Push(3)
value, ok := intStack.Pop()
fmt.Println(value, ok) // 3 true
stringStack := &Stack[string]{}
stringStack.Push("hello")
stringStack.Push("world")
value2, ok2 := stringStack.Pop()
fmt.Println(value2, ok2) // world true
}
型制約(Type Constraints)
型制約を使用して、型パラメータに条件を付けることができます。
基本的な制約
// comparable制約: ==と!=が使える型
func Contains[T comparable](slice []T, target T) bool {
for _, item := range slice {
if item == target {
return true
}
}
return false
}
// 使用例
func main() {
numbers := []int{1, 2, 3, 4, 5}
fmt.Println(Contains(numbers, 3)) // true
words := []string{"apple", "banana", "cherry"}
fmt.Println(Contains(words, "banana")) // true
}
カスタム制約の定義
// 数値型の制約
type Number interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
~float32 | ~float64
}
// 加算可能な型の制約
type Addable interface {
Number | ~string
}
// 数値の合計を計算
func Sum[T Number](numbers []T) T {
var total T
for _, n := range numbers {
total += n
}
return total
}
// 最大値を見つける
func Max[T Number](a, b T) T {
if a > b {
return a
}
return b
}
// 最小値を見つける
func Min[T Number](a, b T) T {
if a < b {
return a
}
return b
}
// 使用例
func main() {
ints := []int{1, 2, 3, 4, 5}
fmt.Println(Sum(ints)) // 15
floats := []float64{1.5, 2.5, 3.5}
fmt.Println(Sum(floats)) // 7.5
fmt.Println(Max(10, 20)) // 20
fmt.Println(Min(3.14, 2.71)) // 2.71
}
メソッド制約
// Stringerインターフェースを持つ型の制約
type Stringer interface {
String() string
}
// Stringerを実装する型を処理
func PrintAll[T Stringer](items []T) {
for _, item := range items {
fmt.Println(item.String())
}
}
// カスタム型
type User struct {
Name string
Age int
}
func (u User) String() string {
return fmt.Sprintf("%s (%d歳)", u.Name, u.Age)
}
type Product struct {
Name string
Price int
}
func (p Product) String() string {
return fmt.Sprintf("%s - %d円", p.Name, p.Price)
}
// 使用例
func main() {
users := []User{
{Name: "太郎", Age: 25},
{Name: "花子", Age: 30},
}
PrintAll(users)
products := []Product{
{Name: "りんご", Price: 150},
{Name: "みかん", Price: 100},
}
PrintAll(products)
}
実践的なパターン
スライス操作ユーティリティ
package slices
// Map: スライスの各要素を変換
func Map[T, U any](slice []T, fn func(T) U) []U {
result := make([]U, len(slice))
for i, item := range slice {
result[i] = fn(item)
}
return result
}
// Filter: 条件に合う要素のみを抽出
func Filter[T any](slice []T, fn func(T) bool) []T {
result := make([]T, 0)
for _, item := range slice {
if fn(item) {
result = append(result, item)
}
}
return result
}
// Reduce: スライスを単一の値に集約
func Reduce[T, U any](slice []T, initial U, fn func(U, T) U) U {
result := initial
for _, item := range slice {
result = fn(result, item)
}
return result
}
// Find: 条件に合う最初の要素を見つける
func Find[T any](slice []T, fn func(T) bool) (T, bool) {
for _, item := range slice {
if fn(item) {
return item, true
}
}
var zero T
return zero, false
}
// Every: すべての要素が条件を満たすか
func Every[T any](slice []T, fn func(T) bool) bool {
for _, item := range slice {
if !fn(item) {
return false
}
}
return true
}
// Some: いずれかの要素が条件を満たすか
func Some[T any](slice []T, fn func(T) bool) bool {
for _, item := range slice {
if fn(item) {
return true
}
}
return false
}
// 使用例
func main() {
numbers := []int{1, 2, 3, 4, 5}
// Map: 各要素を2倍に
doubled := Map(numbers, func(n int) int {
return n * 2
})
fmt.Println(doubled) // [2 4 6 8 10]
// Filter: 偶数のみ抽出
evens := Filter(numbers, func(n int) bool {
return n%2 == 0
})
fmt.Println(evens) // [2 4]
// Reduce: 合計を計算
sum := Reduce(numbers, 0, func(acc, n int) int {
return acc + n
})
fmt.Println(sum) // 15
// Find: 3より大きい最初の数
found, ok := Find(numbers, func(n int) bool {
return n > 3
})
fmt.Println(found, ok) // 4 true
}
マップ操作ユーティリティ
package maps
// Keys: マップのキーをスライスで取得
func Keys[K comparable, V any](m map[K]V) []K {
keys := make([]K, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return keys
}
// Values: マップの値をスライスで取得
func Values[K comparable, V any](m map[K]V) []V {
values := make([]V, 0, len(m))
for _, v := range m {
values = append(values, v)
}
return values
}
// MapKeys: キーを変換して新しいマップを作成
func MapKeys[K1, K2 comparable, V any](m map[K1]V, fn func(K1) K2) map[K2]V {
result := make(map[K2]V, len(m))
for k, v := range m {
result[fn(k)] = v
}
return result
}
// MapValues: 値を変換して新しいマップを作成
func MapValues[K comparable, V1, V2 any](m map[K]V1, fn func(V1) V2) map[K]V2 {
result := make(map[K]V2, len(m))
for k, v := range m {
result[k] = fn(v)
}
return result
}
// Filter: 条件に合うエントリのみを抽出
func Filter[K comparable, V any](m map[K]V, fn func(K, V) bool) map[K]V {
result := make(map[K]V)
for k, v := range m {
if fn(k, v) {
result[k] = v
}
}
return result
}
// Merge: 複数のマップをマージ
func Merge[K comparable, V any](maps ...map[K]V) map[K]V {
result := make(map[K]V)
for _, m := range maps {
for k, v := range m {
result[k] = v
}
}
return result
}
// 使用例
func main() {
scores := map[string]int{
"Alice": 95,
"Bob": 80,
"Carol": 88,
}
// Keys: すべてのキーを取得
names := Keys(scores)
fmt.Println(names) // [Alice Bob Carol] (順不同)
// Values: すべての値を取得
points := Values(scores)
fmt.Println(points) // [95 80 88] (順不同)
// MapValues: すべてのスコアを10点加算
boosted := MapValues(scores, func(score int) int {
return score + 10
})
fmt.Println(boosted) // map[Alice:105 Bob:90 Carol:98]
// Filter: 90点以上のみ抽出
topScores := Filter(scores, func(name string, score int) bool {
return score >= 90
})
fmt.Println(topScores) // map[Alice:95]
}
ジェネリックなデータ構造
// 双方向リンクリスト
type LinkedList[T any] struct {
head *Node[T]
tail *Node[T]
size int
}
type Node[T any] struct {
value T
next *Node[T]
prev *Node[T]
}
func NewLinkedList[T any]() *LinkedList[T] {
return &LinkedList[T]{}
}
func (l *LinkedList[T]) PushFront(value T) {
node := &Node[T]{value: value}
if l.head == nil {
l.head = node
l.tail = node
} else {
node.next = l.head
l.head.prev = node
l.head = node
}
l.size++
}
func (l *LinkedList[T]) PushBack(value T) {
node := &Node[T]{value: value}
if l.tail == nil {
l.head = node
l.tail = node
} else {
node.prev = l.tail
l.tail.next = node
l.tail = node
}
l.size++
}
func (l *LinkedList[T]) PopFront() (T, bool) {
if l.head == nil {
var zero T
return zero, false
}
value := l.head.value
l.head = l.head.next
if l.head == nil {
l.tail = nil
} else {
l.head.prev = nil
}
l.size--
return value, true
}
func (l *LinkedList[T]) PopBack() (T, bool) {
if l.tail == nil {
var zero T
return zero, false
}
value := l.tail.value
l.tail = l.tail.prev
if l.tail == nil {
l.head = nil
} else {
l.tail.next = nil
}
l.size--
return value, true
}
func (l *LinkedList[T]) Size() int {
return l.size
}
func (l *LinkedList[T]) IsEmpty() bool {
return l.size == 0
}
// イテレータ
func (l *LinkedList[T]) ForEach(fn func(T)) {
current := l.head
for current != nil {
fn(current.value)
current = current.next
}
}
// 使用例
func main() {
list := NewLinkedList[string]()
list.PushBack("A")
list.PushBack("B")
list.PushFront("C")
list.ForEach(func(value string) {
fmt.Println(value)
})
// 出力: C, A, B
}
Result型パターン
// Result型: エラー処理をより明示的に
type Result[T any] struct {
value T
err error
}
func Ok[T any](value T) Result[T] {
return Result[T]{value: value}
}
func Err[T any](err error) Result[T] {
return Result[T]{err: err}
}
func (r Result[T]) IsOk() bool {
return r.err == nil
}
func (r Result[T]) IsErr() bool {
return r.err != nil
}
func (r Result[T]) Unwrap() T {
if r.err != nil {
panic(r.err)
}
return r.value
}
func (r Result[T]) UnwrapOr(defaultValue T) T {
if r.err != nil {
return defaultValue
}
return r.value
}
func (r Result[T]) UnwrapOrElse(fn func() T) T {
if r.err != nil {
return fn()
}
return r.value
}
func (r Result[T]) Map[U any](fn func(T) U) Result[U] {
if r.err != nil {
return Err[U](r.err)
}
return Ok(fn(r.value))
}
// 使用例
func Divide(a, b int) Result[int] {
if b == 0 {
return Err[int](errors.New("division by zero"))
}
return Ok(a / b)
}
func main() {
result1 := Divide(10, 2)
if result1.IsOk() {
fmt.Println(result1.Unwrap()) // 5
}
result2 := Divide(10, 0)
value := result2.UnwrapOr(0)
fmt.Println(value) // 0
// Map: 結果を変換
result3 := Divide(10, 2).Map(func(n int) string {
return fmt.Sprintf("結果: %d", n)
})
fmt.Println(result3.UnwrapOr("エラー")) // 結果: 5
}
パフォーマンスへの影響
コンパイル時の型消去
Goのジェネリクスは、コンパイル時に単相化(monomorphization)されます。つまり、使用される型ごとに専用のコードが生成されます。
// この関数は...
func Add[T Number](a, b T) T {
return a + b
}
// 使用時に以下のような専用関数が生成される
// func AddInt(a, b int) int { return a + b }
// func AddFloat64(a, b float64) float64 { return a + b }
パフォーマンス比較
package main
import (
"testing"
)
// ジェネリック版
func SumGeneric[T Number](numbers []T) T {
var total T
for _, n := range numbers {
total += n
}
return total
}
// 非ジェネリック版
func SumInt(numbers []int) int {
var total int
for _, n := range numbers {
total += n
}
return total
}
func BenchmarkSumGeneric(b *testing.B) {
numbers := make([]int, 1000)
for i := range numbers {
numbers[i] = i
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
SumGeneric(numbers)
}
}
func BenchmarkSumInt(b *testing.B) {
numbers := make([]int, 1000)
for i := range numbers {
numbers[i] = i
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
SumInt(numbers)
}
}
// 結果: ほぼ同等のパフォーマンス
// BenchmarkSumGeneric-8 1000000 1020 ns/op
// BenchmarkSumInt-8 1000000 1018 ns/op
ベストプラクティス
- 適切な制約を使用: 必要以上に制約を緩くしない
- インライン化を考慮: 小さな関数はインライン化される
- 型推論を活用: 明示的な型指定は最小限に
- 過度な抽象化を避ける: シンプルさを保つ
// Good: 適切な制約
func Max[T Number](a, b T) T {
if a > b {
return a
}
return b
}
// Bad: 過度に抽象化
func Process[T, U, V any](input T, fn1 func(T) U, fn2 func(U) V) V {
return fn2(fn1(input))
}
まとめ
Go言語のジェネリクスは、型安全性を保ちながらコードの再利用性を大幅に向上させます。主なポイントは以下の通りです。
- 型パラメータ:
[T any]構文で柔軟な型を扱える - 型制約: インターフェースで型の条件を指定
- 実践パターン: スライス、マップ、データ構造の汎用化
- パフォーマンス: コンパイル時の最適化により、ほぼオーバーヘッドなし
ジェネリクスを適切に活用することで、より保守しやすく、再利用可能なGoコードを書くことができます。