第9章: 構造体 - マシンレベル完全解説

学習目標

この章を終えると、以下ができるようになります:

  • 構造体のメモリレイアウトをマシンレベルで理解できる
  • メモリアライメントとパディングを説明できる
  • 構造体の埋め込みがどのように実装されているか理解できる
  • タグとリフレクションの内部機構を理解できる
  • 構造体のベストプラクティスを実践できる

🔑 構造体とは何か - メモリの視点から

構造体は、複数のフィールドを1つのメモリブロックにまとめたデータ型です。CPUとメモリの観点から見ると、構造体は連続したメモリ領域に配置された複数の値の集合です。

CPUとメモリの基礎

現代のコンピュータでは、CPUとメインメモリの間に速度差があります:

CPU レジスタ:    ~1 サイクル
L1 キャッシュ:   ~4 サイクル
L2 キャッシュ:   ~10 サイクル
L3 キャッシュ:   ~40 サイクル
メインメモリ:    ~200 サイクル

💡 重要: 構造体を効率的に設計することで、キャッシュヒット率を向上させ、パフォーマンスを大幅に改善できます。

構造体の基本定義とメモリレイアウト

最もシンプルな構造体

type Point struct {
    X int32
    Y int32
}

このコードがメモリ上でどうなるか見てみましょう:

メモリアドレス         内容
0x1000        [X: 4バイト]
0x1004        [Y: 4バイト]
-----------------------
合計: 8バイト

🔑 キーポイント: Goの構造体は、フィールドが定義された順序でメモリに配置されます。

フィールドへのアクセス - アセンブリレベル

p := Point{X: 10, Y: 20}
fmt.Println(p.X)

このコードは、アセンブリレベルでは次のように実行されます:

; p のベースアドレスを R1 レジスタにロード
MOVQ    p, R1

; X フィールドにアクセス(オフセット0)
MOVL    0(R1), R2    ; p.X を R2 にロード

; Y フィールドにアクセス(オフセット4)
MOVL    4(R1), R3    ; p.Y を R3 にロード

💡 CPUレベルでの理解: フィールドアクセスは、ベースアドレス + オフセット計算によって実行されます。コンパイル時にオフセットが決定されるため、実行時のオーバーヘッドは最小限です。

メモリアライメントとパディング - 最重要概念

なぜアライメントが必要なのか

CPUは、特定のアドレス境界からデータを読み取る方が効率的です。例えば、64ビットCPUは8バイト境界からの読み取りを得意とします。

⚠️ 注意: アライメントされていないデータにアクセスすると、CPUは複数回のメモリアクセスを必要とし、パフォーマンスが低下します。

アライメントルール

各データ型には「アライメント要件」があります:

サイズ アライメント
bool 1 1
int8 1 1
int16 2 2
int32 4 4
int64 8 8
float32 4 4
float64 8 8
pointer 8 8 (64bit)
string 16 8
slice 24 8

パディングの実例

// 悪い例: メモリ効率が悪い
type BadStruct struct {
    A byte   // 1バイト
    B int64  // 8バイト
    C byte   // 1バイト
    D int64  // 8バイト
}

メモリレイアウト(64ビットシステム):

アドレス    フィールド     内容
0x00       A             [1バイト]
0x01-0x07  (padding)     [7バイト] ← 無駄!
0x08       B             [8バイト]
0x10       C             [1バイト]
0x11-0x17  (padding)     [7バイト] ← 無駄!
0x18       D             [8バイト]
------------------------------------
合計: 32バイト(実データは18バイトのみ)

🔑 最適化前: 32バイト使用(パディング14バイト)

// 良い例: メモリ効率が良い
type GoodStruct struct {
    B int64  // 8バイト
    D int64  // 8バイト
    A byte   // 1バイト
    C byte   // 1バイト
}

メモリレイアウト:

アドレス    フィールド     内容
0x00       B             [8バイト]
0x08       D             [8バイト]
0x10       A             [1バイト]
0x11       C             [1バイト]
0x12-0x17  (padding)     [6バイト] ← 最小化!
------------------------------------
合計: 24バイト(実データは18バイト)

🔑 最適化後: 24バイト使用(パディング6バイト) 💡 削減率: 25%のメモリ削減!

パディングを確認する実践コード

package main

