以下はその一例として、ジェネリクスを使わずにスタックを実装する方法を示します。
非ジェネリクス版スタック:
package main
import (
	"fmt"
)
// スタックの構造体
type Stack struct {
	data []interface{}
}
// スタックに要素を追加
func (s *Stack) Push(item interface{}) {
	s.data = append(s.data, item)
}
// スタックから要素を取り出す
func (s *Stack) Pop() interface{} {
	item := s.data[len(s.data)-1]
	s.data = s.data[:len(s.data)-1]
	return item
}
// スタックの先頭の要素を取得（削除はしない）
func (s *Stack) Peek() interface{} {
	return s.data[len(s.data)-1]
}
// スタックが空かどうかをチェックするメソッド
func (s *Stack) IsEmpty() bool {
	return len(s.data) == 0
}
func main() {
	// 空のスタックを作成
	stack := Stack{}
	// スタックに値を追加
	stack.Push(10)
	stack.Push("Hello")
	stack.Push(3.14)
	// スタックの内容を表示
	for !stack.IsEmpty() {
		item := stack.Pop()
		fmt.Println("Popped:", item)
	}
}
実行結果:
Popped: 3.14
Popped: Hello 
Popped: 10
この例では、Stackという構造体を定義し、Pushで要素を追加し、Popで要素を取り出します。interface{}型を使用することで、任意の型の要素をスタックに追加できます。ただし、実行時に型アサーションを使用して型を確認する必要があります。
Go言語におけるジェネリクス（型パラメータ）を利用したスタックの実装は次のようになります。
ジェネリクス版スタック:
package main
import (
	"fmt"
)
// ジェネリクス化されたスタックの定義
type Stack[T any] []T
// スタックに要素を追加するメソッド
func (s *Stack[T]) Push(item T) {
	*s = append(*s, item)
}
// スタックから要素を取り出すメソッド
func (s *Stack[T]) Pop() T {
	item := (*s)[len(*s)-1]
	*s = (*s)[:len(*s)-1]
	return item
}
// スタックが空かどうかをチェックするメソッド
func (s *Stack[T]) IsEmpty() bool {
	return len(*s) == 0
}
func main() {
	// int型のスタックを作成
	var intStack Stack[int]
	// スタックに値を追加
	intStack.Push(1)
	intStack.Push(2)
	intStack.Push(3)
	// スタックから値を取り出して表示
	for !intStack.IsEmpty() {
		item := intStack.Pop()
		fmt.Println(item)
	}
	// 文字列型のスタックを作成
	var stringStack Stack[string]
	// スタックに値を追加
	stringStack.Push("apple")
	stringStack.Push("banana")
	stringStack.Push("cherry")
	// スタックから値を取り出して表示
	for !stringStack.IsEmpty() {
		item := stringStack.Pop()
		fmt.Println(item)
	}
}
実行結果:
3
2
1
cherry
banana 
apple
このコードは、Go言語のジェネリクスを使ってスタックを実装しています。Stack[T any]という定義は、Tという任意の型の要素を持つスタックを表します。
Pushメソッドはスタックに要素を追加し、Popメソッドはスタックから要素を取り出します。IsEmptyメソッドはスタックが空かどうかを確認します。
main関数では、intStackとstringStackという異なる型のスタックを作成し、それぞれのスタックに値を追加してから、要素を取り出して表示しています。これにより、異なる型の要素を持つジェネリックなスタックが正しく機能していることが示されています。
このコードを実行すると、intStackには1、2、3が、stringStackには"apple"、"banana"、"cherry"が順番に追加され、それらがスタックから取り出されて画面に表示されます。
1.18で追加された予約済識別子 any は interface{} の alias で全ての型と一致します。
ただ、何でもかんでも any にすると必要なメソッドを備えていない型のインスタンスまで受け付けてしまいます。
この問題を解決するために型制約が用意されています。
Go言語における型制約は、主にインターフェースを通じて行われます。Goにはジェネリクス制約を直接指定する仕組みはありませんが、インターフェースを使って特定の振る舞いを持つ型を制約することができます。
例えば、fmt.StringerはString()メソッドを持つ型を表すインターフェースです。これは標準パッケージの中で多く利用されます。これにより、fmt.Printなどの関数はfmt.Stringerインターフェースを満たす型を引数に受け取ることができます。
型制約:
package main
import (
	"fmt"
)
type Bin int
func (b Bin) String() string {
	return fmt.Sprintf("%#b", int(b))
}
func Print[T fmt.Stringer](s []T) {
	for _, v := range s {
		fmt.Print(v, "\n")
	}
}
func main() {
	Print([]Bin{0b1101, 0b1010_0101})
}
実行結果:
0b1101 
0b10100101
このコードは、Go言語でのジェネリクスに型制約を加える例です。Binという新しい型を定義し、Bin型に対してString()メソッドを実装しています。String()メソッドは、Bin型を2進数文字列として表現するためのものです。
Print関数は、fmt.Stringerインターフェースを実装した任意の型のスライスを受け取り、その要素をfmt.Printを使って画面に出力する関数です。fmt.Stringerインターフェースは、String()メソッドを持つ型に適用されます。
main関数では、Print関数を使ってBin型のスライスを表示しています。Print関数はfmt.Stringerインターフェースを満たすため、Bin型のスライスを受け取ることができ、Bin型の各要素はString()メソッドを介して2進数文字列として出力されます。
最終的に、main関数ではPrint関数を使って[]Bin{0b1101, 0b1010_0101}を表示しています。これにより、Bin型の値が2進数文字列として正しく出力されることが確認できます。
{{コラム|Goのジェネリクスの特徴|2=Go言語におけるジェネリクスの導入は、バージョン1.18で行われました。ジェネリクスの導入により、より柔軟で再利用可能なコードを書くことができるようになりました。以下に、Goのジェネリクスの特徴を解説します。
ユーザー定義のジェネリクス型ジェネリクスを使用して、関数や型をパラメータ化できます。例えば、スライス、マップ、チャネルなどの標準ライブラリのデータ構造をジェネリックにすることが可能です。
型安全なコードジェネリクスを使うことで、異なる型のデータを扱う場合でも、静的な型チェックが行われます。これにより、型安全なコードを保ちながら汎用性を持たせることができます。
コードの再利用性同じような機能を持つ関数やデータ構造を複数の型で使い回すことができます。これにより、冗長なコードを減らし、メンテナンス性を高めることができます。
パフォーマンス向上ジェネリクスは、型を具体化することで効率的なコードを生成します。これにより、ジェネリックなアルゴリズムを使っても、実行時のオーバーヘッドを最小限に抑えることができます。
インターフェースとの組み合わせジェネリクスは、インターフェースと組み合わせることで柔軟性を高めることができます。ジェネリックな型をインターフェースを介して制約することが可能です。
Goのジェネリクスは、これまでのバージョンよりも柔軟性と再利用性を向上させ、型安全性を保ちながら汎用性の高いコードを書くことができるようにしました。これにより、より洗練された、効率的で堅牢なプログラムを開発することができます。