Go ジェネリクスガイド
Go 1.18で導入された型パラメータと制約を使い、型安全で再利用可能なコードを書く
Go ジェネリクスガイド
Go 1.18で導入された型パラメータと制約を使い、型安全で再利用可能なコードを書く
この章で学ぶこと
- 型パラメータ の構文と基本的な使い方(ジェネリック関数・型)
- 制約(constraints) の定義方法と標準ライブラリの制約パッケージ
- 実践パターン — コレクション操作、リポジトリパターン、Result型の実装
- 標準ライブラリ の
slices、maps、cmpパッケージの活用 - パフォーマンス特性 とジェネリクスの適用判断基準
前提知識
このガイドを読む前に、以下の知識があると理解が深まります:
- 基本的なプログラミングの知識
- 関連する基礎概念の理解
- Go CLI開発ガイド の内容を理解していること
1. ジェネリクスの基本
ジェネリクス導入前後の比較
【導入前】型ごとに関数を複製
func MaxInt(a, b int) int { if a > b { return a }; return b }
func MaxFloat(a, b float64) float64 { if a > b { return a }; return b }
func MaxString(a, b string) string { if a > b { return a }; return b }
↓ ジェネリクスで統一
【導入後】一つの関数で全ての型に対応
func MaxT cmp.Ordered T { if a > b { return a }; return b }
型パラメータの構文
+-------- 型パラメータリスト --------+
| |
func FuncName T constraint returns
| |
| +--- 制約: T が満たすべき条件
+----------- 型パラメータ名
コード例1: 最初のジェネリック関数
package main
import (
"cmp"
"fmt"
)
// T は cmp.Ordered を満たす任意の型
func MaxT cmp.Ordered T {
if a > b {
return a
}
return b
}
func MinT cmp.Ordered T {
if a < b {
return a
}
return b
}
func ClampT cmp.Ordered T {
return Max(lo, Min(val, hi))
}
func main() {
fmt.Println(Max(3, 7)) // 7
fmt.Println(Max(3.14, 2.71)) // 3.14
fmt.Println(Max("apple", "banana")) // banana
fmt.Println(Clamp(150, 0, 100)) // 100
}コード例2: ジェネリックなスライス操作
// Map はスライスの各要素に関数を適用する
func MapT, U any U) []U {
result := make([]U, len(s))
for i, v := range s {
result[i] = f(v)
}
return result
}
// Filter はスライスから条件を満たす要素を抽出する
func FilterT any bool) []T {
var result []T
for _, v := range s {
if pred(v) {
result = append(result, v)
}
}
return result
}
// Reduce はスライスを単一の値に集約する
func ReduceT, U any U) U {
acc := init
for _, v := range s {
acc = f(acc, v)
}
return acc
}
// Find は条件を満たす最初の要素を返す
func FindT any bool) (T, bool) {
for _, v := range s {
if pred(v) {
return v, true
}
}
var zero T
return zero, false
}
// GroupBy はキー関数に基づいてグルーピングする
func GroupByT any, K comparable K) map[K][]T {
result := make(map[K][]T)
for _, v := range s {
key := keyFn(v)
result[key] = append(result[key], v)
}
return result
}
// Chunk はスライスを指定サイズのチャンクに分割する
func ChunkT any [][]T {
if size <= 0 {
return nil
}
var chunks [][]T
for i := 0; i < len(s); i += size {
end := i + size
if end > len(s) {
end = len(s)
}
chunks = append(chunks, s[i:end])
}
return chunks
}
// Unique は重複を排除したスライスを返す
func UniqueT comparable []T {
seen := make(map[T]struct{})
var result []T
for _, v := range s {
if _, ok := seen[v]; !ok {
seen[v] = struct{}{}
result = append(result, v)
}
}
return result
}
// 使用例
func main() {
nums := []int{1, 2, 3, 4, 5}
doubled := Map(nums, func(n int) int { return n * 2 })
// [2, 4, 6, 8, 10]
evens := Filter(nums, func(n int) bool { return n%2 == 0 })
// [2, 4]
sum := Reduce(nums, 0, func(acc, n int) int { return acc + n })
// 15
// 文字列操作
words := []string{"hello", "world", "go", "generics"}
lengths := Map(words, func(s string) int { return len(s) })
// [5, 5, 2, 8]
longWords := Filter(words, func(s string) bool { return len(s) > 3 })
// ["hello", "world", "generics"]
// グルーピング
type User struct {
Name string
Role string
}
users := []User{
{"Alice", "admin"}, {"Bob", "user"}, {"Charlie", "admin"}, {"Dave", "user"},
}
byRole := GroupBy(users, func(u User) string { return u.Role })
// map["admin":[Alice, Charlie] "user":[Bob, Dave]]
// 重複排除
ids := []int{1, 2, 3, 2, 1, 4, 3, 5}
unique := Unique(ids) // [1, 2, 3, 4, 5]
}コード例3: FlatMap と Zip
// FlatMap はスライスの各要素をスライスに変換してフラット化する
func FlatMapT, U any []U) []U {
var result []U
for _, v := range s {
result = append(result, f(v)...)
}
return result
}
// Zip は2つのスライスを組にする
func ZipT, U any []Pair[T, U] {
minLen := len(a)
if len(b) < minLen {
minLen = len(b)
}
result := make([]Pair[T, U], minLen)
for i := 0; i < minLen; i++ {
result[i] = Pair[T, U]{First: a[i], Second: b[i]}
}
return result
}
type Pair[T, U any] struct {
First T
Second U
}
// Partition は条件に基づいてスライスを2つに分割する
func PartitionT any bool) (matched, unmatched []T) {
for _, v := range s {
if pred(v) {
matched = append(matched, v)
} else {
unmatched = append(unmatched, v)
}
}
return
}
// 使用例
func example() {
// FlatMap: 文をトークンに分割
sentences := []string{"hello world", "go generics"}
tokens := FlatMap(sentences, func(s string) []string {
return strings.Split(s, " ")
})
// ["hello", "world", "go", "generics"]
// Zip: 名前とスコアを組にする
names := []string{"Alice", "Bob", "Charlie"}
scores := []int{90, 85, 95}
pairs := Zip(names, scores)
// [{Alice, 90}, {Bob, 85}, {Charlie, 95}]
// Partition: 合格と不合格に分ける
pass, fail := Partition(scores, func(s int) bool { return s >= 90 })
// pass: [90, 95], fail: [85]
}2. 制約(Constraints)
制約の種類
+-------------------+
| any (interface{})| ← 最も緩い: 全ての型を許容
+-------------------+
|
+-------------------+
| comparable | ← == と != が使える型
+-------------------+
|
+-------------------+
| cmp.Ordered | ← 比較演算子が使える型 (<, >, <=, >=)
+-------------------+
|
+-------------------+
| カスタム制約 | ← 特定のメソッドや型を要求
+-------------------+
コード例4: カスタム制約の定義
// メソッドベースの制約
type Stringer interface {
String() string
}
// 型集合ベースの制約(union)
type Number interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
~float32 | ~float64
}
// 整数のみの制約
type Integer interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}
// 浮動小数点のみの制約
type Float interface {
~float32 | ~float64
}
// チルダ (~) は基底型を指定
// ~int は「基底型が int である全ての型」を含む
type MyInt int // ~int に含まれる
type Score int // ~int に含まれる
// 複合制約: メソッド + 型集合
type OrderedStringer interface {
cmp.Ordered
String() string
}
// 実用例: Sum 関数
func SumT Number T {
var total T
for _, n := range nums {
total += n
}
return total
}
// 実用例: Average 関数(浮動小数点を返す)
func AverageT Number float64 {
if len(nums) == 0 {
return 0
}
var sum T
for _, n := range nums {
sum += n
}
return float64(sum) / float64(len(nums))
}
// 実用例: Abs 関数(符号付き数値)
type Signed interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~float32 | ~float64
}
func AbsT Signed T {
if v < 0 {
return -v
}
return v
}
fmt.Println(Sum([]int{1, 2, 3})) // 6
fmt.Println(Sum([]float64{1.1, 2.2})) // 3.3
fmt.Println(Average([]int{10, 20, 30})) // 20.0
fmt.Println(Abs(-42)) // 42主要な制約の比較表
| 制約 | 許容される型 | 使える演算 | 用途 |
|---|---|---|---|
any |
全ての型 | なし(インターフェース経由のみ) | コンテナ、ラッパー |
comparable |
比較可能な型 | ==, != |
マップのキー、重複排除 |
cmp.Ordered |
順序付き型 | <, >, <=, >=, == |
ソート、最大最小 |
~int | ~float64 |
指定型の基底型を持つ型 | 数値演算 | 計算、集計 |
| カスタム interface | メソッドを持つ型 | 指定メソッド | ドメイン固有のロジック |
~ (チルダ) あり/なし 比較表
| 制約定義 | int |
type MyInt int |
type Score int |
|---|---|---|---|
int |
合致 | 不一致 | 不一致 |
~int |
合致 | 合致 | 合致 |
コード例5: 複合制約の実践
// Comparable + Stringer を両方満たす制約
type ComparableStringer interface {
comparable
String() string
}
// マップのキーとして使え、文字列表現を持つ型
func PrintMapK ComparableStringer, V any {
for k, v := range m {
fmt.Printf("%s: %v\n", k.String(), v)
}
}
// 制約インターフェースの合成
type Numeric interface {
Integer | Float
}
type Addable interface {
Numeric
comparable
}
// JSON シリアライズ可能な制約
type JSONSerializable interface {
comparable
MarshalJSON() ([]byte, error)
UnmarshalJSON([]byte) error
}
// バリデーション可能な制約
type Validatable interface {
Validate() error
}
// バリデーション付きの保存関数
func SaveAllT Validatable error {
for i, item := range items {
if err := item.Validate(); err != nil {
return fmt.Errorf("item[%d]: %w", i, err)
}
}
// 保存処理...
return nil
}3. ジェネリック型
コード例6: ジェネリックなデータ構造
// スタック
type Stack[T any] struct {
items []T
}
func NewStack[T any]() *Stack[T] {
return &Stack[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
}
last := len(s.items) - 1
item := s.items[last]
s.items = s.items[:last]
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]) Len() int {
return len(s.items)
}
func (s *Stack[T]) IsEmpty() bool {
return len(s.items) == 0
}
// 使用例
intStack := NewStack[int]()
intStack.Push(1)
intStack.Push(2)
val, _ := intStack.Pop() // 2
strStack := NewStack[string]()
strStack.Push("hello")コード例7: ジェネリックなキュー
// Queue はジェネリックなFIFOキュー
type Queue[T any] struct {
items []T
}
func NewQueue[T any]() *Queue[T] {
return &Queue[T]{}
}
func (q *Queue[T]) Enqueue(item T) {
q.items = append(q.items, item)
}
func (q *Queue[T]) Dequeue() (T, bool) {
if len(q.items) == 0 {
var zero T
return zero, false
}
item := q.items[0]
q.items = q.items[1:]
return item, true
}
func (q *Queue[T]) Peek() (T, bool) {
if len(q.items) == 0 {
var zero T
return zero, false
}
return q.items[0], true
}
func (q *Queue[T]) Len() int {
return len(q.items)
}
// PriorityQueue は優先度付きキュー
type PriorityQueue[T any] struct {
items []T
less func(a, b T) bool
}
func NewPriorityQueueT any bool) *PriorityQueue[T] {
return &PriorityQueue[T]{less: less}
}
func (pq *PriorityQueue[T]) Push(item T) {
pq.items = append(pq.items, item)
pq.up(len(pq.items) - 1)
}
func (pq *PriorityQueue[T]) Pop() (T, bool) {
if len(pq.items) == 0 {
var zero T
return zero, false
}
n := len(pq.items) - 1
pq.items[0], pq.items[n] = pq.items[n], pq.items[0]
item := pq.items[n]
pq.items = pq.items[:n]
if n > 0 {
pq.down(0)
}
return item, true
}
func (pq *PriorityQueue[T]) up(j int) {
for {
i := (j - 1) / 2
if i == j || !pq.less(pq.items[j], pq.items[i]) {
break
}
pq.items[i], pq.items[j] = pq.items[j], pq.items[i]
j = i
}
}
func (pq *PriorityQueue[T]) down(i int) {
n := len(pq.items)
for {
left := 2*i + 1
if left >= n {
break
}
j := left
if right := left + 1; right < n && pq.less(pq.items[right], pq.items[left]) {
j = right
}
if !pq.less(pq.items[j], pq.items[i]) {
break
}
pq.items[i], pq.items[j] = pq.items[j], pq.items[i]
i = j
}
}
func (pq *PriorityQueue[T]) Len() int {
return len(pq.items)
}
// 使用例
pq := NewPriorityQueue(func(a, b int) bool { return a < b })
pq.Push(3)
pq.Push(1)
pq.Push(2)
val, _ := pq.Pop() // 1(最小値が先に出る)コード例8: ジェネリックな並行安全マップ
// SyncMap はジェネリックな並行安全マップ
type SyncMap[K comparable, V any] struct {
mu sync.RWMutex
m map[K]V
}
func NewSyncMap[K comparable, V any]() *SyncMap[K, V] {
return &SyncMap[K, V]{
m: make(map[K]V),
}
}
func (sm *SyncMap[K, V]) Get(key K) (V, bool) {
sm.mu.RLock()
defer sm.mu.RUnlock()
val, ok := sm.m[key]
return val, ok
}
func (sm *SyncMap[K, V]) Set(key K, value V) {
sm.mu.Lock()
defer sm.mu.Unlock()
sm.m[key] = value
}
func (sm *SyncMap[K, V]) Delete(key K) {
sm.mu.Lock()
defer sm.mu.Unlock()
delete(sm.m, key)
}
func (sm *SyncMap[K, V]) Len() int {
sm.mu.RLock()
defer sm.mu.RUnlock()
return len(sm.m)
}
func (sm *SyncMap[K, V]) Range(fn func(K, V) bool) {
sm.mu.RLock()
defer sm.mu.RUnlock()
for k, v := range sm.m {
if !fn(k, v) {
break
}
}
}
// GetOrSet は値が存在しなければ設定して返す
func (sm *SyncMap[K, V]) GetOrSet(key K, defaultVal V) V {
sm.mu.Lock()
defer sm.mu.Unlock()
if val, ok := sm.m[key]; ok {
return val
}
sm.m[key] = defaultVal
return defaultVal
}
// 使用例
cache := NewSyncMap[string, int]()
cache.Set("count", 42)
val, ok := cache.Get("count") // 42, trueコード例9: Result 型(エラーハンドリング改善)
// Result はエラーまたは値を持つ型
type Result[T any] struct {
value T
err error
}
func OkT any Result[T] {
return Result[T]{value: value}
}
func ErrT any 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, error) {
return r.value, r.err
}
func (r Result[T]) UnwrapOr(defaultVal T) T {
if r.err != nil {
return defaultVal
}
return r.value
}
func (r Result[T]) UnwrapOrElse(fn func(error) T) T {
if r.err != nil {
return fn(r.err)
}
return r.value
}
// Map: 値がある場合のみ変換を適用
func MapResultT, U any U) Result[U] {
if r.err != nil {
return ErrU
}
return Ok(f(r.value))
}
// FlatMap: 値がある場合に別のResult生成関数を適用
func FlatMapResultT, U any Result[U]) Result[U] {
if r.err != nil {
return ErrU
}
return f(r.value)
}
// Collect: Result のスライスから成功値を収集(1つでもエラーなら失敗)
func CollectT any Result[[]T] {
values := make([]T, 0, len(results))
for _, r := range results {
if r.IsErr() {
return Err[[]T](r.err)
}
values = append(values, r.value)
}
return Ok(values)
}
// 使用例
result := Ok(42)
doubled := MapResult(result, func(n int) int { return n * 2 })
val, _ := doubled.Unwrap() // 84
// チェーン
func fetchUser(id string) Result[User] {
user, err := db.FindUser(id)
if err != nil {
return ErrUser
}
return Ok(*user)
}
func getEmail(u User) Result[string] {
if u.Email == "" {
return Errstring)
}
return Ok(u.Email)
}
// Result チェーン
email := FlatMapResult(fetchUser("123"), getEmail)
fmt.Println(email.UnwrapOr("no-email@example.com"))コード例10: Optional 型
// Optional はnil安全な値コンテナ
type Optional[T any] struct {
value *T
}
func SomeT any Optional[T] {
return Optional[T]{value: &v}
}
func None[T any]() Optional[T] {
return Optional[T]{}
}
func (o Optional[T]) IsPresent() bool {
return o.value != nil
}
func (o Optional[T]) Get() (T, bool) {
if o.value == nil {
var zero T
return zero, false
}
return *o.value, true
}
func (o Optional[T]) OrElse(defaultVal T) T {
if o.value == nil {
return defaultVal
}
return *o.value
}
func (o Optional[T]) IfPresent(fn func(T)) {
if o.value != nil {
fn(*o.value)
}
}
func MapOptionalT, U any U) Optional[U] {
if o.value == nil {
return None[U]()
}
return Some(f(*o.value))
}
// 使用例
name := Some("Alice")
name.IfPresent(func(n string) {
fmt.Printf("Hello, %s!\n", n)
})
empty := None[string]()
fmt.Println(empty.OrElse("anonymous")) // "anonymous"4. 実践パターン
コード例11: ジェネリックなリポジトリパターン
type Entity interface {
GetID() string
}
type Repository[T Entity] interface {
FindByID(id string) (T, error)
FindAll() ([]T, error)
Save(entity T) error
Delete(id string) error
}
// インメモリ実装
type InMemoryRepo[T Entity] struct {
mu sync.RWMutex
store map[string]T
}
func NewInMemoryRepo[T Entity]() *InMemoryRepo[T] {
return &InMemoryRepo[T]{
store: make(map[string]T),
}
}
func (r *InMemoryRepo[T]) FindByID(id string) (T, error) {
r.mu.RLock()
defer r.mu.RUnlock()
entity, ok := r.store[id]
if !ok {
var zero T
return zero, fmt.Errorf("entity %s not found", id)
}
return entity, nil
}
func (r *InMemoryRepo[T]) FindAll() ([]T, error) {
r.mu.RLock()
defer r.mu.RUnlock()
result := make([]T, 0, len(r.store))
for _, entity := range r.store {
result = append(result, entity)
}
return result, nil
}
func (r *InMemoryRepo[T]) Save(entity T) error {
r.mu.Lock()
defer r.mu.Unlock()
r.store[entity.GetID()] = entity
return nil
}
func (r *InMemoryRepo[T]) Delete(id string) error {
r.mu.Lock()
defer r.mu.Unlock()
if _, ok := r.store[id]; !ok {
return fmt.Errorf("entity %s not found", id)
}
delete(r.store, id)
return nil
}
// FindBy は条件に一致するエンティティを検索する
func (r *InMemoryRepo[T]) FindBy(pred func(T) bool) []T {
r.mu.RLock()
defer r.mu.RUnlock()
var result []T
for _, entity := range r.store {
if pred(entity) {
result = append(result, entity)
}
}
return result
}
// 使用例
type User struct {
ID string
Name string
Age int
}
func (u User) GetID() string { return u.ID }
repo := NewInMemoryRepo[User]()
repo.Save(User{ID: "1", Name: "Alice", Age: 30})
repo.Save(User{ID: "2", Name: "Bob", Age: 25})
user, _ := repo.FindByID("1")
// 条件検索
adults := repo.FindBy(func(u User) bool { return u.Age >= 18 })コード例12: ジェネリックなページネーション
// Page はページネーション結果を表す
type Page[T any] struct {
Items []T `json:"items"`
Total int `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int `json:"total_pages"`
HasNext bool `json:"has_next"`
HasPrev bool `json:"has_prev"`
}
// Paginate はスライスをページネーションする
func PaginateT any Page[T] {
total := len(items)
totalPages := (total + pageSize - 1) / pageSize
if page < 1 {
page = 1
}
if page > totalPages && totalPages > 0 {
page = totalPages
}
start := (page - 1) * pageSize
end := start + pageSize
if start > total {
start = total
}
if end > total {
end = total
}
return Page[T]{
Items: items[start:end],
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
HasNext: page < totalPages,
HasPrev: page > 1,
}
}
// 使用例
users := getAllUsers() // []User
page := Paginate(users, 2, 10) // 2ページ目、1ページ10件
fmt.Printf("Page %d/%d, %d items\n", page.Page, page.TotalPages, len(page.Items))コード例13: ジェネリックなキャッシュ
// Cache はTTL付きのジェネリックキャッシュ
type Cache[K comparable, V any] struct {
mu sync.RWMutex
items map[K]cacheItem[V]
ttl time.Duration
maxSize int
}
type cacheItem[V any] struct {
value V
expiresAt time.Time
}
func NewCacheK comparable, V any *Cache[K, V] {
return &Cache[K, V]{
items: make(map[K]cacheItem[V]),
ttl: ttl,
maxSize: maxSize,
}
}
func (c *Cache[K, V]) Get(key K) (V, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
item, ok := c.items[key]
if !ok || time.Now().After(item.expiresAt) {
var zero V
return zero, false
}
return item.value, true
}
func (c *Cache[K, V]) Set(key K, value V) {
c.mu.Lock()
defer c.mu.Unlock()
// maxSize を超えたら期限切れのアイテムを削除
if len(c.items) >= c.maxSize {
c.evictExpired()
}
c.items[key] = cacheItem[V]{
value: value,
expiresAt: time.Now().Add(c.ttl),
}
}
func (c *Cache[K, V]) Delete(key K) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.items, key)
}
func (c *Cache[K, V]) evictExpired() {
now := time.Now()
for k, item := range c.items {
if now.After(item.expiresAt) {
delete(c.items, k)
}
}
}
// GetOrLoad はキャッシュに値がなければloader関数で取得してキャッシュする
func (c *Cache[K, V]) GetOrLoad(key K, loader func(K) (V, error)) (V, error) {
if val, ok := c.Get(key); ok {
return val, nil
}
val, err := loader(key)
if err != nil {
var zero V
return zero, err
}
c.Set(key, val)
return val, nil
}
// 使用例
userCache := NewCachestring, *User
user, err := userCache.GetOrLoad("user-123", func(id string) (*User, error) {
return db.FindUser(id)
})ジェネリクスの型推論フロー
Max(3, 7)
|
+-- コンパイラが引数の型を推論
| 3 → int, 7 → int
|
+-- T = int と決定
|
+-- Maxint として展開
|
+-- int は cmp.Ordered を満たすか? → Yes
|
+-- コンパイル成功
Max(3, 7.0)
|
+-- 3 → int, 7.0 → float64
|
+-- 型が一致しない → コンパイルエラー
|
+-- 修正: Max(float64(3), 7.0) または Maxfloat64
5. 標準ライブラリのジェネリック関数
コード例14: slices パッケージ
import "slices"
// ソート
nums := []int{3, 1, 4, 1, 5, 9, 2, 6}
slices.Sort(nums) // [1, 1, 2, 3, 4, 5, 6, 9]
// カスタムソート
type User struct {
Name string
Age int
}
users := []User{{"Charlie", 30}, {"Alice", 25}, {"Bob", 35}}
slices.SortFunc(users, func(a, b User) int {
return cmp.Compare(a.Age, b.Age)
})
// [{Alice 25}, {Charlie 30}, {Bob 35}]
// 安定ソート(同じキーの要素の順序を保持)
slices.SortStableFunc(users, func(a, b User) int {
return cmp.Compare(a.Name, b.Name)
})
// 二分探索
sorted := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
idx, found := slices.BinarySearch(sorted, 5) // 4, true
// 含有チェック
slices.Contains([]string{"a", "b", "c"}, "b") // true
// 最大・最小
slices.Max([]int{3, 1, 4, 1, 5}) // 5
slices.Min([]int{3, 1, 4, 1, 5}) // 1
// コンパクト(連続する重複を除去)
nums = []int{1, 1, 2, 3, 3, 3, 4}
slices.Compact(nums) // [1, 2, 3, 4]
// リバース
slices.Reverse([]int{1, 2, 3}) // [3, 2, 1]
// インデックス検索
slices.Index([]string{"a", "b", "c"}, "b") // 1
// 等値比較
slices.Equal([]int{1, 2, 3}, []int{1, 2, 3}) // true
// クローン
original := []int{1, 2, 3}
cloned := slices.Clone(original) // ディープコピーコード例15: maps パッケージ
import "maps"
m := map[string]int{"a": 1, "b": 2, "c": 3}
// キー一覧
keys := maps.Keys(m) // イテレータを返す(Go 1.23+)
// 値一覧
values := maps.Values(m) // イテレータを返す
// クローン
cloned := maps.Clone(m) // 浅いコピー
// 等値比較
maps.Equal(m, cloned) // true
// コピー(dstにsrcをマージ)
dst := map[string]int{"a": 10, "d": 4}
maps.Copy(dst, m) // dst = {"a": 1, "b": 2, "c": 3, "d": 4}
// 条件による削除
maps.DeleteFunc(m, func(k string, v int) bool {
return v < 2
})
// m = {"b": 2, "c": 3}コード例16: cmp パッケージ
import "cmp"
// 比較
cmp.Compare(1, 2) // -1
cmp.Compare(2, 2) // 0
cmp.Compare(3, 2) // 1
// ゼロ値チェック
cmp.Or(0, 42) // 42(最初の非ゼロ値)
cmp.Or("", "default") // "default"
cmp.Or("hello", "default") // "hello"
// 複数フォールバック
cmp.Or("", "", "fallback") // "fallback"
// ソートキーの合成
type Employee struct {
Department string
Name string
Salary int
}
employees := []Employee{...}
slices.SortFunc(employees, func(a, b Employee) int {
// まず部門でソート、同じなら名前でソート
if c := cmp.Compare(a.Department, b.Department); c != 0 {
return c
}
return cmp.Compare(a.Name, b.Name)
})6. パフォーマンス特性
GCShape Stenciling
+----------------------------------------------------------+
| Go ジェネリクスのコンパイル戦略 |
+----------------------------------------------------------+
| |
| func MaxT cmp.Ordered T |
| |
| コンパイル時: |
| +-------------------+ +-------------------+ |
| | ポインタ型 | | 値型 | |
| | (*User, *string等)| | (int, float64等) | |
| | → 共通の実装を共有 | | → 型ごとに特殊化 | |
| +-------------------+ +-------------------+ |
| |
| GCShape = 同じメモリレイアウトの型は同じ実装を共有 |
| → コードサイズの爆発を防ぐ |
| → ポインタ型はすべて同じ shape |
+----------------------------------------------------------+
コード例17: ベンチマークによるパフォーマンス比較
// インターフェース版
func SumInterface(nums []interface{}) int {
sum := 0
for _, n := range nums {
sum += n.(int)
}
return sum
}
// ジェネリック版
func SumGenericT Number T {
var sum T
for _, n := range nums {
sum += n
}
return sum
}
// 具体型版
func SumInt(nums []int) int {
sum := 0
for _, n := range nums {
sum += n
}
return sum
}
// ベンチマーク
func BenchmarkSumInterface(b *testing.B) {
nums := make([]interface{}, 1000)
for i := range nums { nums[i] = i }
b.ResetTimer()
for i := 0; i < b.N; i++ {
SumInterface(nums)
}
}
func BenchmarkSumGeneric(b *testing.B) {
nums := make([]int, 1000)
for i := range nums { nums[i] = i }
b.ResetTimer()
for i := 0; i < b.N; i++ {
SumGeneric(nums)
}
}
func BenchmarkSumConcrete(b *testing.B) {
nums := make([]int, 1000)
for i := range nums { nums[i] = i }
b.ResetTimer()
for i := 0; i < b.N; i++ {
SumInt(nums)
}
}
// 典型的な結果:
// BenchmarkSumInterface-8 500000 2800 ns/op 0 B/op 0 allocs/op
// BenchmarkSumGeneric-8 2000000 600 ns/op 0 B/op 0 allocs/op
// BenchmarkSumConcrete-8 2000000 580 ns/op 0 B/op 0 allocs/op
// → ジェネリクスは具体型とほぼ同等、interfaceより大幅に高速7. ジェネリクスの適用判断
ジェネリクスを使うべき場面
+----------------------------------------------------------+
| ジェネリクスの適用判断フロー |
+----------------------------------------------------------+
| |
| 同じロジックを異なる型に適用したい? |
| | |
| +-- YES → 型パラメータが2つ以上の具体型で使われる? |
| | | |
| | +-- YES → ジェネリクスが適切 |
| | +-- NO → 具体型を直接使う |
| | |
| +-- NO → 異なる実装を同じ振る舞いに抽象化したい? |
| | |
| +-- YES → インターフェースが適切 |
| +-- NO → ジェネリクスは不要 |
+----------------------------------------------------------+
| 場面 | 推奨 | 理由 |
|---|---|---|
| コレクション操作(Map, Filter, Reduce) | ジェネリクス | 同じアルゴリズムを全ての型に適用 |
| データ構造(Stack, Queue, Tree) | ジェネリクス | 型安全なコンテナ |
| DB接続の抽象化 | インターフェース | 実装が異なる(MySQL vs PostgreSQL) |
| HTTPハンドラ | インターフェース | http.Handler パターン |
| ソートアルゴリズム | ジェネリクス | 比較可能な全ての型に対応 |
| ロガー | インターフェース | 出力先が異なる |
fmt.Println(v any) のような関数 |
any 引数 |
ジェネリクスは不要 |
8. アンチパターン
アンチパターン1: 不要なジェネリクス化
// NG: ジェネリクスが不要なケース
func PrintValueT any {
fmt.Println(v) // any なら interface{} で十分
}
// OK: interface{} または any を直接使う
func PrintValue(v any) {
fmt.Println(v)
}
// NG: 型パラメータが1つの具体型にしか使われない
func ParseUserJSONT User (T, error) {
var result T
err := json.Unmarshal(data, &result)
return result, err
}
// OK: 具体型を直接使う
func ParseUserJSON(data []byte) (User, error) {
var user User
err := json.Unmarshal(data, &user)
return user, err
}アンチパターン2: 過度に複雑な制約
// NG: 制約が複雑すぎて可読性が低い
type ComplexConstraint[K comparable, V interface {
~int | ~string
fmt.Stringer
encoding.BinaryMarshaler
}] struct {
data map[K]V
}
// OK: 制約を分離して名前をつける
type Serializable interface {
fmt.Stringer
encoding.BinaryMarshaler
}
type ValueType interface {
~int | ~string
Serializable
}
type Store[K comparable, V ValueType] struct {
data map[K]V
}アンチパターン3: ジェネリクスで多態性を実現しようとする
// NG: ジェネリクスで振る舞いの切り替え
func ProcessT Animal string {
// T の具体型によって処理を変えたい
// → ジェネリクスでは型に基づくディスパッチはできない
}
// OK: インターフェースを使う
type Animal interface {
Speak() string
}
func Process(a Animal) string {
return a.Speak()
}アンチパターン4: ゼロ値の誤った扱い
// NG: ジェネリックなゼロ値チェック
func IsZeroT any bool {
// any にはゼロ値比較の演算がない → コンパイルエラー
return v == T{} // 不可
}
// OK: comparable 制約を使う
func IsZeroT comparable bool {
var zero T
return v == zero
}
// OK: reflect を使う(any の場合)
func IsZeroAny(v any) bool {
return reflect.ValueOf(v).IsZero()
}実践演習
演習1: 基本的な実装
以下の要件を満たすコードを実装してください。
要件:
- 入力データの検証を行うこと
- エラーハンドリングを適切に実装すること
- テストコードも作成すること
# 演習1: 基本実装のテンプレート
class Exercise1:
"""基本的な実装パターンの演習"""
def __init__(self):
self.data = []
def validate_input(self, value):
"""入力値の検証"""
if value is None:
raise ValueError("入力値がNoneです")
return True
def process(self, value):
"""データ処理のメインロジック"""
self.validate_input(value)
self.data.append(value)
return self.data
def get_results(self):
"""処理結果の取得"""
return {
'count': len(self.data),
'data': self.data
}
# テスト
def test_exercise1():
ex = Exercise1()
assert ex.process(1) == [1]
assert ex.process(2) == [1, 2]
assert ex.get_results()['count'] == 2
try:
ex.process(None)
assert False, "例外が発生するべき"
except ValueError:
pass
print("全テスト合格!")
test_exercise1()演習2: 応用パターン
基本実装を拡張して、以下の機能を追加してください。
# 演習2: 応用パターン
from typing import List, Dict, Optional
from datetime import datetime
class AdvancedExercise:
"""応用パターンの演習"""
def __init__(self, max_size: int = 100):
self._items: List[Dict] = []
self._max_size = max_size
self._created_at = datetime.now()
def add(self, key: str, value: any) -> bool:
"""アイテムの追加(サイズ制限付き)"""
if len(self._items) >= self._max_size:
return False
self._items.append({
'key': key,
'value': value,
'timestamp': datetime.now().isoformat()
})
return True
def find(self, key: str) -> Optional[Dict]:
"""キーによる検索"""
for item in reversed(self._items):
if item['key'] == key:
return item
return None
def remove(self, key: str) -> bool:
"""キーによる削除"""
for i, item in enumerate(self._items):
if item['key'] == key:
self._items.pop(i)
return True
return False
def stats(self) -> Dict:
"""統計情報"""
return {
'total_items': len(self._items),
'max_size': self._max_size,
'usage_percent': len(self._items) / self._max_size * 100,
'uptime': str(datetime.now() - self._created_at)
}
# テスト
def test_advanced():
ex = AdvancedExercise(max_size=3)
assert ex.add("a", 1) == True
assert ex.add("b", 2) == True
assert ex.add("c", 3) == True
assert ex.add("d", 4) == False # サイズ制限
assert ex.find("b")['value'] == 2
assert ex.remove("b") == True
assert ex.find("b") is None
stats = ex.stats()
assert stats['total_items'] == 2
print("応用テスト全合格!")
test_advanced()演習3: パフォーマンス最適化
以下のコードのパフォーマンスを改善してください。
# 演習3: パフォーマンス最適化
import time
from functools import lru_cache
# 最適化前(O(n^2))
def slow_search(data: list, target: int) -> int:
"""非効率な検索"""
for i in range(len(data)):
for j in range(i + 1, len(data)):
if data[i] + data[j] == target:
return (i, j)
return (-1, -1)
# 最適化後(O(n))
def fast_search(data: list, target: int) -> tuple:
"""ハッシュマップを使った効率的な検索"""
seen = {}
for i, num in enumerate(data):
complement = target - num
if complement in seen:
return (seen[complement], i)
seen[num] = i
return (-1, -1)
# ベンチマーク
def benchmark():
import random
data = list(range(5000))
random.shuffle(data)
target = data[100] + data[4000]
start = time.time()
result1 = slow_search(data, target)
slow_time = time.time() - start
start = time.time()
result2 = fast_search(data, target)
fast_time = time.time() - start
print(f"非効率版: {slow_time:.4f}秒")
print(f"効率版: {fast_time:.6f}秒")
print(f"高速化率: {slow_time/fast_time:.0f}倍")
benchmark()ポイント:
- アルゴリズムの計算量を意識する
- 適切なデータ構造を選択する
- ベンチマークで効果を測定する
トラブルシューティング
よくあるエラーと解決策
| エラー | 原因 | 解決策 |
|---|---|---|
| 初期化エラー | 設定ファイルの不備 | 設定ファイルのパスと形式を確認 |
| タイムアウト | ネットワーク遅延/リソース不足 | タイムアウト値の調整、リトライ処理の追加 |
| メモリ不足 | データ量の増大 | バッチ処理の導入、ページネーションの実装 |
| 権限エラー | アクセス権限の不足 | 実行ユーザーの権限確認、設定の見直し |
| データ不整合 | 並行処理の競合 | ロック機構の導入、トランザクション管理 |
デバッグの手順
- エラーメッセージの確認: スタックトレースを読み、発生箇所を特定する
- 再現手順の確立: 最小限のコードでエラーを再現する
- 仮説の立案: 考えられる原因をリストアップする
- 段階的な検証: ログ出力やデバッガを使って仮説を検証する
- 修正と回帰テスト: 修正後、関連する箇所のテストも実行する
# デバッグ用ユーティリティ
import logging
import traceback
from functools import wraps
# ロガーの設定
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s [%(levelname)s] %(name)s: %(message)s'
)
logger = logging.getLogger(__name__)
def debug_decorator(func):
"""関数の入出力をログ出力するデコレータ"""
@wraps(func)
def wrapper(*args, **kwargs):
logger.debug(f"呼び出し: {func.__name__}(args={args}, kwargs={kwargs})")
try:
result = func(*args, **kwargs)
logger.debug(f"戻り値: {func.__name__} -> {result}")
return result
except Exception as e:
logger.error(f"例外発生: {func.__name__}: {e}")
logger.error(traceback.format_exc())
raise
return wrapper
@debug_decorator
def process_data(items):
"""データ処理(デバッグ対象)"""
if not items:
raise ValueError("空のデータ")
return [item * 2 for item in items]パフォーマンス問題の診断
パフォーマンス問題が発生した場合の診断手順:
- ボトルネックの特定: プロファイリングツールで計測
- メモリ使用量の確認: メモリリークの有無をチェック
- I/O待ちの確認: ディスクやネットワークI/Oの状況を確認
- 同時接続数の確認: コネクションプールの状態を確認
| 問題の種類 | 診断ツール | 対策 |
|---|---|---|
| CPU負荷 | cProfile, py-spy | アルゴリズム改善、並列化 |
| メモリリーク | tracemalloc, objgraph | 参照の適切な解放 |
| I/Oボトルネック | strace, iostat | 非同期I/O、キャッシュ |
| DB遅延 | EXPLAIN, slow query log | インデックス、クエリ最適化 |
設計判断ガイド
選択基準マトリクス
技術選択を行う際の判断基準を以下にまとめます。
| 判断基準 | 重視する場合 | 妥協できる場合 |
|---|---|---|
| パフォーマンス | リアルタイム処理、大規模データ | 管理画面、バッチ処理 |
| 保守性 | 長期運用、チーム開発 | プロトタイプ、短期プロジェクト |
| スケーラビリティ | 成長が見込まれるサービス | 社内ツール、固定ユーザー |
| セキュリティ | 個人情報、金融データ | 公開データ、社内利用 |
| 開発速度 | MVP、市場投入スピード | 品質重視、ミッションクリティカル |
アーキテクチャパターンの選択
| アーキテクチャ選択フロー |
|---|
| ① チーム規模は? |
| ├─ 小規模(1-5人)→ モノリス |
| └─ 大規模(10人+)→ ②へ |
| ② デプロイ頻度は? |
| ├─ 週1回以下 → モノリス + モジュール分割 |
| └─ 毎日/複数回 → ③へ |
| ③ チーム間の独立性は? |
| ├─ 高い → マイクロサービス |
| └─ 中程度 → モジュラーモノリス |
トレードオフの分析
技術的な判断には必ずトレードオフが伴います。以下の観点で分析を行いましょう:
1. 短期 vs 長期のコスト
- 短期的に速い方法が長期的には技術的負債になることがある
- 逆に、過剰な設計は短期的なコストが高く、プロジェクトの遅延を招く
2. 一貫性 vs 柔軟性
- 統一された技術スタックは学習コストが低い
- 多様な技術の採用は適材適所が可能だが、運用コストが増加
3. 抽象化のレベル
- 高い抽象化は再利用性が高いが、デバッグが困難になる場合がある
- 低い抽象化は直感的だが、コードの重複が発生しやすい
# 設計判断の記録テンプレート
class ArchitectureDecisionRecord:
"""ADR (Architecture Decision Record) の作成"""
def __init__(self, title: str):
self.title = title
self.context = ""
self.decision = ""
self.consequences = []
self.alternatives = []
def set_context(self, context: str):
"""背景と課題の記述"""
self.context = context
return self
def set_decision(self, decision: str):
"""決定内容の記述"""
self.decision = decision
return self
def add_consequence(self, consequence: str, positive: bool = True):
"""結果の追加"""
self.consequences.append({
'description': consequence,
'type': 'positive' if positive else 'negative'
})
return self
def add_alternative(self, name: str, reason_rejected: str):
"""却下した代替案の追加"""
self.alternatives.append({
'name': name,
'reason_rejected': reason_rejected
})
return self
def to_markdown(self) -> str:
"""Markdown形式で出力"""
md = f"# ADR: {self.title}\n\n"
md += f"## 背景\n{self.context}\n\n"
md += f"## 決定\n{self.decision}\n\n"
md += "## 結果\n"
for c in self.consequences:
icon = "✅" if c['type'] == 'positive' else "⚠️"
md += f"- {icon} {c['description']}\n"
md += "\n## 却下した代替案\n"
for a in self.alternatives:
md += f"- **{a['name']}**: {a['reason_rejected']}\n"
return mdFAQ
Q1. ジェネリクスとインターフェースの使い分けは?
ジェネリクスは「同じアルゴリズムを異なる型に適用する」場合に使う。インターフェースは「異なる実装を同じ振る舞いとして抽象化する」場合に使う。例えば、ソートアルゴリズムはジェネリクス向き。一方、データベース接続のような多態性はインターフェース向き。
Q2. ジェネリクスはパフォーマンスに影響するか?
Go のジェネリクスはコンパイル時にGCShape stenciling(形状ベースの特殊化)を行う。ポインタ型は共通の実装を共有し、値型は必要に応じて特殊化される。大半のケースでインターフェース経由の呼び出しより高速または同等。
Q3. Go 1.18以降、標準ライブラリでジェネリクスはどう使われている?
slices パッケージ(ソート、検索、比較)、maps パッケージ(キー取得、値取得、クローン)、cmp パッケージ(比較関数)が追加された。sync.Map のジェネリクス版は標準ライブラリにはないが、サードパーティで提供されている。
Q4. 型パラメータにメソッドを定義できるか?
型パラメータ自体にはメソッドを定義できない。ただし、ジェネリック型(例: Stack[T any])にはメソッドを定義可能。メソッドの型パラメータは型定義で宣言されたものを使い、メソッド宣言で新しい型パラメータを追加することはできない。
type Stack[T any] struct { items []T }
// OK: 型定義の T を使う
func (s *Stack[T]) Push(item T) { ... }
// NG: メソッドに新しい型パラメータを追加
func (s *Stack[T]) MapU any U) *Stack[U] { ... } // コンパイルエラー
// OK: 関数として定義する
func MapStackT, U any U) *Stack[U] { ... }Q5. ジェネリクスと reflect の使い分けは?
ジェネリクスはコンパイル時の型安全性を保証し、パフォーマンスも良好。reflect はランタイムの型情報にアクセスでき柔軟性が高いが、型安全性がなくパフォーマンスも劣る。原則としてジェネリクスで解決できる場合はジェネリクスを使い、JSON マーシャリングやORM のようにランタイムで型を動的に扱う必要がある場合のみ reflect を使う。
// ジェネリクスで解決できるケース → ジェネリクスを使う
func ContainsT comparable bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}
// reflect が必要なケース → 構造体のフィールドを動的に走査
func StructToMap(v any) map[string]any {
result := make(map[string]any)
val := reflect.ValueOf(v)
typ := val.Type()
for i := 0; i < val.NumField(); i++ {
field := typ.Field(i)
if field.IsExported() {
result[field.Name] = val.Field(i).Interface()
}
}
return result
}Q6. ジェネリクスで再帰的な型制約は可能か?
Go 1.18時点では直接的な再帰制約はサポートされていないが、間接的に実現可能。
// 自己参照型のパターン
type Comparable[T any] interface {
CompareTo(other T) int
}
// 使用例
type MyString string
func (s MyString) CompareTo(other MyString) int {
return strings.Compare(string(s), string(other))
}
func Sort[T Comparable[T]](items []T) {
slices.SortFunc(items, func(a, b T) int {
return a.CompareTo(b)
})
}Q7. union 型制約内のメソッドは呼び出せるか?
union 型(int | string など)はメソッドを持たないため、union 型制約のみではメソッド呼び出しはできない。メソッドを呼び出したい場合は、インターフェースメソッドを制約に追加する必要がある。
// NG: union 型にはメソッドがない
type Numeric interface {
~int | ~float64
}
func DoubleT Numeric string {
return v.String() // コンパイルエラー: String() は定義されていない
}
// OK: メソッドを制約に含める
type StringableNumeric interface {
~int | ~float64
String() string
}Q8. ジェネリクスでイテレータパターンは実現できるか?
Go 1.23 以降の range over function(レンジ関数)と組み合わせることで、型安全なイテレータを実装できる。
// iter.Seq を使ったジェネリックイテレータ(Go 1.23+)
func FilterT any bool) iter.Seq[T] {
return func(yield func(T) bool) {
for v := range seq {
if predicate(v) {
if !yield(v) {
return
}
}
}
}
}
func MapT, U any U) iter.Seq[U] {
return func(yield func(U) bool) {
for v := range seq {
if !yield(transform(v)) {
return
}
}
}
}
// 使用例
numbers := slices.Values([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
evens := Filter(numbers, func(n int) bool { return n%2 == 0 })
doubled := Map(evens, func(n int) int { return n * 2 })
for v := range doubled {
fmt.Println(v) // 4, 8, 12, 16, 20
}まとめ
| 概念 | 要点 |
|---|---|
型パラメータ [T ...] |
関数・型を複数の型に対して一般化 |
any |
全ての型を許容する制約(= interface{}) |
comparable |
== / != が使える型の制約 |
cmp.Ordered |
比較演算子が使える型の制約 |
~T (チルダ) |
基底型が T である全ての型を含む |
| 型推論 | 引数から型パラメータを自動推論 |
| ゼロ値 | var zero T でジェネリック型のゼロ値を取得 |
slices / maps |
標準ライブラリのジェネリックユーティリティ |
| Result / Optional | エラーハンドリング・nil安全のジェネリック型 |
| GCShape stenciling | コンパイル時の型特殊化戦略 |
次に読むべきガイド
- 03-tools/02-profiling.md — プロファイリング:pprof、trace
- 03-tools/04-best-practices.md — ベストプラクティス:Effective Go
- 03-tools/00-cli-development.md — CLI開発:cobra、flag、promptui
参考文献
- Go公式 — Type Parameters Proposal https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md
- Go公式 — Tutorial: Getting started with generics https://go.dev/doc/tutorial/generics
- Go Blog — An Introduction To Generics https://go.dev/blog/intro-generics
- Go標準ライブラリ — slices パッケージ https://pkg.go.dev/slices
- Go標準ライブラリ — maps パッケージ https://pkg.go.dev/maps
- Go標準ライブラリ — cmp パッケージ https://pkg.go.dev/cmp