import (
    "fmt"
    "unsafe"
)

type BadStruct struct {
    A byte
    B int64
    C byte
    D int64
}

type GoodStruct struct {
    B int64
    D int64
    A byte
    C byte
}

func main() {
    fmt.Printf("BadStruct size:  %d bytes\n", unsafe.Sizeof(BadStruct{}))
    fmt.Printf("GoodStruct size: %d bytes\n", unsafe.Sizeof(GoodStruct{}))

    // 各フィールドのオフセットを表示
    bad := BadStruct{}
    fmt.Printf("\nBadStruct offsets:\n")
    fmt.Printf("  A offset: %d\n", unsafe.Offsetof(bad.A))
    fmt.Printf("  B offset: %d\n", unsafe.Offsetof(bad.B))
    fmt.Printf("  C offset: %d\n", unsafe.Offsetof(bad.C))
    fmt.Printf("  D offset: %d\n", unsafe.Offsetof(bad.D))
}

出力:

BadStruct size:  32 bytes
GoodStruct size: 24 bytes

BadStruct offsets:
  A offset: 0
  B offset: 8
  C offset: 16
  D offset: 24

構造体の作成方法 - メモリ割り当ての視点

スタックとヒープの違い

Goでは、構造体はスタックまたはヒープに割り当てられます。

スタック:
┌─────────────────┐
│  高速アクセス    │  - 関数呼び出しごとに確保
│  自動解放        │  - サイズ制限あり
│  連続メモリ      │  - GC不要
└─────────────────┘

ヒープ:
┌─────────────────┐
│  柔軟なサイズ    │  - 動的割り当て
│  GCで解放        │  - サイズ制限なし(メモリ次第)
│  断片化の可能性  │  - GCオーバーヘッドあり
└─────────────────┘

方法1: 値による初期化(スタック割り当て)

p1 := Point{X: 10, Y: 20}

メモリ図:

スタック:
┌──────────────────────┐
│  関数フレーム         │
│  ┌────────────────┐  │
│  │ p1.X: 10       │  │  ← スタック上に直接配置
│  │ p1.Y: 20       │  │
│  └────────────────┘  │
└──────────────────────┘

🔑 特徴:

  • 関数がリターンすると自動的に解放
  • メモリアクセスが高速
  • エスケープ解析により、必要に応じてヒープに移動

方法2: ポインタ初期化(ヒープ割り当ての可能性)

p2 := &Point{X: 10, Y: 20}

メモリ図:

スタック:              ヒープ:
┌──────────────┐      ┌──────────────┐
│ p2: 0x8000   │─────→│ X: 10        │
└──────────────┘      │ Y: 20        │
                      └──────────────┘

💡 エスケープ解析: Goコンパイラは、構造体が関数外で使用されるかを分析し、必要に応じてヒープに割り当てます。

方法3: new() を使用

p3 := new(Point)  // ゼロ値で初期化されたポインタ

メモリ図:

スタック:              ヒープ:
┌──────────────┐      ┌──────────────┐
│ p3: 0x8010   │─────→│ X: 0         │
└──────────────┘      │ Y: 0         │
                      └──────────────┘

エスケープ解析の実例

// スタックに割り当て
func createLocal() Point {
    return Point{X: 10, Y: 20}  // コピーして返す
}

// ヒープに割り当て
func createHeap() *Point {
    return &Point{X: 10, Y: 20}  // 関数外で使用されるのでヒープへ
}

エスケープ解析を確認:

go build -gcflags='-m' main.go

出力例:

./main.go:10:9: &Point{...} escapes to heap

構造体のコピーとポインタ - メモリの動き

値渡し(ディープコピー)

type Person struct {
    Name string
    Age  int
}

func modifyPerson(p Person) {
    p.Age = 30  // コピーを変更
}

func main() {
    p1 := Person{Name: "Alice", Age: 25}
    modifyPerson(p1)
    fmt.Println(p1.Age)  // 25(変更されない)
}

メモリ図:

呼び出し前:
main のスタック:
┌─────────────────┐
│ p1:             │
│  Name: "Alice"  │
│  Age: 25        │
└─────────────────┘

関数呼び出し:
modifyPerson のスタック:
┌─────────────────┐
│ p (コピー):     │
│  Name: "Alice"  │  ← p1 からコピーされた
│  Age: 30        │  ← ここを変更
└─────────────────┘

