logo hsb.horse
← スニペット一覧に戻る

Snippets

Golangのデータ型ごとのメモリサイズ

Goの各データ型が占有するメモリサイズの一覧と、メモリ効率を意識したコーディングのポイント。

公開日: 更新日:

Goの各データ型がメモリ上で占めるサイズを理解することは、メモリ効率の高いプログラムを書く上で重要だ。特に大量のデータを扱う場合や、組み込みシステム・高負荷なアプリケーションでは、データ構造の選択がパフォーマンスに直結する。

データ型ごとのメモリサイズ一覧

データ型サイズ (バイト)備考
int / uint8 / 864ビットシステムでは8バイト、32ビットでは4バイト
int8 / uint81 / 1
int16 / uint162 / 2
int32 / uint324 / 4
int64 / uint648 / 8
float324IEEE 754 単精度浮動小数点
float648IEEE 754 倍精度浮動小数点
complex648float32 × 2(実部・虚部)
complex12816float64 × 2(実部・虚部)
byte (uint8の別名)1
rune (int32の別名)4Unicode コードポイント
bool1内部的には1バイト
string16ポインタ(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 bytes
type Before struct {
flag1 bool // 1 + 7 padding
num1 int64 // 8
flag2 bool // 1 + 7 padding
num2 int64 // 8
}
// 最適化後: 24 bytes
type 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 = 42

5. 文字列の結合でBuilderを使う

文字列は不変なため、+ 演算子での結合は毎回新しいメモリ領域を確保する。大量の結合には strings.Builder を使う。

import "strings"
var b strings.Builder
for 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プログラムを書ける。