Go/cap

capとは

capはGoの事前宣言された組み込み関数で、スライス、配列、チャネルの容量(キャパシティ)を返します。

func cap(v Type) int

対応する型:

  • 配列: 配列の長さ(lenと同じ)
  • スライス: 底層配列の容量
  • チャネル: チャネルバッファの容量

基本的な使い方

1. 配列での使用

arr := [5]int{1, 2, 3, 4, 5}
fmt.Println(cap(arr))  // 5
fmt.Println(len(arr))  // 5(配列では常にcap == len)

2. スライスでの使用

s := make([]int, 3, 10)  // 長さ3、容量10
fmt.Println(len(s))     // 3
fmt.Println(cap(s))     // 10

3. チャネルでの使用

ch := make(chan int, 5)  // バッファサイズ5
fmt.Println(cap(ch))     // 5

unbuffered := make(chan int)
fmt.Println(cap(unbuffered))  // 0

他のキーワード・識別子との組み合わせ

1. lenとの組み合わせ

func printSliceInfo(s []int) {
    fmt.Printf("Length: %d, Capacity: %d\n", len(s), cap(s))
    fmt.Printf("Available space: %d\n", cap(s)-len(s))
}

s := make([]int, 5, 10)
printSliceInfo(s)  // Length: 5, Capacity: 10, Available space: 5

2. makeとの組み合わせ

// スライスの作成時にキャパシティを指定
s1 := make([]int, 5)       // len=5, cap=5
s2 := make([]int, 5, 10)   // len=5, cap=10
s3 := make([]int, 0, 10)   // len=0, cap=10

// チャネルの作成時にバッファサイズを指定
ch1 := make(chan int)      // cap=0(アンバッファード)
ch2 := make(chan int, 10)  // cap=10(バッファード)

3. appendとの組み合わせ

func demonstrateCapacityGrowth() {
    s := make([]int, 0, 2)
    fmt.Printf("Initial: len=%d cap=%d\n", len(s), cap(s))
    
    for i := 0; i < 10; i++ {
        s = append(s, i)
        fmt.Printf("After append %d: len=%d cap=%d\n", 
                   i, len(s), cap(s))
    }
}

// 出力例:
// Initial: len=0 cap=2
// After append 0: len=1 cap=2
// After append 1: len=2 cap=2
// After append 2: len=3 cap=4  // 容量が拡張された
// After append 3: len=4 cap=4
// After append 4: len=5 cap=8  // 再び拡張

4. スライスの再スライシングとの組み合わせ

original := make([]int, 5, 10)
fmt.Printf("Original: len=%d cap=%d\n", len(original), cap(original))

// スライスの一部を取得
sub := original[1:3]
fmt.Printf("Sub-slice: len=%d cap=%d\n", len(sub), cap(sub))  // len=2 cap=9

// 元のスライスからのオフセットで容量が決まる
sub2 := original[2:]
fmt.Printf("Sub-slice2: len=%d cap=%d\n", len(sub2), cap(sub2))  // len=3 cap=8

5. copyとの組み合わせ

func efficientCopy(src []int) []int {
    // 必要最小限のキャパシティで新しいスライスを作成
    dst := make([]int, len(src), cap(src))
    copy(dst, src)
    return dst
}

// キャパシティを考慮したコピー
func copyWithCapacity(src []int) []int {
    if cap(src) > len(src)*2 {
        // 容量が過剰な場合は最適化
        dst := make([]int, len(src))
        copy(dst, src)
        return dst
    }
    return src
}

6. 条件文との組み合わせ

func needsReallocation(s []int, additionalItems int) bool {
    return len(s)+additionalItems > cap(s)
}

func smartAppend(s []int, items ...int) []int {
    if needsReallocation(s, len(items)) {
        // 事前に適切なキャパシティで再割り当て
        newCap := len(s) + len(items)
        if newCap < cap(s)*2 {
            newCap = cap(s) * 2
        }
        newSlice := make([]int, len(s), newCap)
        copy(newSlice, s)
        s = newSlice
    }
    return append(s, items...)
}

7. rangeとの組み合わせ

func analyzeSlices(slices [][]int) {
    for i, s := range slices {
        utilization := float64(len(s)) / float64(cap(s)) * 100
        fmt.Printf("Slice %d: len=%d cap=%d utilization=%.1f%%\n",
                   i, len(s), cap(s), utilization)
    }
}

8. deferとの組み合わせ

func monitorCapacity(s *[]int) {
    originalCap := cap(*s)
    defer func() {
        if cap(*s) != originalCap {
            fmt.Printf("Capacity changed from %d to %d\n", 
                       originalCap, cap(*s))
        }
    }()
    
    // スライスの操作
    *s = append(*s, 1, 2, 3, 4, 5)
}

実用的なユースケース

1. メモリ使用量の最適化

func optimizeSliceMemory(s []int) []int {
    utilization := float64(len(s)) / float64(cap(s))
    
    // 使用率が50%未満なら再パック
    if utilization < 0.5 && cap(s) > 100 {
        optimized := make([]int, len(s))
        copy(optimized, s)
        return optimized
    }
    return s
}

2. 容量制限チェック

type LimitedSlice struct {
    data     []int
    maxCap   int
}