リターン後:
main のスタック:
┌─────────────────┐
│ p1:             │
│  Name: "Alice"  │
│  Age: 25        │  ← 変更されていない
└─────────────────┘

🔑 重要: 構造体を値で渡すと、全フィールドがコピーされます。大きな構造体では、ポインタを使用する方が効率的です。

ポインタ渡し(参照)

func modifyPersonPtr(p *Person) {
    p.Age = 30  // 元のデータを変更
}

func main() {
    p1 := Person{Name: "Alice", Age: 25}
    modifyPersonPtr(&p1)
    fmt.Println(p1.Age)  // 30(変更される)
}

メモリ図:

main のスタック:        ヒープ/スタック:
┌──────────────┐       ┌─────────────────┐
│ p1:          │       │ Name: "Alice"   │
│   @0x1000    │──────→│ Age: 25→30      │
└──────────────┘       └─────────────────┘
                              ↑
modifyPersonPtr のスタック:   │
┌──────────────┐             │
│ p: 0x1000    │─────────────┘
└──────────────┘

💡 パフォーマンス: ポインタは8バイト(64ビットシステム)のみコピーされるため、大きな構造体では大幅に効率的です。

パフォーマンス比較

type LargeStruct struct {
    Data [1000]int64  // 8000バイト
}

// 値渡し: 8000バイトコピー
func processByValue(s LargeStruct) {
    // ...
}

// ポインタ渡し: 8バイトコピー
func processByPointer(s *LargeStruct) {
    // ...
}

⚠️ 注意: 構造体が16バイト以下なら値渡し、それより大きければポインタ渡しを検討しましょう。

構造体の埋め込み - 実装の内部構造

埋め込みの基本

type Engine struct {
    Power int
    Type  string
}

type Car struct {
    Model  string
    Engine  // 埋め込み
}

メモリレイアウト:

Car のメモリ構造:
┌───────────────────────────┐
│ Model: string (16バイト)  │  ← Car 自身のフィールド
├───────────────────────────┤
│ Engine.Power: int         │  ← 埋め込まれた Engine
│ Engine.Type: string       │
└───────────────────────────┘

🔑 重要: 埋め込みは「継承」ではなく「合成」です。Engine のフィールドは Car の一部として直接配置されます。

フィールドアクセスのメカニズム

car := Car{
    Model: "Tesla",
    Engine: Engine{Power: 400, Type: "Electric"},
}

// 両方とも同じメモリ位置にアクセス
power1 := car.Power          // ショートカット
power2 := car.Engine.Power   // 明示的

アセンブリレベル:

; car のベースアドレスが R1 にある場合

; car.Power にアクセス
MOVQ    16(R1), R2    ; Model (16バイト) をスキップして Power を取得

; car.Engine.Power にアクセス
MOVQ    16(R1), R2    ; 同じアドレス!

💡 コンパイラマジック: car.Power は、コンパイル時に car.Engine.Power に変換されます。実行時のオーバーヘッドはゼロです。

複数の埋め込み

type Logger struct {
    Name string
}

func (l Logger) Log(msg string) {
    fmt.Printf("[%s] %s\n", l.Name, msg)
}

type Validator struct {
    Strict bool
}

func (v Validator) Validate() bool {
    return v.Strict
}

type Service struct {
    ID        int
    Logger     // 埋め込み1
    Validator  // 埋め込み2
}

メモリレイアウト:

Service の構造:
┌─────────────────────────┐  オフセット
│ ID: int (8バイト)       │  0
├─────────────────────────┤
│ Logger.Name: string     │  8
│   (16バイト)            │
├─────────────────────────┤
│ Validator.Strict: bool  │  24
│   (1バイト + padding)   │
└─────────────────────────┘
合計: 32バイト(パディング含む)

名前衝突の解決

type A struct {
    Name string
}

type B struct {
    Name string
}

type C struct {
    A
    B
}

メモリレイアウト:

C の構造:
┌─────────────────────────┐
│ A.Name: string          │  ← 区別される
│   (16バイト)            │
├─────────────────────────┤
│ B.Name: string          │  ← 区別される
│   (16バイト)            │
└─────────────────────────┘

