Goの各データ型がメモリ上で占めるサイズを理解することは、メモリ効率の高いプログラムを書く上で重要だ。特に大量のデータを扱う場合や、組み込みシステム・高負荷なアプリケーションでは、データ構造の選択がパフォーマンスに直結する。
データ型ごとのメモリサイズ一覧
| データ型 | サイズ (バイト) | 備考 |
|---|---|---|
| int / uint | 8 / 8 | 64ビットシステムでは8バイト、32ビットでは4バイト |
| int8 / uint8 | 1 / 1 | |
| int16 / uint16 | 2 / 2 | |
| int32 / uint32 | 4 / 4 | |
| int64 / uint64 | 8 / 8 | |
| float32 | 4 | IEEE 754 単精度浮動小数点 |
| float64 | 8 | IEEE 754 倍精度浮動小数点 |
| complex64 | 8 | float32 × 2(実部・虚部) |
| complex128 | 16 | float64 × 2(実部・虚部) |
| byte (uint8の別名) | 1 | |
| rune (int32の別名) | 4 | Unicode コードポイント |
| bool | 1 | 内部的には1バイト |
| string | 16 | ポインタ(8) + 長さ(8)、実データは別領域 |
| スライス (例: []int) | 24 | ポインタ(8) + 長さ(8) + 容量(8) |
| マップ (例: map[string]int) | 8 | 内部構造へのポインタ |
| チャネル (例: chan int) | 8 | 内部構造へのポインタ |
| 構造体 (例: struct{}) | 0 | フィールドがない場合は0 |
| インターフェース | 16 | 型情報(8) + 値へのポインタ(8) |
※ サイズは64ビットシステムでの標準的な値。32ビットシステムではポインタサイズが4バイトになる。
サンプル実装
サイズ確認
unsafe.Sizeof を使うことで、実行時にデータ型のサイズを確認できる。
package main
import ( "fmt" "unsafe")
func main() { // 基本型 var i int var i8 int8 var f32 float32 var f64 float64 var b bool var r rune
fmt.Printf("int: %d bytes\n", unsafe.Sizeof(i)) fmt.Printf("int8: %d bytes\n", unsafe.Sizeof(i8)) fmt.Printf("float32: %d bytes\n", unsafe.Sizeof(f32)) fmt.Printf("float64: %d bytes\n", unsafe.Sizeof(f64)) fmt.Printf("bool: %d bytes\n", unsafe.Sizeof(b)) fmt.Printf("rune: %d bytes\n", unsafe.Sizeof(r))
// 文字列・スライス・マップ var s string var slice []int var m map[string]int
fmt.Printf("string: %d bytes\n", unsafe.Sizeof(s)) fmt.Printf("slice: %d bytes\n", unsafe.Sizeof(slice)) fmt.Printf("map: %d bytes\n", unsafe.Sizeof(m))
// インターフェース var iface interface{} fmt.Printf("interface{}: %d bytes\n", unsafe.Sizeof(iface))}構造体のパディング確認
構造体はフィールドの配置順序によってメモリ効率が変わる。パディングにより、予想以上にメモリを消費することがある。
package main
import ( "fmt" "unsafe")
// 非効率な配置type BadLayout struct { a bool // 1 byte + 7 bytes padding b int64 // 8 bytes c bool // 1 byte + 7 bytes padding d int64 // 8 bytes}
// 効率的な配置type GoodLayout struct { b int64 // 8 bytes d int64 // 8 bytes a bool // 1 byte c bool // 1 byte + 6 bytes padding}
func main() { fmt.Printf("BadLayout: %d bytes\n", unsafe.Sizeof(BadLayout{})) // 32 bytes fmt.Printf("GoodLayout: %d bytes\n", unsafe.Sizeof(GoodLayout{})) // 24 bytes}スライスの容量とメモリ確保
スライスの容量を適切に管理することで、不要な再割り当てを避けられる。
package main
import ( "fmt")
func main() { // 容量を指定しない場合、追加のたびに再割り当てが発生する可能性がある s1 := []int{} for i := 0; i < 1000; i++ { s1 = append(s1, i) }
// 事前に容量を確保することで、再割り当てを回避 s2 := make([]int, 0, 1000) for i := 0; i < 1000; i++ { s2 = append(s2, i) }
fmt.Printf("s1 len: %d, cap: %d\n", len(s1), cap(s1)) fmt.Printf("s2 len: %d, cap: %d\n", len(s2), cap(s2))}マップの事前確保
マップも初期容量を指定することで、動的拡張によるコストを削減できる。
package main
import ( "fmt")
func main() { // 容量を指定しない場合 m1 := make(map[int]string) for i := 0; i < 1000; i++ { m1[i] = fmt.Sprintf("value%d", i) }
// 容量を事前に確保 m2 := make(map[int]string, 1000) for i := 0; i < 1000; i++ { m2[i] = fmt.Sprintf("value%d", i) }
fmt.Printf("m1 length: %d\n", len(m1)) fmt.Printf("m2 length: %d\n", len(m2))}応用・工夫アイデア
1. 構造体フィールドの配置を最適化する
サイズの大きいフィールドを先に配置し、小さいフィールドを後に配置することで、パディングを最小化できる。
// 最適化前: 32 bytestype Before struct { flag1 bool // 1 + 7 padding num1 int64 // 8 flag2 bool // 1 + 7 padding num2 int64 // 8}
// 最適化後: 24 bytestype After struct { num1 int64 // 8 num2 int64 // 8 flag1 bool // 1 flag2 bool // 1 + 6 padding}2. スライスの容量を適切に設定する
最終的な要素数が予測できる場合、make で容量を指定することで、メモリの再割り当てを避けられる。
// 1000要素を追加する場合items := make([]Item, 0, 1000)3. 固定長配列とスライスを使い分ける
要素数が固定で変更されない場合は、スライスではなく配列を使う方がメモリ効率が良い。
// スライス: 24 bytes (header) + 実データs := []int{1, 2, 3}
// 配列: 24 bytes (int × 3、実データのみ)a := [3]int{1, 2, 3}4. 空インターフェースの使用を最小限にする
interface{} は型情報とポインタで16バイト消費するため、型が明確な場合はジェネリクスや具体的な型を使う。
// 空インターフェースを使う場合(16 bytes)var v interface{} = 42
// 具体的な型を使う場合(8 bytes)var v int = 425. 文字列の結合でBuilderを使う
文字列は不変なため、+ 演算子での結合は毎回新しいメモリ領域を確保する。大量の結合には strings.Builder を使う。
import "strings"
var b strings.Builderfor i := 0; i < 1000; i++ { b.WriteString("item")}result := b.String()6. sync.Poolでオブジェクトを再利用する
頻繁に生成・破棄されるオブジェクトは sync.Pool で再利用することで、ガベージコレクションの負荷を減らせる。
import "sync"
var bufferPool = sync.Pool{ New: func() interface{} { return make([]byte, 1024) },}
// 使用buf := bufferPool.Get().([]byte)defer bufferPool.Put(buf)7. ポインタを使って大きな構造体のコピーを避ける
大きな構造体を関数に渡す際は、値渡しではなくポインタ渡しにすることで、コピーのコストを削減できる。
// 値渡し: 構造体全体がコピーされるfunc ProcessValue(data BigStruct) { // ...}
// ポインタ渡し: ポインタ(8 bytes)のみがコピーされるfunc ProcessPointer(data *BigStruct) { // ...}8. メモリプロファイリングで問題箇所を特定する
pprof を使ってメモリ使用量を可視化し、最適化すべき箇所を特定する。
import _ "net/http/pprof"import "net/http"
func main() { go func() { http.ListenAndServe("localhost:6060", nil) }() // アプリケーションロジック}ブラウザで http://localhost:6060/debug/pprof/heap にアクセスするか、go tool pprof でプロファイルを解析できる。
まとめ
- 基本型は明示的なサイズを持ち、文字列・スライス・マップ・インターフェースはポインタベース
- 構造体のフィールド配置順序でパディングが変わり、メモリ効率に影響する
- スライスやマップは初期容量を指定することで、再割り当てのコストを削減できる
- 大きな構造体はポインタ渡しにし、頻繁に生成するオブジェクトは
sync.Poolで再利用する pprofでメモリプロファイリングを行い、最適化すべき箇所を特定する
これらの知識を活用することで、メモリ効率の高いGoプログラムを書ける。
hsb.horse