- Go言語
Goジェネリクスの使い方|型パラメータと制約any・comparable
Goのジェネリクスとは、関数や型の定義に [T any] のような「型パラメータ」を付けて、1つのコードを複数の型で使い回せるようにする仕組みです(Go 1.18で導入)。 [T 制約] の「制約」の部分で、T に入れられる型と、T に対して使える演算(== や < など)が決まります。
この記事は理論の網羅よりも「すぐ使えること」に絞りました。①制約の早見表(any / comparable / cmp.Ordered / ~T / ユニオン)、②Map・Filter・Keys・Contains・Stack・Set などコピペで動く定番コード集と実行結果、③よく出るコンパイルエラーの原文と直し方、④標準の slices / maps / cmp で足りる場面の一覧、の4つを用意しています。コードはすべて package main からの完成形で、Go 1.24.7(linux/amd64)で go run した出力をそのまま載せています(ジェネリックメソッドの1例だけ Go 1.27.1 で実行)。
ジェネリクスの早見表
書き方の早見表
| やりたいこと | 書き方 | 呼び出し方 |
|---|---|---|
| ジェネリック関数 | func Max[T cmp.Ordered](a, b T) T |
Max(3, 7)(T は推論される) |
| 型引数を明示する | 同上 | Max[int64](10, 20) |
| 型パラメータ2つ | func Map[T, U any](s []T, f func(T) U) []U |
Map(nums, strconv.Itoa) |
| ジェネリックな構造体 | type Stack[T any] struct { items []T } |
var s Stack[string] |
| そのメソッド | func (s *Stack[T]) Push(v T) |
s.Push("a") |
| ジェネリックなmap型 | type Set[T comparable] map[T]struct{} |
NewSet(1, 2, 3) |
| 自作の制約 | type Number interface { ~int | ~float64 } |
func Sum[T Number](s []T) T |
| T のゼロ値 | var zero T |
return zero, false |
制約の早見表
| 制約 | T に入れられる型 | T に使える演算 | 主な用途 |
|---|---|---|---|
any |
すべての型 | 代入、引数渡し、[]T や map[K]T の要素にする |
Map、Filter、Stack など「中身に触れない」処理 |
comparable |
== で比べられる型(数値、文字列、bool、ポインタ、チャネル、比較可能な要素の配列・構造体、インターフェース型) |
==、!=、mapのキー |
Contains、Index、Set、Uniq |
cmp.Ordered |
整数、浮動小数点数、文字列(とそれを基底型にする独自型) | < <= > >= と == |
Max、Min、並べ替え |
ユニオン int | float64 |
列挙した型そのもの | 列挙した型すべてに共通する演算 | 数値だけ受け付ける関数 |
~int |
基底型が int の型すべて(type Yen int を含む) |
int と同じ | 独自型も受け付けたいとき |
メソッド付き interface { Name() string } |
そのメソッドを持つ型 | そのメソッドの呼び出し | 共通の振る舞いを要求する |
組み合わせ interface { ~string; Len() int } |
基底型が string で、かつ Len を持つ型 | 両方 | 型の集合とメソッドを同時に要求する |
any、comparable、cmp.Ordered の定義は、手元の Go 1.24.7 で go doc builtin.any、go doc builtin.comparable、go doc cmp.Ordered を実行して確認しました。cmp.Ordered の中身は次のとおりです。
type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 |
~string
}
最初の1本:型ごとの関数を1つにまとめる
ジェネリクスが役立つのは、「型が違うだけで中身が同じ関数」をいくつも書いているときです。MaxInt をジェネリックな Max に書き換えてみます。
package main
import "fmt"
// ジェネリクスなし: 型ごとに同じ関数を書く
func MaxInt(a, b int) int {
if a > b {
return a
}
return b
}
// ジェネリクスあり: [T 制約] が型パラメータ
type Ordered interface {
~int | ~int64 | ~float64 | ~string
}
func Max[T Ordered](a, b T) T {
if a > b {
return a
}
return b
}
func main() {
fmt.Println(MaxInt(3, 7))
fmt.Println(Max(3, 7)) // T は int と推論される
fmt.Println(Max(2.5, 1.5)) // T は float64
fmt.Println(Max("apple", "go")) // T は string
fmt.Println(Max[int64](10, 20)) // 型引数を明示することもできる
}
実行結果(go run main.go):
7
7
2.5
go
20
[T Ordered]が型パラメータの宣言です。関数名のすぐ後ろ、引数リストの前に書きます。- 呼び出すときの型はたいてい省略できます。 引数から推論されるためです(型推論)。
Max[int64](10, 20)のように角かっこで型を明示することもできます。- ここでは説明のために
Orderedを自作しましたが、普段は標準のcmp.Orderedを使えば足ります。
コピペで使える定番のジェネリック関数
よく書く8つの関数を1ファイルにまとめました。必要なものだけ切り出して使えます。
package main
import (
"fmt"
"strings"
)
// Map: 各要素を変換する(T → U)
func Map[T, U any](s []T, f func(T) U) []U {
out := make([]U, 0, len(s))
for _, v := range s {
out = append(out, f(v))
}
return out
}
// Filter: 条件に合う要素だけ残す
func Filter[T any](s []T, keep func(T) bool) []T {
var out []T
for _, v := range s {
if keep(v) {
out = append(out, v)
}
}
return out
}
// Reduce: 1つの値にまとめる
func Reduce[T, A any](s []T, init A, f func(A, T) A) A {
acc := init
for _, v := range s {
acc = f(acc, v)
}
return acc
}
// Contains: 含まれているか(== を使うので comparable)
func Contains[T comparable](s []T, target T) bool {
for _, v := range s {
if v == target {
return true
}
}
return false
}
// Keys: mapのキー一覧(キーは必ず comparable)
func Keys[K comparable, V any](m map[K]V) []K {
out := make([]K, 0, len(m))
for k := range m {
out = append(out, k)
}
return out
}
// Uniq: 重複を除く(順序は保つ)
func Uniq[T comparable](s []T) []T {
seen := make(map[T]struct{}, len(s))
var out []T
for _, v := range s {
if _, ok := seen[v]; !ok {
seen[v] = struct{}{}
out = append(out, v)
}
}
return out
}
// GroupBy: キーごとにまとめる
func GroupBy[T any, K comparable](s []T, key func(T) K) map[K][]T {
out := make(map[K][]T)
for _, v := range s {
k := key(v)
out[k] = append(out[k], v)
}
return out
}
// Ptr: 値からポインタを作る(リテラルのアドレスが欲しいとき)
func Ptr[T any](v T) *T { return &v }
type Task struct {
Title string
Done bool
}
func main() {
nums := []int{1, 2, 3, 4, 5, 6}
fmt.Println("Map :", Map(nums, func(n int) string { return fmt.Sprintf("#%d", n) }))
fmt.Println("Filter :", Filter(nums, func(n int) bool { return n%2 == 0 }))
fmt.Println("Reduce :", Reduce(nums, 0, func(acc, n int) int { return acc + n }))
fmt.Println("Contains:", Contains(nums, 4), Contains([]string{"go", "rust"}, "java"))
fmt.Println("Uniq :", Uniq([]string{"a", "b", "a", "c", "b"}))
stock := map[string]int{"pen": 12}
fmt.Println("Keys :", Keys(stock))
tasks := []Task{{"設計", true}, {"実装", false}, {"レビュー", false}}
titles := Map(tasks, func(t Task) string { return t.Title })
fmt.Println("構造体 :", strings.Join(titles, " / "))
g := GroupBy(tasks, func(t Task) bool { return t.Done })
fmt.Println("GroupBy :", len(g[true]), "件完了 /", len(g[false]), "件未完了")
p := Ptr(42)
fmt.Println("Ptr :", *p)
}
実行結果:
Map : [#1 #2 #3 #4 #5 #6]
Filter : [2 4 6]
Reduce : 21
Contains: true false
Uniq : [a b c]
Keys : [pen]
構造体 : 設計 / 実装 / レビュー
GroupBy : 1 件完了 / 2 件未完了
Ptr : 42
| 関数 | 制約 | 制約がそうなる理由 |
|---|---|---|
Map[T, U any] |
any | 要素を関数に渡すだけで、中身を比べない |
Filter[T any] |
any | 判定は引数の関数に任せる |
Reduce[T, A any] |
any | 集計の型 A を要素の型 T と別にできる |
Contains[T comparable] |
comparable | 中で v == target を使う |
Keys[K comparable, V any] |
K は comparable | mapのキーは必ず比較可能 |
Uniq[T comparable] |
comparable | T をmapのキーにする |
GroupBy[T any, K comparable] |
K は comparable | まとめる単位をmapのキーにする |
Ptr[T any] |
any | 値を受け取ってアドレスを返すだけ |
制約を選ぶコツは、「関数の中で T に何をしているか」を見ることです。 何もしていなければ any、== を使うなら comparable、< を使うなら cmp.Ordered、という順に考えると迷いません。
構造体とメソッドにジェネリクスを使う(Stack・Set・Pair)
型パラメータは型定義にも付けられます。メソッドのレシーバには Stack[T] のように型パラメータの名前を書きます(ここでは制約を書き直しません)。
package main
import (
"errors"
"fmt"
)
// ---- Stack: どんな型でも積める ----
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) }
func (s *Stack[T]) Pop() (T, bool) {
var zero T // T のゼロ値
if len(s.items) == 0 {
return zero, false
}
last := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return last, true
}
func (s *Stack[T]) Len() int { return len(s.items) }
// ---- Set: mapのキーにするので comparable ----
type Set[T comparable] map[T]struct{}
func NewSet[T comparable](vs ...T) Set[T] {
s := make(Set[T])
for _, v := range vs {
s.Add(v)
}
return s
}
func (s Set[T]) Add(v T) { s[v] = struct{}{} }
func (s Set[T]) Has(v T) bool { _, ok := s[v]; return ok }
func (s Set[T]) Len() int { return len(s) }
func (s Set[T]) Union(o Set[T]) Set[T] {
out := NewSet[T]()
for v := range s {
out.Add(v)
}
for v := range o {
out.Add(v)
}
return out
}
// ---- Pair: 型パラメータ2つ ----
type Pair[K comparable, V any] struct {
Key K
Val V
}
func (p Pair[K, V]) String() string { return fmt.Sprintf("%v=%v", p.Key, p.Val) }
// ---- Result風: 値かエラーのどちらか ----
type Result[T any] struct {
Val T
Err error
}
func Try[T any](f func() (T, error)) Result[T] {
v, err := f()
return Result[T]{Val: v, Err: err}
}
func main() {
var st Stack[string] // ゼロ値のまま使える
st.Push("a")
st.Push("b")
v, ok := st.Pop()
fmt.Println("Stack:", v, ok, "残り", st.Len())
st.Pop()
v, ok = st.Pop()
fmt.Printf("空のPop: %q %v\n", v, ok)
ids := NewSet(1, 2, 3)
ids.Add(3)
fmt.Println("Set :", ids.Len(), ids.Has(2), ids.Has(9))
fmt.Println("Union:", ids.Union(NewSet(3, 4)).Len())
p := Pair[string, int]{"pen", 12}
fmt.Println("Pair :", p)
r := Try(func() (int, error) { return 0, errors.New("在庫なし") })
fmt.Println("Result:", r.Val, r.Err)
}
実行結果:
Stack: b true 残り 1
空のPop: "" false
Set : 3 true false
Union: 4
Pair : pen=12
Result: 0 在庫なし
| ポイント | 説明 |
|---|---|
var st Stack[string] |
型を使うときは型引数が必須。ゼロ値のまま Push できる |
var zero T |
T のゼロ値を作る定番の書き方。string なら ""、int なら 0、ポインタなら nil |
NewSet(1, 2, 3) |
コンストラクタ関数を用意すると、引数から型が推論されて書きやすい |
NewSet[T]() |
引数が無いときは推論できないので型を明示する |
Set[T comparable] |
T をmapのキーにするので any では定義できない |
Pair[K, V] の String メソッド |
型パラメータが複数ならレシーバにも全部書く |
メソッドだけに型パラメータを付ける書き方(Go 1.27から)
func (b Box) Map[T any](v T) T のように、メソッド自身が新しい型パラメータを持つ書き方は、Go 1.24.7 では次のエラーになります。
main.go:7:17: syntax error: method must have no type parameters
同じ形のコードを Go 1.27.1 で実行したところ、コンパイルが通り動作しました。
package main
import "fmt"
type Box struct{ prefix string }
// Go 1.27: メソッド自身が型パラメータを持てる
func (b Box) Wrap[T any](v T) string {
return fmt.Sprintf("%s<%v>", b.prefix, v)
}
func main() {
b := Box{"box"}
fmt.Println(b.Wrap(42), b.Wrap("go"))
}
実行結果(Go 1.27.1):
box<42> box<go>
Go 1.26以前でも動かしたいコードでは、メソッドではなく関数にする(func Wrap[T any](b Box, v T) string)と、どの版でも同じように使えます。
制約を自作する(ユニオン・チルダ・メソッド)
interface の中に型を並べると、制約を自分で定義できます。
package main
import (
"cmp"
"fmt"
)
// ユニオン + ~ : 「基底型がこのどれか」の型すべて
type Number interface {
~int | ~int64 | ~float64
}
func Sum[T Number](s []T) T {
var total T
for _, v := range s {
total += v
}
return total
}
// cmp.Ordered: < > で比べられる型(整数・浮動小数点・文字列)
func MaxOf[T cmp.Ordered](s []T) (T, bool) {
var zero T
if len(s) == 0 {
return zero, false
}
m := s[0]
for _, v := range s[1:] {
if v > m {
m = v
}
}
return m, true
}
// メソッドを要求する制約
type Namer interface {
Name() string
}
func Names[T Namer](s []T) []string {
out := make([]string, 0, len(s))
for _, v := range s {
out = append(out, v.Name())
}
return out
}
// 型の集合とメソッドを両方要求する制約
type StringLike interface {
~string
Len() int
}
type Yen int // 基底型は int
type Code string // 基底型は string
type Member struct{ name string }
func (c Code) Len() int { return len(c) }
func (m Member) Name() string { return m.name }
func TotalLen[T StringLike](s []T) int {
n := 0
for _, v := range s {
n += v.Len()
}
return n
}
func Index[T comparable](s []T, x T) int {
for i, v := range s {
if v == x {
return i
}
}
return -1
}
func main() {
fmt.Println("Sum int :", Sum([]int{1, 2, 3}))
fmt.Println("Sum float64:", Sum([]float64{1.5, 2.5}))
fmt.Println("Sum Yen :", Sum([]Yen{100, 250})) // ~int なので独自型もOK
m, _ := MaxOf([]string{"go", "rust", "java"})
fmt.Println("MaxOf :", m)
_, ok := MaxOf([]int{})
fmt.Println("空のMaxOf :", ok)
fmt.Println("Names :", Names([]Member{{"sato"}, {"suzuki"}}))
fmt.Println("TotalLen :", TotalLen([]Code{"A01", "B2"}))
// comparable: 構造体や配列、インターフェース型も使える
type Point struct{ X, Y int }
fmt.Println("Index struct:", Index([]Point{{1, 2}, {3, 4}}, Point{3, 4}))
fmt.Println("Index any :", Index([]any{1, "a", 2.5}, any("a")))
}
実行結果:
Sum int : 6
Sum float64: 4
Sum Yen : 350
MaxOf : rust
空のMaxOf : false
Names : [sato suzuki]
TotalLen : 5
Index struct: 1
Index any : 1
| 書き方 | 意味 | type Yen int を渡せるか |
|---|---|---|
int | float64 |
int か float64 そのもの | 渡せない |
~int | ~float64 |
基底型が int か float64 の型すべて | 渡せる |
cmp.Ordered |
順序のある型すべて(中身は ~ 付き) |
渡せる |
自作の制約では、基本的に ~ を付けておくのがおすすめです。 type Yen int や type Code string のような独自型を後から渡したくなる場面が多いためです。
comparable については、出力の最後の2行のとおり構造体やインターフェース型(any)も渡せます。インターフェース型を comparable として扱えるのは Go 1.20 からです。ただし、中身が比較できない値(スライスなど)だった場合は実行時に panic になるため、any を要素にする使い方は必要なときに限るのが安心です。
型推論が効く場面・型を明示する場面
package main
import "fmt"
// 引数に T が出てこないので、呼び出し側で型を明示する
func Zero[T any]() T {
var zero T
return zero
}
// 見つからなければ既定値を返す
func GetOr[K comparable, V any](m map[K]V, key K, def V) V {
if v, ok := m[key]; ok {
return v
}
return def
}
// 戻り値の型だけ明示し、残りは推論に任せる(明示は左から順)
func Convert[To, From any](v From, f func(From) To) To { return f(v) }
func main() {
fmt.Printf("%v %q %v\n", Zero[int](), Zero[string](), Zero[[]int]() == nil)
stock := map[string]int{"pen": 12}
fmt.Println(GetOr(stock, "pen", 0), GetOr(stock, "clip", -1))
s := Convert[string](42, func(n int) string { return fmt.Sprint("No.", n) })
fmt.Println(s)
// ジェネリック関数を変数に入れるときは型を確定させる
f := Zero[float64]
fmt.Println(f())
}
実行結果:
0 "" true
12 -1
No.42
0
| 場面 | 推論 | 書き方 |
|---|---|---|
| 引数に T が出てくる | 効く | Max(3, 7) |
| T が戻り値にしか出てこない | 効かない | Zero[int]() |
| 一部だけ明示したい | 左から順に明示、残りは推論 | Convert[string](42, f) |
| 関数を変数に入れる | 型を確定させる | f := Zero[float64] |
| ジェネリックな型を使う | 常に明示 | var s Stack[string] |
「明示したい型パラメータを左に置く」と、Convert[string](...) のように必要な分だけ書けて呼び出しが短くなります。
よく出るコンパイルエラーと直し方
エラーが出るコードを実際に go run し、メッセージの原文を載せます(Go 1.24.7)。
[]int does not satisfy comparable
package main
import "fmt"
func Contains[T comparable](s []T, x T) bool {
for _, v := range s {
if v == x {
return true
}
}
return false
}
func main() {
rows := [][]int{{1, 2}, {3, 4}}
fmt.Println(Contains(rows, []int{3, 4}))
}
main.go:16:22: []int does not satisfy comparable
スライス・map・関数は == で比べられないため、comparable を満たしません。比較の方法を関数で受け取る形にするか、標準の slices.ContainsFunc と slices.Equal を組み合わせます。
found := slices.ContainsFunc(rows, func(r []int) bool {
return slices.Equal(r, []int{3, 4})
})
in call to Zero, cannot infer T
package main
import "fmt"
func Zero[T any]() T {
var zero T
return zero
}
func main() {
fmt.Println(Zero())
}
main.go:11:18: in call to Zero, cannot infer T (declared at main.go:5:11)
T が引数に出てこないので、手がかりがありません。Zero[int]() のように型を明示します。
operator + not defined on total (variable of type T constrained by any)
package main
import "fmt"
func Sum[T any](s []T) T {
var total T
for _, v := range s {
total += v
}
return total
}
func main() {
fmt.Println(Sum([]int{1, 2, 3}))
}
main.go:8:3: invalid operation: operator + not defined on total (variable of type T constrained by any)
any は「どんな型でもよい」代わりに、演算子は使えません。+ を使える型だけに絞った制約(~int | ~float64 など)に変えます。
Yen does not satisfy Number (possibly missing ~ for int in Number)
package main
import "fmt"
type Number interface {
int | float64
}
func Sum[T Number](s []T) T {
var total T
for _, v := range s {
total += v
}
return total
}
type Yen int
func main() {
fmt.Println(Sum([]Yen{100, 250}))
}
main.go:20:17: Yen does not satisfy Number (possibly missing ~ for int in Number)
コンパイラが直し方まで教えてくれています。制約を ~int | ~float64 にすると、基底型が int の Yen も渡せます。
cannot use generic type Stack[T any] without instantiation
package main
import "fmt"
type Stack[T any] struct {
items []T
}
func main() {
var s Stack
fmt.Println(s)
}
main.go:10:8: cannot use generic type Stack[T any] without instantiation
ジェネリックな型は、型引数を付けて初めて使える型になります。var s Stack[int] のように書きます。
cannot use type Number outside a type constraint
package main
import "fmt"
type Number interface {
~int | ~float64
}
func main() {
var n Number = 1
fmt.Println(n)
}
main.go:10:8: cannot use type Number outside a type constraint: interface contains type constraints
型を並べた interface(ユニオンや ~ を含むもの)と comparable は、制約専用です。変数や引数の型には使えないので、func F[T Number](n T) のように型パラメータ経由で使います。
type float64 of rate does not match inferred type int for T
package main
import "fmt"
func Max[T int | float64](a, b T) T {
if a > b {
return a
}
return b
}
func main() {
count := 3
rate := 2.5
fmt.Println(Max(count, rate))
}
main.go:15:25: in call to Max, type float64 of rate does not match inferred type int for T
1つの T に2つの型を当てはめようとしています。Max(float64(count), rate) のように型をそろえてから渡します。
エラーの逆引き表
| メッセージ(原文の要点) | 原因 | 直し方 |
|---|---|---|
X does not satisfy comparable |
スライス・map・関数を comparable に渡した | 比較関数を受け取る形にする/slices.ContainsFunc |
cannot infer T |
T が引数に出てこない | F[int]() と明示する |
operator + not defined on ... constrained by any |
any のまま演算子を使った | 数値のユニオンや cmp.Ordered に絞る |
possibly missing ~ for int in ... |
独自型をユニオンに渡した | 制約を ~int にする |
cannot use generic type ... without instantiation |
型引数なしで型を使った | Stack[int] と書く |
cannot use type ... outside a type constraint |
制約用のinterfaceを変数の型にした | 型パラメータの制約として使う |
does not match inferred type int for T |
同じ T に違う型の引数を渡した | 変換して型をそろえる |
method must have no type parameters |
メソッドに型パラメータを付けた(Go 1.26以前) | 関数にする/Go 1.27以降を使う |
標準のslices・maps・cmpで足りる場面
Go 1.21で slices、maps、cmp パッケージと組み込み関数 min / max が標準に入り、Go 1.23でイテレータ対応の関数(slices.Sorted、maps.Keys など)が加わりました。自作する前に、標準にあるかどうかを確認すると書く量が減ります。
package main
import (
"cmp"
"fmt"
"maps"
"slices"
)
type Item struct {
Name string
Stock int
}
func main() {
nums := []int{5, 2, 8, 2, 9, 1}
fmt.Println("Contains :", slices.Contains(nums, 8))
fmt.Println("Index :", slices.Index(nums, 2))
fmt.Println("IndexFunc:", slices.IndexFunc(nums, func(n int) bool { return n > 7 }))
fmt.Println("Max/Min :", slices.Max(nums), slices.Min(nums))
fmt.Println("max/min :", max(3, 9, 4), min(2.5, 1.0)) // 組み込み関数(Go 1.21〜)
sorted := slices.Sorted(slices.Values(nums)) // 元のスライスは変えない
fmt.Println("Sorted :", sorted, "/ 元:", nums)
fmt.Println("Compact :", slices.Compact(slices.Clone(sorted)))
i, found := slices.BinarySearch(sorted, 8)
fmt.Println("BinarySearch:", i, found)
rev := slices.Clone(nums)
slices.Reverse(rev)
fmt.Println("Reverse :", rev)
fmt.Println("Equal :", slices.Equal(nums, rev))
items := []Item{{"pen", 12}, {"note", 3}, {"clip", 12}}
slices.SortFunc(items, func(a, b Item) int {
return cmp.Or(
cmp.Compare(b.Stock, a.Stock), // 在庫の多い順
cmp.Compare(a.Name, b.Name), // 同じなら名前順
)
})
fmt.Println("SortFunc :", items)
stock := map[string]int{"pen": 12, "note": 3, "clip": 12}
fmt.Println("Keys :", slices.Sorted(maps.Keys(stock)))
fmt.Println("Values :", slices.Sorted(maps.Values(stock)))
cp := maps.Clone(stock)
cp["pen"] = 0
fmt.Println("Clone :", stock["pen"], cp["pen"])
maps.DeleteFunc(cp, func(k string, v int) bool { return v == 0 })
fmt.Println("DeleteFunc:", len(cp))
fmt.Println("cmp.Compare:", cmp.Compare(1, 2), cmp.Compare("b", "a"), cmp.Compare(3, 3))
fmt.Println("cmp.Or :", cmp.Or("", "既定値"))
}
実行結果:
Contains : true
Index : 1
IndexFunc: 2
Max/Min : 9 1
max/min : 9 1
Sorted : [1 2 2 5 8 9] / 元: [5 2 8 2 9 1]
Compact : [1 2 5 8 9]
BinarySearch: 4 true
Reverse : [1 9 2 8 2 5]
Equal : false
SortFunc : [{clip 12} {pen 12} {note 3}]
Keys : [clip note pen]
Values : [3 12 12]
Clone : 12 0
DeleteFunc: 2
cmp.Compare: -1 1 0
cmp.Or : 既定値
自作が必要かどうかの判断表
| やりたいこと | 標準で足りるか | 使うもの |
|---|---|---|
| 含まれているか | 足りる | slices.Contains / slices.ContainsFunc |
| 位置を探す | 足りる | slices.Index / slices.IndexFunc |
| 最大・最小 | 足りる | slices.Max / slices.Min、組み込みの max / min |
| 並べ替え | 足りる | slices.Sort / slices.SortFunc / slices.Sorted |
| 複数キーで並べ替え | 足りる | cmp.Or と cmp.Compare の組み合わせ |
| 隣り合う重複を除く | 足りる | slices.Sort のあと slices.Compact |
| 逆順・複製・比較 | 足りる | slices.Reverse / slices.Clone / slices.Equal |
| ソート済みから探す | 足りる | slices.BinarySearch |
| mapのキー・値の一覧 | 足りる | slices.Sorted(maps.Keys(m)) / maps.Values(m) |
| mapの複製・条件で削除 | 足りる | maps.Clone / maps.DeleteFunc |
| 空なら既定値 | 足りる | cmp.Or(v, 既定値) |
| 各要素を変換(Map) | 自作 | 上の Map |
| 条件で絞り込んだ新しいスライス(Filter) | 自作 | 上の Filter(元を書き換えてよければ slices.DeleteFunc) |
| 1つの値に集計(Reduce) | 自作 | 上の Reduce(for文でも十分) |
| 順序を保って重複を除く | 自作 | 上の Uniq |
| グループ分け | 自作 | 上の GroupBy |
| Stack・Set などのコンテナ | 自作 | 上の Stack / Set |
maps.Keys と maps.Values はイテレータを返すので、スライスにしたいときは slices.Sorted(...) か slices.Collect(...) に渡します。mapの走査順は決まっていないため、出力を安定させたい場面では slices.Sorted が便利です。
ジェネリクスにするか迷ったときの判断表
| 状況 | おすすめ | 理由 |
|---|---|---|
| 型が違うだけで本体が同じ関数が2つ以上ある | ジェネリクス | 1つにまとめられる |
| スライス・map・チャネルを扱う汎用の処理 | ジェネリクス | 要素の型に依存しない |
| Stack・Set・キャッシュなどのコンテナ | ジェネリクス | 取り出すときの型アサーションが不要になる |
| 型ごとに処理の中身が違う | インターフェース | メソッドを呼び分けるのが目的だから |
| 使う型が1種類だけ | 具体的な型のまま | 読みやすさが勝る |
any を受けて中で型スイッチしている |
そのまま、またはインターフェース | 型ごとの分岐はジェネリクスの得意分野ではない |
「まず具体的な型で書き、同じ形が2回3回と出てきたら型パラメータにする」という順番だと、制約に何が必要かも自然に決まります。
よくある質問
Q. any と interface{} は違うものですか?
A. 同じものです。go doc builtin.any でも type any = interface{} と表示され、any は別名として定義されています。制約として [T any] と書いた場合は「どんな型でもよい」という意味になります。
Q. any と comparable はどう使い分けますか?
A. 関数の中で == や != を使う、または T をmapのキーにするなら comparable、そうでなければ any です。comparable にしておくと、スライスのように比べられない型を渡したときにコンパイルの段階で気づけます。
Q. constraints.Ordered と cmp.Ordered のどちらを使えばよいですか?
A. 標準ライブラリの cmp.Ordered(Go 1.21以降)を使えば、外部モジュールを追加せずに済みます。この記事のコードはすべて標準ライブラリだけで動かしています。
Q. メソッドに型パラメータを付けられますか?
A. レシーバの型が持つ型パラメータ(func (s *Stack[T]) Push(v T))はどの版でも使えます。メソッド自身に新しい型パラメータを付ける書き方は、Go 1.24.7 では method must have no type parameters となり、Go 1.27.1 では動作することを確認しました。
まとめ
- 型パラメータは
[T 制約]の形で関数名や型名の後ろに書き、呼び出しでは多くの場合に推論されます。 - 制約は「T に何をするか」で選ぶ:何もしないなら any、
==なら comparable、<なら cmp.Ordered。 - 自作の制約には
~を付けると、type Yen intのような独自型も受け付けられます。 - Map・Filter・Reduce・Uniq・GroupBy・Stack・Set は自作、Contains・Index・Sort・Max・Keys などは標準の slices / maps / cmp で足ります。
- 型推論が効かないのは、T が引数に出てこないとき。
Zero[int]()のように明示します。 - エラーメッセージは直し方のヒント付き(
possibly missing ~など)なので、原文をそのまま読むのが近道です。 - メソッド自身の型パラメータは Go 1.27 から。それより前の版でも動かすなら関数にします。
参考・出典
- Go 1.24.7 に同梱のドキュメント(
go doc builtin.any/go doc builtin.comparable/go doc cmp.Ordered)― 定義を確認 - 本文の実行結果・エラーメッセージは Go 1.24.7(linux/amd64)で
go runを実行したものです。ジェネリックメソッドの例のみ Go 1.27.1 で実行しました。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。