⚠️ 注意: c.Name は曖昧さのためコンパイルエラー。必ず c.A.Name または c.B.Name と明示的に指定する必要があります。

構造体タグとリフレクション - メタプログラミング

タグの構造

構造体タグは、フィールド宣言の後に続く文字列リテラルです:

type User struct {
    ID       int    `json:"id" db:"user_id" validate:"required"`
    Username string `json:"username" db:"username" validate:"min=3,max=20"`
    Email    string `json:"email" db:"email" validate:"email"`
}

タグはどこに保存されるのか

タグは、型情報(メタデータ)として実行可能ファイルに埋め込まれます。

実行可能ファイルの構造:
┌──────────────────┐
│ コードセグメント  │  ← 実際の命令
├──────────────────┤
│ データセグメント  │  ← 定数、文字列
├──────────────────┤
│ 型情報セクション  │  ← ここにタグが格納される
│  - 型名          │
│  - フィールド名  │
│  - タグ文字列    │
└──────────────────┘

💡 重要: タグは実行時ではなく、コンパイル時に処理され、型情報として保存されます。

リフレクションによるタグへのアクセス

package main

import (
    "fmt"
    "reflect"
)

type User struct {
    ID    int    `json:"id" validate:"required"`
    Email string `json:"email" validate:"email"`
}

func printTags(u User) {
    t := reflect.TypeOf(u)

    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        jsonTag := field.Tag.Get("json")
        validateTag := field.Tag.Get("validate")

        fmt.Printf("Field: %s\n", field.Name)
        fmt.Printf("  Type: %s\n", field.Type)
        fmt.Printf("  JSON tag: %s\n", jsonTag)
        fmt.Printf("  Validate tag: %s\n", validateTag)
        fmt.Printf("  Offset: %d bytes\n", field.Offset)
    }
}

func main() {
    printTags(User{})
}

出力:

Field: ID
  Type: int
  JSON tag: id
  Validate tag: required
  Offset: 0 bytes
Field: Email
  Type: string
  JSON tag: email
  Validate tag: email
  Offset: 8 bytes

JSONエンコーディングの内部動作

type Product struct {
    ID    int     `json:"id"`
    Name  string  `json:"name"`
    Price float64 `json:"price"`
}

product := Product{ID: 1, Name: "Laptop", Price: 999.99}
jsonData, _ := json.Marshal(product)

json.Marshal の内部動作:

1. リフレクションで Product の型情報を取得
   ┌─────────────────────────────┐
   │ Type: Product               │
   │ Fields:                     │
   │  - ID: int, tag:"id"        │
   │  - Name: string, tag:"name" │
   │  - Price: float64, tag:"price"│
   └─────────────────────────────┘

2. 各フィールドを走査
   for i := 0; i < numFields; i++ {
       field := type.Field(i)
       value := getValue(product, field.Offset)
       jsonKey := field.Tag.Get("json")
       // JSON フォーマットで書き込み
   }

3. 出力バッファに書き込み
   {"id":1,"name":"Laptop","price":999.99}

⚠️ パフォーマンス注意: リフレクションは比較的重い処理です。頻繁に実行される場合は、コード生成やキャッシングを検討しましょう。

カスタムマーシャリング

type Date struct {
    Year  int
    Month int
    Day   int
}

// Marshaler インターフェースを実装
func (d Date) MarshalJSON() ([]byte, error) {
    s := fmt.Sprintf("\"%04d-%02d-%02d\"", d.Year, d.Month, d.Day)
    return []byte(s), nil
}

// Unmarshaler インターフェースを実装
func (d *Date) UnmarshalJSON(data []byte) error {
    s := string(data)
    s = strings.Trim(s, "\"")
    parts := strings.Split(s, "-")
    if len(parts) != 3 {
        return fmt.Errorf("invalid date format")
    }

    d.Year, _ = strconv.Atoi(parts[0])
    d.Month, _ = strconv.Atoi(parts[1])
    d.Day, _ = strconv.Atoi(parts[2])
    return nil
}

🔑 インターフェース駆動: json.Marshalerjson.Unmarshaler インターフェースを実装すると、標準の JSON 処理をオーバーライドできます。

構造体のベストプラクティス - メモリ最適化

1. フィールドの並び替え

