備忘録として、個人用に残してます。
Go言語のスライスと配列:重要な注意点
基本概念
配列 (Array)
- 固定長のデータ構造
- サイズはコンパイル時に決定
- 値型(関数に渡すとコピーが作成される)
arr := [3]int{1, 2, 3} // 要素数3の配列
スライス (Slice)
- 可変長のデータ構造
- 配列への「ビュー」として機能
- 参照型(関数に渡すと元データを参照)
slice := []int{1, 2, 3} // スライス
スライスの重要な特性
1. 可変長引数としての使用
- 関数の最後の引数のみ可変長(
...T
)に可能 - スライスを展開して渡すには
...
を使用
正しい使用例:
func sum(nums ...int) int {
total := 0
for _, num := range nums {
total += num
}
return total
}
nums := []int{1, 2, 3}
sum(nums...) // スライスを展開
不正な例(コンパイルエラー):
func invalid(a ...int, b bool) {} // 可変長が最後でない
func invalid(a ...int, b ...int) {} // 可変長引数が複数
2. 参照型としての振る舞い
- スライスを関数に渡すと、元のデータを変更可能
func modify(s []int) {
s[0] = 100 // 元のスライスを変更
}
func main() {
s := []int{1, 2, 3}
modify(s)
fmt.Println(s) // [100 2 3]
}
3. 配列との比較
- 配列は値型(関数に渡すとコピーが作成される)
func modifyArray(a [3]int) {
a[0] = 100 // コピーを変更
}
func main() {
a := [3]int{1, 2, 3}
modifyArray(a)
fmt.Println(a) // [1 2 3](変更されない)
}
注意すべき操作
1. スライスの初期化
// 長さ0、容量10のスライス
s := make([]int, 0, 10)
// nilスライス
var nilSlice []int
// 空スライス
emptySlice := []int{}
2. スライスのコピー
- 単純な代入は参照をコピーするだけ
- 完全なコピーには
copy
関数を使用
// 参照のコピー(危険)
s1 := []int{1, 2, 3}
s2 := s1
s2[0] = 100 // s1も変更される
// 安全なコピー
s3 := make([]int, len(s1))
copy(s3, s1)
s3[0] = 200 // s1は変更されない
3. スライスの拡張
-
append
使用時には新しいスライスが返される可能性
s := make([]int, 2, 3) // len=2, cap=3
s = append(s, 1) // 容量内
s = append(s, 2) // 容量超過→新しい配列が割り当て
ベストプラクティス
-
関数の引数:
- 元データを変更したくない場合 → コピーを作成
- 大きなデータを扱う場合 → スライス(コピーコスト回避)
-
スライスの容量:
- 要素数が事前にわかっている場合 →
make
で初期容量を指定
- 要素数が事前にわかっている場合 →
-
nilチェック:
- スライスがnilかどうか確認する必要がある場合
if slice == nil { // nilスライスの処理 }
-
空スライス表現:
- 明確に空を表現したい場合 →
[]int{}
を使用
- 明確に空を表現したい場合 →
メモリ管理に関する注意点
1. メモリリークの危険性
スライスが大きな配列の一部を参照し続けると、GCが働かずメモリリークの原因に:
func getBigSlice() []byte {
bigData := make([]byte, 1000000)
return bigData[:10] // 10要素だけ返すが、背後で全体を保持
}
対策:必要な部分だけコピーして返す
func getSafeSlice() []byte {
bigData := make([]byte, 1000000)
smallData := make([]byte, 10)
copy(smallData, bigData[:10])
return smallData
}
2. 容量拡張の挙動
こちらは、変更の可能性ありなので
最新仕様を確認してください。
append
時の容量拡張アルゴリズム:
- 1024要素までは2倍ずつ拡張
- それ以降は約1.25倍ずつ拡張
var s []int
for i := 0; i < 10000; i++ {
s = append(s, i) // 複数回のメモリ再割り当てが発生
}
最適化:
s := make([]int, 0, 10000) // 事前に容量確保
for i := 0; i < 10000; i++ {
s = append(s, i)
}
スライス操作の落とし穴
1. 部分スライスの共有問題
original := []int{1, 2, 3, 4, 5}
sub := original[1:3] // [2, 3]
// subに変更を加えると...
sub[0] = 99
fmt.Println(original) // [1, 99, 3, 4, 5] 元データも変更される
安全な部分スライス取得:
// 方法1: 完全なコピーを作成
safeSub := make([]int, 2)
copy(safeSub, original[1:3])
// 方法2: appendを利用(Go 1.21以降)
safeSub := append([]int(nil), original[1:3]...)
2. スライスの比較
スライスは直接比較できない:
a := []int{1, 2, 3}
b := []int{1, 2, 3}
// if a == b { ... } // コンパイルエラー
比較方法:
// 方法1: reflect.DeepEqual
if reflect.DeepEqual(a, b) { ... }
// 方法2: 手動で比較(パフォーマンス重視時)
func slicesEqual(a, b []int) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
並行処理での注意
1. スライスはスレッドセーフではない
複数のgoroutineから同時にアクセスするとデータ競合が発生:
var s []int
func appendData() {
for i := 0; i < 1000; i++ {
go func(n int) {
s = append(s, n) // 競合状態
}(i)
}
}
安全な使用方法:
// 方法1: sync.Mutexを使用
var (
s []int
mut sync.Mutex
)
func safeAppend(n int) {
mut.Lock()
defer mut.Unlock()
s = append(s, n)
}
// 方法2: channelを使用
results := make(chan int, 1000)
go func() {
for n := range results {
s = append(s, n)
}
}()
2. スライスの読み取り専用使用
複数goroutineからの読み取りは安全:
func printSlice(s []int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Println(s) // 読み取りのみなら安全
}
func main() {
s := []int{1, 2, 3}
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go printSlice(s, &wg)
}
wg.Wait()
}
パフォーマンスチューニング
1. スライスの事前割り当て
// 非効率的
var s []string
for _, item := range items {
s = append(s, process(item))
}
// 効率的
s := make([]string, 0, len(items))
for _, item := range items {
s = append(s, process(item))
}
2. スライスの再利用
// バッファプールの使用例
var bufPool = sync.Pool{
New: func() interface{} {
return make([]byte, 0, 1024)
},
}
func getBuffer() []byte {
return bufPool.Get().([]byte)
}
func putBuffer(buf []byte) {
buf = buf[:0] // リセット
bufPool.Put(buf)
}
3. 大きなスライスのメモリ解放
bigSlice := make([]int, 1e6)
// 使用後...
bigSlice = nil // GCがメモリを解放できるように
特殊なスライス操作
1. スライスの削除
// インデックスiの要素を削除
func remove(s []int, i int) []int {
return append(s[:i], s[i+1:]...)
}
// 順序を保持しない高速削除
func fastRemove(s []int, i int) []int {
s[i] = s[len(s)-1]
return s[:len(s)-1]
}
2. スライスのフィルタリング
// 条件に合わない要素を削除
s := []int{1, 2, 3, 4, 5}
n := 0
for _, x := range s {
if x%2 == 1 { // 奇数だけ残す
s[n] = x
n++
}
}
s = s[:n]
3. スライスの反転
func reverse(s []int) {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}
まとめチェックリスト
✅ スライスは参照型、配列は値型と理解している
✅ 大きなデータを扱う時はメモリ使用に注意している
✅ append
の容量拡張挙動を理解している
✅ 並行処理では適切な同期機構を使用している
✅ スライスのコピーが必要な場面でcopy
を使っている
✅ 部分スライスが元データを参照していることを意識している
✅ スライスの事前割り当てでパフォーマンスを最適化している
✅ 不要なメモリ保持を避けるため、大きなスライスはnil
を代入している。
Go言語のスライスと配列:実践的な応用テクニック
高度なスライス操作
1. スライスのスタック/キュー操作
// スタックとして使用(LIFO)
stack := []int{1, 2, 3}
// Push
stack = append(stack, 4)
// Pop
val := stack[len(stack)-1]
stack = stack[:len(stack)-1]
// キューとして使用(FIFO)
queue := []int{1, 2, 3}
// Enqueue
queue = append(queue, 4)
// Dequeue
val := queue[0]
queue = queue[1:] // メモリリークに注意!
2. スライスのバッチ処理
// 大きなスライスをバッチ処理
data := make([]int, 10000)
batchSize := 100
for i := 0; i < len(data); i += batchSize {
end := i + batchSize
if end > len(data) {
end = len(data)
}
batch := data[i:end]
processBatch(batch)
}
パフォーマンス最適化の深堀り
1. メモリ割り当てのベンチマーク
// 悪い例(都度拡張)
func BenchmarkAppend(b *testing.B) {
var s []int
for i := 0; i < b.N; i++ {
s = append(s, i)
}
}
// 良い例(事前割り当て)
func BenchmarkPreAllocate(b *testing.B) {
s := make([]int, 0, b.N)
for i := 0; i < b.N; i++ {
s = append(s, i)
}
}
2. スライスのメモリフットプリント削減
// 不要なメモリ保持を解放
func processLargeData() {
largeData := loadHugeData() // 大きなデータ読み込み
// 処理後に不要なメモリを解放
result := process(largeData)
largeData = nil // GCが回収できるように
return result
}
エラーハンドリングのベストプラクティス
1. スライス範囲外アクセス防止
func safeAccess(s []int, index int) (int, error) {
if index < 0 || index >= len(s) {
return 0, fmt.Errorf("index out of range: %d", index)
}
return s[index], nil
}
2. nilスライスと空スライスの適切な扱い
func processSlice(s []int) {
if s == nil {
// nilスライス特有の処理
return
}
if len(s) == 0 {
// 空スライス処理
return
}
// 通常処理
}
標準ライブラリの活用
1. sortパッケージの利用
// スライスのソート
nums := []int{3, 1, 4, 1, 5, 9}
sort.Ints(nums)
// カスタムソート
users := []struct{
Name string
Age int
}{
{"Alice", 25},
{"Bob", 30},
}
sort.Slice(users, func(i, j int) bool {
return users[i].Age < users[j].Age
})
2. bytes.Bufferの効率的な使用
// 文字列結合の効率的な方法
var buf bytes.Buffer
for _, s := range []string{"a", "b", "c"} {
buf.WriteString(s)
}
result := buf.String()
リアルワールドユースケース
1. 設定値のマージ
// デフォルト設定とユーザー設定をマージ
func mergeConfig(defaults, overrides []Config) []Config {
merged := make([]Config, len(defaults))
copy(merged, defaults)
for _, override := range overrides {
if override.ID < len(merged) {
merged[override.ID] = override
}
}
return merged
}
2. バイナリデータのチャンク処理
func processInChunks(data []byte, chunkSize int) error {
for offset := 0; offset < len(data); offset += chunkSize {
end := offset + chunkSize
if end > len(data) {
end = len(data)
}
chunk := data[offset:end]
if err := processChunk(chunk); err != nil {
return err
}
}
return nil
}
デバッグテクニック
1. スライスの内部構造調査
func inspectSlice(s []int) {
fmt.Printf("Pointer: %p, Length: %d, Capacity: %d\n",
&s[0], len(s), cap(s))
}
// 使用例
s := make([]int, 3, 5)
inspectSlice(s) // 内部状態を出力
2. メモリリーク検出
```go
func printMemUsage() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("Alloc = %v MiB", m.Alloc/1024/1024)
}
新しいのGoバージョンでの改善
1. Go 1.21の新しいslicesパッケージ
import "slices"
// 新しい便利関数群
nums := []int{1, 2, 3}
if slices.Contains(nums, 2) {
fmt.Println("Contains 2")
}
// スライスの比較
other := []int{1, 2, 3}
if slices.Equal(nums, other) {
fmt.Println("Equal")
}
2. ゼロアロケーションスライス
// 空スライスの最適化表現
var zeroAlloc []int // nilスライス
empty := []int{} // 空スライス(Go 1.21以降で最適化)
// パフォーマンス重視の場合はnilスライスを使用
実践的なアドバイス
-
API設計のポイント:
- 関数がスライスを変更するかどうかをドキュメントに明記
- 可能な場合、スライスではなくインターフェースを返すことを検討
-
テストの重要性:
- nilスライス、空スライス、容量不足の場合などエッジケースをテスト
- ベンチマークテストでパフォーマンスを計測
-
プロファイリング:
-
pprof
を使用してスライス操作のメモリ使用量を分析 - 不必要な割り当てがないか定期的にチェック
-
-
コードレビュー時のチェックポイント:
- スライスの容量が適切か
- 並行処理での安全対策があるか
- 大きなデータのコピーが発生していないか