func NewLimitedSlice(maxCap int) *LimitedSlice {
    return &LimitedSlice{
        data:   make([]int, 0, min(maxCap, 10)),
        maxCap: maxCap,
    }
}

func (ls *LimitedSlice) Append(value int) error {
    if len(ls.data) >= ls.maxCap {
        return errors.New("capacity limit exceeded")
    }
    
    // 現在の容量が不足しているが、制限内で拡張可能
    if len(ls.data) == cap(ls.data) && cap(ls.data) < ls.maxCap {
        newCap := min(cap(ls.data)*2, ls.maxCap)
        newData := make([]int, len(ls.data), newCap)
        copy(newData, ls.data)
        ls.data = newData
    }
    
    ls.data = append(ls.data, value)
    return nil
}

3. パフォーマンス測定

import "time"

func benchmarkAppendStrategies() {
    size := 1000000
    
    // 戦略1: 容量を事前設定しない
    start := time.Now()
    var s1 []int
    for i := 0; i < size; i++ {
        s1 = append(s1, i)
    }
    duration1 := time.Since(start)
    
    // 戦略2: 容量を事前設定
    start = time.Now()
    s2 := make([]int, 0, size)
    for i := 0; i < size; i++ {
        s2 = append(s2, i)
    }
    duration2 := time.Since(start)
    
    fmt.Printf("Without pre-allocation: %v (final cap: %d)\n", 
               duration1, cap(s1))
    fmt.Printf("With pre-allocation: %v (final cap: %d)\n", 
               duration2, cap(s2))
}

4. 動的バッファの実装

type DynamicBuffer struct {
    data     []byte
    maxCap   int
    growBy   int
}

func NewDynamicBuffer(initial, maxCap, growBy int) *DynamicBuffer {
    return &DynamicBuffer{
        data:   make([]byte, 0, initial),
        maxCap: maxCap,
        growBy: growBy,
    }
}

func (db *DynamicBuffer) Write(p []byte) (n int, err error) {
    needed := len(db.data) + len(p)
    
    if needed > cap(db.data) {
        if needed > db.maxCap {
            return 0, errors.New("buffer capacity exceeded")
        }
        
        newCap := cap(db.data) + db.growBy
        if newCap > db.maxCap {
            newCap = db.maxCap
        }
        if newCap < needed {
            newCap = needed
        }
        
        newData := make([]byte, len(db.data), newCap)
        copy(newData, db.data)
        db.data = newData
    }
    
    db.data = append(db.data, p...)
    return len(p), nil
}

func (db *DynamicBuffer) Info() string {
    return fmt.Sprintf("len=%d cap=%d maxCap=%d", 
                       len(db.data), cap(db.data), db.maxCap)
}

5. スライスプールの実装

import "sync"

type SlicePool struct {
    pool sync.Pool
    cap  int
}

func NewSlicePool(capacity int) *SlicePool {
    return &SlicePool{
        pool: sync.Pool{
            New: func() interface{} {
                return make([]int, 0, capacity)
            },
        },
        cap: capacity,
    }
}

func (sp *SlicePool) Get() []int {
    s := sp.pool.Get().([]int)
    return s[:0]  // 長さを0にリセット
}

func (sp *SlicePool) Put(s []int) {
    if cap(s) == sp.cap {  // 元の容量のもののみプールに戻す
        sp.pool.Put(s)
    }
}

6. チャネル容量の監視

func monitorChannelCapacity(ch chan int, name string) {
    go func() {
        for {
            time.Sleep(time.Second)
            fmt.Printf("%s: len=%d cap=%d\n", 
                       name, len(ch), cap(ch))
        }
    }()
}

// 使用例
ch := make(chan int, 10)
monitorChannelCapacity(ch, "WorkQueue")

// チャネルの使用
go func() {
    for i := 0; i < 20; i++ {
        ch <- i
        time.Sleep(500 * time.Millisecond)
    }
}()

7. 容量を考慮した安全なスライス操作

func SafeAppend(slice []int, maxCap int, values ...int) ([]int, error) {
    needed := len(slice) + len(values)
    if needed > maxCap {
        return slice, fmt.Errorf("would exceed max capacity %d", maxCap)
    }
    
    if needed > cap(slice) {
        // 新しい容量を計算(maxCapを超えない)
        newCap := cap(slice) * 2
        if newCap > maxCap {
            newCap = maxCap
        }
        if newCap < needed {
            newCap = needed
        }
        
        newSlice := make([]int, len(slice), newCap)
        copy(newSlice, slice)
        slice = newSlice
    }
    
    return append(slice, values...), nil
}

重要なポイント

  1. メモリ効率: cap()を使って実際の容量を把握し、無駄なメモリ使用を避ける
  2. パフォーマンス: 頻繁な再割り当てを避けるため、適切な初期容量を設定
  3. スライシング: スライスを再スライシングした際の容量の変化を理解する
  4. チャネル: バッファードチャネルの容量管理
  5. 配列: 配列ではcap() == len()が常に成り立つ

cap()関数は、Goでメモリ効率的なプログラムを書く際に不可欠な関数であり、特にスライスとチャネルの動作を理解し制御するのに重要な役割を果たします。

カテゴリ:Go
カテゴリ:Go