// 最適化前: 56バイト
type Before struct {
    Flag1  bool    // 1バイト
    Number int64   // 8バイト
    Flag2  bool    // 1バイト
    Ptr    *int    // 8バイト
    Flag3  bool    // 1バイト
    Value  float64 // 8バイト
}

// 最適化後: 32バイト
type After struct {
    Number int64   // 8バイト
    Value  float64 // 8バイト
    Ptr    *int    // 8バイト
    Flag1  bool    // 1バイト
    Flag2  bool    // 1バイト
    Flag3  bool    // 1バイト
    // 5バイト padding
}

💡 最適化ルール: 大きなフィールドから小さなフィールドの順に並べる。

2. ビットフィールドのエミュレーション

Goには明示的なビットフィールドはありませんが、uint8でフラグをまとめることができます:

// 非効率: 8バイト
type Flags struct {
    Read    bool  // 1バイト + padding
    Write   bool  // 1バイト + padding
    Execute bool  // 1バイト + padding
}

// 効率的: 1バイト
type FlagsOptimized uint8

const (
    FlagRead    FlagsOptimized = 1 << 0  // 0b00000001
    FlagWrite   FlagsOptimized = 1 << 1  // 0b00000010
    FlagExecute FlagsOptimized = 1 << 2  // 0b00000100
)

func (f FlagsOptimized) Has(flag FlagsOptimized) bool {
    return f&flag != 0
}

func (f *FlagsOptimized) Set(flag FlagsOptimized) {
    *f |= flag
}

func (f *FlagsOptimized) Clear(flag FlagsOptimized) {
    *f &^= flag
}

使用例:

var perms FlagsOptimized
perms.Set(FlagRead | FlagWrite)

if perms.Has(FlagRead) {
    fmt.Println("読み取り可能")
}

3. ゼロ値を有効にする設計

// 良い設計
type Buffer struct {
    data []byte  // nil スライスでも append が動作
    pos  int     // 0 は有効な位置
}

func (b *Buffer) Write(p []byte) {
    b.data = append(b.data, p...)  // nil でも OK
}

// 使用例
var buf Buffer  // ゼロ値
buf.Write([]byte("hello"))  // 問題なく動作

4. 構造体サイズの分析ツール

package main

import (
    "fmt"
    "reflect"
    "unsafe"
)

func analyzeStruct(v interface{}) {
    t := reflect.TypeOf(v)
    size := unsafe.Sizeof(v)

    fmt.Printf("Struct: %s\n", t.Name())
    fmt.Printf("Total size: %d bytes\n", size)
    fmt.Printf("Alignment: %d bytes\n", unsafe.Alignof(v))
    fmt.Println("\nFields:")

    totalFieldSize := uintptr(0)
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        fieldSize := field.Type.Size()
        totalFieldSize += fieldSize

        padding := uintptr(0)
        if i < t.NumField()-1 {
            nextField := t.Field(i + 1)
            padding = nextField.Offset - (field.Offset + fieldSize)
        }

        fmt.Printf("  [%2d] %-15s offset: %3d, size: %2d, padding: %2d\n",
            i, field.Name, field.Offset, fieldSize, padding)
    }

    totalPadding := size - totalFieldSize
    wastePercent := float64(totalPadding) / float64(size) * 100

    fmt.Printf("\nSummary:\n")
    fmt.Printf("  Field data: %d bytes\n", totalFieldSize)
    fmt.Printf("  Padding: %d bytes (%.1f%%)\n", totalPadding, wastePercent)
}

type Example struct {
    A byte
    B int64
    C byte
    D int32
}

func main() {
    analyzeStruct(Example{})
}

コンストラクタパターン - 実践的な初期化

基本的なコンストラクタ

type Server struct {
    host    string
    port    int
    timeout time.Duration
    logger  *log.Logger
}

// コンストラクタ
func NewServer(host string, port int) *Server {
    return &Server{
        host:    host,
        port:    port,
        timeout: 30 * time.Second,  // デフォルト値
        logger:  log.New(os.Stdout, "SERVER: ", log.LstdFlags),
    }
}

メモリ割り当て:

ヒープ:
┌────────────────────────────┐
│ Server instance:           │
│  host: "localhost"         │  ← 文字列はヒープに別途確保
│  port: 8080                │
│  timeout: 30000000000      │
│  logger: *log.Logger       │  ← ポインタ
└────────────────────────────┘
       ↑
       │
スタック:
┌────────────────┐
│ server: ptr    │  ← NewServer の戻り値
└────────────────┘

オプションパターン - 柔軟な初期化

type ServerOption func(*Server)

func WithHost(host string) ServerOption {
    return func(s *Server) {
        s.host = host
    }
}

func WithPort(port int) ServerOption {
    return func(s *Server) {
        s.port = port
    }
}

func WithTimeout(timeout time.Duration) ServerOption {
    return func(s *Server) {
        s.timeout = timeout
    }
}

func NewServer(opts ...ServerOption) *Server {
    // デフォルト値
    server := &Server{
        host:    "localhost",
        port:    8080,
        timeout: 30 * time.Second,
    }

    // オプションを適用
    for _, opt := range opts {
        opt(server)
    }

    return server
}

使用例:

// デフォルト設定
server1 := NewServer()

// カスタム設定
server2 := NewServer(
    WithHost("0.0.0.0"),
    WithPort(3000),
    WithTimeout(60 * time.Second),
)

💡 メリット:

  • オプションの追加が容易
  • 後方互換性を維持
  • 読みやすいAPI

🔍 自己確認問題

問題1: メモリレイアウト

次の構造体のサイズとパディングを計算してください(64ビットシステム):

type Data struct {
    A bool
    B int32
    C byte
    D *int
    E int16
}

解答

フィールド  サイズ  アライメント  オフセット  パディング
A (bool)    1      1             0           3
B (int32)   4      4             4           0
C (byte)    1      1             8           7
D (*int)    8      8             16          0
E (int16)   2      2             24          6
---------------------------------------------------
合計: 32バイト(実データ16バイト、パディング16バイト)

最適化版:

type DataOptimized struct {
    D *int   // 8バイト, offset 0
    B int32  // 4バイト, offset 8
    E int16  // 2バイト, offset 12
    A bool   // 1バイト, offset 14
    C byte   // 1バイト, offset 15
}
// 合計: 16バイト(パディング0バイト)

問題2: 埋め込みの理解

次のコードの出力を予測してください:

type Base struct {
    Value int
}

func (b Base) Print() {
    fmt.Println("Value:", b.Value)
}

type Derived struct {
    Base
    Extra string
}

func main() {
    d := Derived{
        Base:  Base{Value: 42},
        Extra: "test",
    }

    fmt.Println(unsafe.Sizeof(d))
    fmt.Println(unsafe.Offsetof(d.Base))
    fmt.Println(unsafe.Offsetof(d.Extra))
}

解答

出力(64ビットシステム):

24
0
8

説明:

  • unsafe.Sizeof(d): Base(8) + Extra(16) = 24バイト
  • unsafe.Offsetof(d.Base): 0(最初のフィールド)
  • unsafe.Offsetof(d.Extra): 8(Base の後)

問題3: ポインタとコピー

次のコードの出力を予測してください:

type Counter struct {
    value int
}

func (c Counter) IncrementValue() {
    c.value++
}

func (c *Counter) IncrementPointer() {
    c.value++
}

func main() {
    c := Counter{value: 0}
    c.IncrementValue()
    fmt.Println(c.value)  // ?

    c.IncrementPointer()
    fmt.Println(c.value)  // ?
}

解答

出力:

0
1

説明:

  • IncrementValue(): レシーバーが値渡しなので、コピーに対して操作。元の値は変更されない。
  • IncrementPointer(): レシーバーがポインタなので、元の値が変更される。

問題4: タグとリフレクション

次のコードを完成させて、構造体のすべてのJSONタグを取得してください:

type User struct {
    ID       int    `json:"id"`
    Username string `json:"username"`
    Email    string `json:"email" validate:"email"`
}

func getJSONTags(v interface{}) []string {
    // ここを実装
}

func main() {
    tags := getJSONTags(User{})
    fmt.Println(tags)  // ["id", "username", "email"]
}

解答

func getJSONTags(v interface{}) []string {
    t := reflect.TypeOf(v)
    tags := make([]string, 0, t.NumField())

    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        jsonTag := field.Tag.Get("json")
        if jsonTag != "" && jsonTag != "-" {
            // カンマで分割して最初の部分のみ取得
            parts := strings.Split(jsonTag, ",")
            tags = append(tags, parts[0])
        }
    }

    return tags
}

問題5: メモリ最適化

次の構造体を最適化して、メモリ使用量を削減してください:

type Record struct {
    ID        int64
    Active    bool
    Timestamp int64
    Flag      bool
    Score     float64
    Status    bool
}

解答

最適化版:

type RecordOptimized struct {
    ID        int64   // 8バイト, offset 0
    Timestamp int64   // 8バイト, offset 8
    Score     float64 // 8バイト, offset 16
    Active    bool    // 1バイト, offset 24
    Flag      bool    // 1バイト, offset 25
    Status    bool    // 1バイト, offset 26
    // 5バイト padding
}

// Before: 40バイト(パディング含む)
// After: 32バイト(パディング含む)
// 削減率: 20%

または、フラグをビットフィールドにまとめる:

type RecordOptimized2 struct {
    ID        int64   // 8バイト
    Timestamp int64   // 8バイト
    Score     float64 // 8バイト
    Flags     uint8   // 1バイト(3つのboolをまとめる)
    // 7バイト padding
}

const (
    FlagActive RecordOptimized2Flags = 1 << 0
    FlagFlag   RecordOptimized2Flags = 1 << 1
    FlagStatus RecordOptimized2Flags = 1 << 2
)

// サイズ: 32バイト(パディングは不可避)

問題6: エスケープ解析

次の2つの関数のうち、どちらがヒープ割り当てを引き起こしますか?

func function1() int {
    x := 42
    return x
}

func function2() *int {
    x := 42
    return &x
}

解答

  • function1(): スタック割り当て。値をコピーして返すため、xは関数内で完結。
  • function2(): ヒープ割り当て。xのアドレスを返すため、関数外でも使用される可能性があり、エスケープ解析によりヒープに割り当てられる。

確認方法:

go build -gcflags='-m' main.go

出力:

./main.go:7:2: moved to heap: x

問題7: 構造体の比較

次のコードはコンパイルされますか?

type Person struct {
    Name    string
    Age     int
    Friends []string
}

func main() {
    p1 := Person{Name: "Alice", Age: 25, Friends: []string{"Bob"}}
    p2 := Person{Name: "Alice", Age: 25, Friends: []string{"Bob"}}

    if p1 == p2 {
        fmt.Println("Equal")
    }
}

解答

いいえ、コンパイルエラーになります。

理由:Person構造体にFriends []string(スライス)が含まれているため、比較不可能です。スライス、マップ、関数型を含む構造体は==で比較できません。

解決策:

// 方法1: スライスを比較しないフィールドのみで比較
if p1.Name == p2.Name && p1.Age == p2.Age {
    // ...
}

// 方法2: reflect.DeepEqual を使用
if reflect.DeepEqual(p1, p2) {
    // ...
}

// 方法3: カスタム Equal メソッドを実装
func (p Person) Equal(other Person) bool {
    if p.Name != other.Name || p.Age != other.Age {
        return false
    }
    if len(p.Friends) != len(other.Friends) {
        return false
    }
    for i := range p.Friends {
        if p.Friends[i] != other.Friends[i] {
            return false
        }
    }
    return true
}

問題8: 構造体のサイズ計算

次の構造体の実際のメモリサイズを計算してください(64ビットシステム):

type Complex struct {
    Name    string   // string: 16バイト(ポインタ8 + 長さ8)
    Numbers []int    // slice: 24バイト(ポインタ8 + 長さ8 + 容量8)
    Data    map[string]int  // map: 8バイト(ポインタ)
    Flag    bool     // bool: 1バイト
}

解答

フィールド  サイズ  オフセット  パディング
Name        16     0           0
Numbers     24     16          0
Data        8      40          0
Flag        1      48          7
------------------------------------------
合計: 56バイト(実データ49バイト + パディング7バイト)

注意点:

  • string: ポインタ(データへ)+ 長さ = 16バイト
  • slice: ポインタ + 長さ + 容量 = 24バイト
  • map: ポインタのみ(実際のデータは別の場所) = 8バイト
  • 最後のフィールドの後にもパディングが追加される場合がある

問題9: オプションパターンの実装

次のコンストラクタにオプションパターンを実装してください:

type Database struct {
    host     string
    port     int
    username string
    password string
    maxConns int
    timeout  time.Duration
}

// ここにオプションパターンを実装

解答

type Database struct {
    host     string
    port     int
    username string
    password string
    maxConns int
    timeout  time.Duration
}

type DBOption func(*Database)

func WithHost(host string) DBOption {
    return func(db *Database) {
        db.host = host
    }
}

func WithPort(port int) DBOption {
    return func(db *Database) {
        db.port = port
    }
}

func WithCredentials(username, password string) DBOption {
    return func(db *Database) {
        db.username = username
        db.password = password
    }
}

func WithMaxConnections(max int) DBOption {
    return func(db *Database) {
        db.maxConns = max
    }
}

func WithTimeout(timeout time.Duration) DBOption {
    return func(db *Database) {
        db.timeout = timeout
    }
}

func NewDatabase(opts ...DBOption) *Database {
    // デフォルト値
    db := &Database{
        host:     "localhost",
        port:     5432,
        maxConns: 10,
        timeout:  30 * time.Second,
    }

    // オプション適用
    for _, opt := range opts {
        opt(db)
    }

    return db
}

// 使用例
func main() {
    db := NewDatabase(
        WithHost("192.168.1.100"),
        WithPort(3306),
        WithCredentials("user", "pass"),
        WithMaxConnections(50),
    )
}

問題10: 構造体の深いコピー

次の構造体の深いコピー(ディープコピー)メソッドを実装してください:

type User struct {
    ID      int
    Name    string
    Friends []string
    Meta    map[string]string
}

func (u User) DeepCopy() User {
    // ここを実装
}

解答

func (u User) DeepCopy() User {
    // 基本フィールドは値渡しで自動的にコピーされる
    copy := u

    // スライスのディープコピー
    if u.Friends != nil {
        copy.Friends = make([]string, len(u.Friends))
        for i, friend := range u.Friends {
            copy.Friends[i] = friend
        }
        // または: copy(copy.Friends, u.Friends)
    }

    // マップのディープコピー
    if u.Meta != nil {
        copy.Meta = make(map[string]string, len(u.Meta))
        for key, value := range u.Meta {
            copy.Meta[key] = value
        }
    }

    return copy
}

// 使用例
func main() {
    u1 := User{
        ID:      1,
        Name:    "Alice",
        Friends: []string{"Bob", "Carol"},
        Meta:    map[string]string{"role": "admin"},
    }

    u2 := u1.DeepCopy()

    // 変更しても u1 に影響しない
    u2.Friends[0] = "Dave"
    u2.Meta["role"] = "user"

    fmt.Println(u1.Friends[0])  // "Bob"(変更されていない)
    fmt.Println(u1.Meta["role"]) // "admin"(変更されていない)
}

⚠️ 注意: ポインタフィールドやネストした構造体が含まれる場合は、それらも再帰的にコピーする必要があります。

まとめ

この章では、Goの構造体をマシンレベルで深く理解しました。

🔑 重要ポイント:

  • メモリレイアウト: 構造体は連続したメモリブロックで、フィールドは定義順に配置される
  • アライメントとパディング: CPUの効率のため、フィールド間にパディングが挿入される
  • 最適化: 大きなフィールドから順に並べることで、メモリ使用量を削減できる
  • 埋め込み: コンパイル時に展開され、実行時オーバーヘッドはゼロ
  • タグ: 型情報として保存され、リフレクションで取得可能
  • スタックとヒープ: エスケープ解析により、適切な場所に割り当てられる
  • ポインタ vs 値: 16バイト以下なら値渡し、それ以上ならポインタを検討

💡 パフォーマンスのヒント:

  • 頻繁にアクセスするフィールドを先頭に配置
  • キャッシュラインサイズ(64バイト)を意識する
  • 不必要なポインタを避ける(GC負荷軽減)
  • 大きな構造体はポインタで渡す

⚠️ よくある落とし穴:

  • パディングを考慮しないメモリ計算
  • スライス・マップを含む構造体の浅いコピー
  • エスケープ解析を無視したヒープ割り当て
  • リフレクションの過度な使用

次の章では、インターフェースの内部構造について、マシンレベルで詳しく学びます。