第4章: 型システム基礎 - メモリレベルからの理解

学習目標

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

  • Goの型システムの設計思想を理解する
  • 各データ型のメモリレイアウトを説明できる
  • 型変換と型アサーションの内部動作を理解する
  • 名前付き型とエイリアスの違いを使い分けられる
  • ゼロ値の設計思想とその重要性を説明できる
  • スライスとマップの内部実装を理解し、効率的に使える
  • ---

    1. Goの型システム設計思想

    1.1 静的型付けの意味

    🔑 Goは静的型付け言語 - すべての変数の型がコンパイル時に決定されます。

    静的型付けのメリット:

    ┌─────────────────────────────────────────┐
    │          コンパイル時                     │
    │  ┌────────┐      ┌──────────────┐       │
    │  │ソース  │ -->  │型チェッカー  │       │
    │  │コード  │      │(エラー検出)│       │
    │  └────────┘      └──────────────┘       │
    │                        ↓                 │
    │                 型エラーは                │
    │               ここで全て検出!            │
    └─────────────────────────────────────────┘
                    ↓
    ┌─────────────────────────────────────────┐
    │           実行時                         │
    │   型エラーは発生しない                   │
    │   → パフォーマンス向上                   │
    │   → 安全性の保証                         │
    └─────────────────────────────────────────┘
    

    静的型付けの実例:

    // コンパイル時にエラーを検出
    var x int = 42
    var y string = "hello"
    
    // ❌ エラー: 型が一致しない(コンパイル時に検出)
    // x = y  // cannot use y (type string) as type int in assignment
    
    // ✅ 明示的な変換が必要
    x = len(y)  // OK(len関数はintを返す)
    

    💡 型安全性の保証:

  • メモリ安全性:型が違えば操作できない
  • バグの早期発見:実行前にエラーを発見
  • 最適化の可能性:コンパイラが型情報を活用
  • ドキュメント効果:型がコードの意図を明示

1.2 型システムの構造

Goの型は以下のように分類されます:

型の階層
├── 基本型(Basic types)
│   ├── bool
│   ├── 数値型
│   │   ├── 整数型(int, int8, int16, int32, int64)
│   │   ├── 符号なし整数(uint, uint8, uint16, uint32, uint64)
│   │   ├── 浮動小数点(float32, float64)
│   │   └── 複素数(complex64, complex128)
│   └── string
│
├── 複合型(Composite types)
│   ├── 配列(Array)
│   ├── スライス(Slice)
│   ├── マップ(Map)
│   ├── 構造体(Struct)
│   └── ポインタ(Pointer)
│
└── インターフェース型(Interface types)
    └── interface

---

2. 基本型のメモリレイアウト

2.1 整数型のサイズとメモリ表現

🔑 整数型は固定サイズのビット列として格納されます。

整数型のメモリサイズ:

┌──────────┬─────────┬──────────────────────┬────────────────────────┐
│   型     │サイズ   │      範囲(符号付き)  │   範囲(符号なし)      │
├──────────┼─────────┼──────────────────────┼────────────────────────┤
│ int8     │ 1バイト │  -128 ~ 127         │                        │
│ uint8    │ 1バイト │                      │  0 ~ 255              │
│ int16    │ 2バイト │  -32768 ~ 32767     │                        │
│ uint16   │ 2バイト │                      │  0 ~ 65535            │
│ int32    │ 4バイト │  -2^31 ~ 2^31-1     │                        │
│ uint32   │ 4バイト │                      │  0 ~ 2^32-1           │
│ int64    │ 8バイト │  -2^63 ~ 2^63-1     │                        │
│ uint64   │ 8バイト │                      │  0 ~ 2^64-1           │
│ int      │ 可変    │ プラットフォーム依存  │                        │
│ uint     │ 可変    │                      │ プラットフォーム依存    │
│ uintptr  │ 可変    │ ポインタサイズに一致  │                        │
└──────────┴─────────┴──────────────────────┴────────────────────────┘

メモリ表現の例(int8):

var x int8 = 42
var y int8 = -42

メモリ上の表現(2の補数表現):

x = 42(正の数)
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ 0 │ 0 │ 1 │ 0 │ 1 │ 0 │ 1 │ 0 │  = 0x2A = 42
└───┴───┴───┴───┴───┴───┴───┴───┘
  ↑
符号ビット(0=正)

y = -42(負の数)
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ 1 │ 1 │ 0 │ 1 │ 0 │ 1 │ 1 │ 0 │  = 0xD6 = -42
└───┴───┴───┴───┴───┴───┴───┴───┘
  ↑
符号ビット(1=負)

計算方法:
-42 = ~42 + 1 = ~(00101010) + 1 = 11010101 + 1 = 11010110

💡 intとint64の違い:

import "unsafe"

func main() {
    var i int
    var i64 int64

    fmt.Printf("intのサイズ: %d バイト\n", unsafe.Sizeof(i))
    fmt.Printf("int64のサイズ: %d バイト\n", unsafe.Sizeof(i64))

    // 32bitシステム: intのサイズ: 4 バイト
    // 64bitシステム: intのサイズ: 8 バイト
    // すべてのシステム: int64のサイズ: 8 バイト
}

オーバーフローの挙動:

var x int8 = 127
x = x + 1  // -128(オーバーフロー、ラップアラウンド)

// ビットレベルで見ると:
// 127:   01111111
// +1:  + 00000001
// ---    ---------
// -128:  10000000

⚠️ 注意:オーバーフローは検出されません!

var x uint8 = 255
x = x + 1  // 0(警告なし)

// 実務ではmath.MaxInt8などの定数を使う
import "math"

if x > math.MaxInt8 - 1 {
    // オーバーフローの危険性
}

2.2 浮動小数点のIEEE 754形式

🔑 Goの浮動小数点はIEEE 754標準に従います。

float64のメモリレイアウト(64ビット):

┌──────┬────────────┬────────────────────────────────────────────┐
│ 符号 │  指数部    │              仮数部                         │
│  1bit│  11bits    │              52bits                        │
└──────┴────────────┴────────────────────────────────────────────┘
 63     62       52  51                                         0

 値 = (-1)^符号 × 2^(指数-1023) × (1.仮数部)

float32のメモリレイアウト(32ビット):

┌──────┬────────────┬────────────────────┐
│ 符号 │  指数部    │     仮数部          │
│  1bit│  8bits     │     23bits         │
└──────┴────────────┴────────────────────┘
 31     30       23  22                 0

 値 = (-1)^符号 × 2^(指数-127) × (1.仮数部)

具体例:3.14をfloat64で表現

var pi float64 = 3.14

3.14の内部表現:

1. 二進数に変換
   3.14 (10進) = 11.001000111101... (2進)

2. 正規化
   11.001000111101... = 1.1001000111101... × 2^1

3. IEEE 754形式で格納
   ┌─┬───────────┬──────────────────────────────────────────────┐
   │0│10000000000│1001000111101011100001010001111010111000010100│
   └─┴───────────┴──────────────────────────────────────────────┘
    符号    指数      仮数
   (正)  (1+1023)   (1.仮数部)

💡 浮動小数点の精度問題:

// 有名な浮動小数点誤差
a := 0.1 + 0.2
fmt.Println(a)        // 0.30000000000000004
fmt.Println(a == 0.3) // false

// 理由:0.1と0.2は二進数で正確に表現できない
// 0.1 (10進) = 0.0001100110011... (2進、無限小数)
// 0.2 (10進) = 0.0011001100110... (2進、無限小数)

安全な浮動小数点比較:

import "math"

func almostEqual(a, b, epsilon float64) bool {
    return math.Abs(a-b) < epsilon
}

a := 0.1 + 0.2
b := 0.3
fmt.Println(almostEqual(a, b, 0.00001))  // true

⚠️ 浮動小数点の特殊値:

import "math"

// 無限大
positiveInf := math.Inf(1)   // +∞
negativeInf := math.Inf(-1)  // -∞

// 非数(Not a Number)
nan := math.NaN()

// 判定
math.IsInf(positiveInf, 1)   // true
math.IsNaN(nan)              // true

// NaNの特殊な性質
fmt.Println(nan == nan)      // false(NaNは自分自身とも等しくない)

2.3 complex128の内部構造

🔑 複素数は実部と虚部の2つの浮動小数点数のペアです。

complex128のメモリレイアウト:

┌────────────────────────────────┬────────────────────────────────┐
│     実部(float64)             │     虚部(float64)             │
│     64 bits                    │     64 bits                    │
└────────────────────────────────┴────────────────────────────────┘
 0                              63 64                           127

合計:128ビット = 16バイト

複素数の使用例:

// 複素数の作成
var c1 complex128 = 1 + 2i
c2 := complex(3.0, 4.0)  // 3 + 4i

// 実部と虚部の取得
r := real(c1)  // 1.0
i := imag(c1)  // 2.0

// メモリサイズの確認
import "unsafe"
fmt.Println(unsafe.Sizeof(c1))  // 16 (bytes)

// 複素数の演算
c3 := c1 + c2  // (1+2i) + (3+4i) = 4+6i
c4 := c1 * c2  // (1+2i) * (3+4i) = (3-8) + (4+6)i = -5+10i

内部的な演算:

複素数の乗算:(a+bi) × (c+di) = (ac-bd) + (ad+bc)i

メモリレベルでは:
1. 実部同士の乗算:a × c
2. 虚部同士の乗算:b × d
3. 実部の計算:(a×c) - (b×d)
4. 虚部の計算:(a×d) + (b×c)
5. 結果を新しいcomplex128に格納

---

3. 文字列型のメモリ構造

3.1 文字列の内部表現

🔑 Goの文字列は不変(immutable)なバイトスライスへのポインタです。

文字列のメモリレイアウト:

string構造体(runtime.stringStruct)
┌──────────────────┬─────────┐
│  pointer (8byte) │ len (8) │
│  データへのポインタ│  長さ   │
└────────┬─────────┴─────────┘
         │
         └─────> [actual byte data]  ← 不変領域
                 ['H']['e']['l']['l']['o']

実例:

s := "Hello"

// 内部構造
// pointer: バイト配列の先頭アドレス(例:0x1234)
// len: 5

import "unsafe"
import "reflect"

// 文字列構造体のサイズ
fmt.Println(unsafe.Sizeof(s))  // 16 bytes(64bitシステム)
                                // pointer(8) + len(8)

// 内部構造を見る
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
fmt.Printf("Data: %x, Len: %d\n", sh.Data, sh.Len)
// Data: 10c3a90, Len: 5

3.2 UTF-8エンコーディング

🔑 Goの文字列はUTF-8でエンコードされたバイト列です。

UTF-8の可変長エンコーディング:

┌─────────────────┬───────────┬─────────────────────────┐
│   文字範囲      │ バイト数  │      エンコード形式      │
├─────────────────┼───────────┼─────────────────────────┤
│ U+0000..U+007F  │  1 byte   │ 0xxxxxxx                │
│ U+0080..U+07FF  │  2 bytes  │ 110xxxxx 10xxxxxx       │
│ U+0800..U+FFFF  │  3 bytes  │ 1110xxxx 10xxxxxx ...   │
│ U+10000..U+10FFFF│ 4 bytes  │ 11110xxx 10xxxxxx ...   │
└─────────────────┴───────────┴─────────────────────────┘

具体例:

s := "Hello世界"

メモリ上のバイト配列:

'H'  'e'  'l'  'l'  'o'  '世'          '界'
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│48 │65 │6C │6C │6F │E4 │B8 │96 │E7 │95 │8C │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
 1   2   3   4   5   6   7   8   9  10  11

len(s) = 11 (バイト数)
len([]rune(s)) = 7 (文字数)

ASCIIは1バイト:'H' = 0x48
日本語は3バイト:'世' = 0xE4B896

💡 文字列のイテレーション:

s := "Hello世界"

// バイト単位のループ(非推奨)
for i := 0; i < len(s); i++ {
    fmt.Printf("%x ", s[i])
}
// 出力: 48 65 6c 6c 6f e4 b8 96 e7 95 8c

// rune単位のループ(推奨)
for i, r := range s {
    fmt.Printf("index: %d, rune: %c (U+%04X)\n", i, r, r)
}
// 出力:
// index: 0, rune: H (U+0048)
// index: 1, rune: e (U+0065)
// index: 2, rune: l (U+006C)
// index: 3, rune: l (U+006C)
// index: 4, rune: o (U+006F)
// index: 5, rune: 世 (U+4E16)  ← インデックス5から開始
// index: 8, rune: 界 (U+754C)  ← インデックス8から開始

3.3 文字列の不変性

🔑 文字列は不変なので、安全に共有できます。

s1 := "Hello"
s2 := s1  // ポインタのコピー(データはコピーされない)

// s1[0] = 'h'  // ❌ コンパイルエラー

メモリ図:

s1 ┌──────────┬────┐
   │ pointer  │ 5  │
   └────┬─────┴────┘
        │
        └──> ['H']['e']['l']['l']['o']  ← 読み取り専用
        ↑
        │
s2 ┌───┴──────┬────┐
   │ pointer  │ 5  │
   └──────────┴────┘

両方の文字列が同じメモリを参照
→ コピーコストなし
→ 安全(変更不可)

文字列を「変更」する場合:

s := "Hello"
s = "h" + s[1:]  // 新しい文字列を作成

// 内部的には:
// 1. "h"という新しい文字列を作成
// 2. s[1:]で"ello"という部分文字列を作成(ポインタ移動)
// 3. "h"と"ello"を結合して新しい文字列を作成
// 4. sを新しい文字列を参照するように更新

---

4. 配列のメモリレイアウト

4.1 配列は値型

🔑 配列は連続したメモリ領域に格納される値型です。

var arr [5]int = [5]int{1, 2, 3, 4, 5}

メモリレイアウト(連続配置):

arr(アドレス: 0x1000)
┌───────┬───────┬───────┬───────┬───────┐
│   1   │   2   │   3   │   4   │   5   │
└───────┴───────┴───────┴───────┴───────┘
0x1000  0x1008  0x1010  0x1018  0x1020

各要素:8バイト(int64の場合)
合計サイズ:40バイト

配列のコピー:

a := [3]int{1, 2, 3}
b := a  // 全体がコピーされる

b[0] = 100

fmt.Println(a)  // [1, 2, 3](変更されない)
fmt.Println(b)  // [100, 2, 3]

メモリ図:

a ┌───────┬───────┬───────┐
  │   1   │   2   │   3   │
  └───────┴───────┴───────┘
  0x1000  0x1008  0x1010

コピー操作(memcpy)
  ↓ ↓ ↓

b ┌───────┬───────┬───────┐
  │   1   │   2   │   3   │
  └───────┴───────┴───────┘
  0x2000  0x2008  0x2010

b[0]を変更
  ↓

b ┌───────┬───────┬───────┐
  │  100  │   2   │   3   │
  └───────┴───────┴───────┘
  0x2000  0x2008  0x2010

aは変更されない

⚠️ 大きな配列のコピーは高コスト:

type LargeArray [1000000]int

func processArray(arr LargeArray) {  // ❌ 8MB のコピー
    // ...
}

// 推奨:ポインタを渡す
func processArray(arr *LargeArray) {  // ✅ 8バイトのポインタ
    // ...
}

// または:スライスを使う
func processSlice(arr []int) {  // ✅ 24バイトのスライス構造体
    // ...
}

---

5. スライスの内部実装

5.1 スライスの構造体

🔑 スライスは配列への参照、長さ、容量の3要素を持つ構造体です。

スライスの内部構造(runtime.slice):

type slice struct {
    array unsafe.Pointer  // 配列へのポインタ(8バイト)
    len   int            // 長さ(8バイト)
    cap   int            // 容量(8バイト)
}
// 合計:24バイト(64bitシステム)

メモリ図:

s := make([]int, 3, 5)
s[0] = 10
s[1] = 20
s[2] = 30

スライス構造体 s
┌──────────────┬─────┬─────┐
│  array ptr   │ len │ cap │
│  0x1000      │  3  │  5  │
└──────┬───────┴─────┴─────┘
       │
       └──> 内部配列(容量5)
            ┌────┬────┬────┬────┬────┐
            │ 10 │ 20 │ 30 │ 0  │ 0  │
            └────┴────┴────┴────┴────┘
            0x1000              使用中  未使用
            ←───── len=3 ────→
            ←─────── cap=5 ──────────→

5.2 スライシング操作

🔑 スライシングは新しいスライス構造体を作るが、内部配列は共有します。

original := []int{1, 2, 3, 4, 5}
slice1 := original[1:4]  // [2, 3, 4]
slice2 := original[2:]   // [3, 4, 5]

メモリ図:

original
┌──────────┬─────┬─────┐
│ array    │ 5   │ 5   │
└────┬─────┴─────┴─────┘
     │
     └──> ┌───┬───┬───┬───┬───┐
          │ 1 │ 2 │ 3 │ 4 │ 5 │
          └───┴───┴───┴───┴───┘
          0x1000

slice1
┌──────────┬─────┬─────┐
│ array    │ 3   │ 4   │  len=3(要素2,3,4)
└────┬─────┴─────┴─────┘  cap=4(残り容量)
     │
     └──> (0x1000 + 8)
          ┌───┬───┬───┬───┐
          │ 2 │ 3 │ 4 │ 5 │
          └───┴───┴───┴───┘

slice2
┌──────────┬─────┬─────┐
│ array    │ 3   │ 3   │  len=3(要素3,4,5)
└────┬─────┴─────┴─────┘  cap=3(残り容量)
     │
     └──> (0x1000 + 16)
          ┌───┬───┬───┐
          │ 3 │ 4 │ 5 │
          └───┴───┴───┘

⚠️ スライスは内部配列を共有する:

original := []int{1, 2, 3, 4, 5}
slice1 := original[1:4]  // [2, 3, 4]

slice1[0] = 100  // 内部配列を変更

fmt.Println(original)  // [1, 100, 3, 4, 5]  変更される!
fmt.Println(slice1)    // [100, 3, 4]

5.3 appendの内部動作

🔑 appendは容量が足りない場合、新しい配列を確保します。

ケース1:容量が十分な場合

s := make([]int, 3, 5)  // len=3, cap=5
s = append(s, 40)       // len=4, cap=5

append前:
s ┌──────────┬─────┬─────┐
  │ array    │ 3   │ 5   │
  └────┬─────┴─────┴─────┘
       │
       └──> ┌────┬────┬────┬────┬────┐
            │ 10 │ 20 │ 30 │ 0  │ 0  │
            └────┴────┴────┴────┴────┘

append後:
s ┌──────────┬─────┬─────┐
  │ array    │ 4   │ 5   │  lenが増加
  └────┬─────┴─────┴─────┘
       │
       └──> ┌────┬────┬────┬────┬────┐
            │ 10 │ 20 │ 30 │ 40 │ 0  │  40を追加
            └────┴────┴────┴────┴────┘

ケース2:容量が不足する場合

s := []int{1, 2, 3}  // len=3, cap=3
s = append(s, 4)     // len=4, cap=6(拡張)

append前:
s ┌──────────┬─────┬─────┐
  │ 0x1000   │ 3   │ 3   │
  └────┬─────┴─────┴─────┘
       │
       └──> ┌───┬───┬───┐
            │ 1 │ 2 │ 3 │
            └───┴───┴───┘
            0x1000(古い配列)

容量不足を検出
↓
新しい配列を確保(通常2倍)
┌───┬───┬───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │ 0 │ 0 │
└───┴───┴───┴───┴───┴───┘
0x2000(新しい配列、cap=6)
↓
古いデータをコピー + 新要素を追加

append後:
s ┌──────────┬─────┬─────┐
  │ 0x2000   │ 4   │ 6   │  新しい配列を参照
  └────┬─────┴─────┴─────┘
       │
       └──> ┌───┬───┬───┬───┬───┬───┐
            │ 1 │ 2 │ 3 │ 4 │ 0 │ 0 │
            └───┴───┴───┴───┴───┴───┘
            0x2000

容量拡張のアルゴリズム:

// Go 1.18以降の拡張戦略(簡略版)
func growslice(oldCap, neededCap int) int {
    newCap := oldCap
    doubleCap := newCap + newCap

    if neededCap > doubleCap {
        newCap = neededCap
    } else {
        if oldCap < 256 {
            newCap = doubleCap  // 小さいスライスは2倍
        } else {
            // 大きいスライスは1.25倍に近い成長率
            for newCap < neededCap {
                newCap += (newCap + 3*256) / 4
            }
        }
    }

    return newCap
}

💡 パフォーマンス最適化:容量を事前確保

// ❌ 非効率:何度も再割り当て
var s []int
for i := 0; i < 1000; i++ {
    s = append(s, i)
}
// 再割り当て回数:約10回

// ✅ 効率的:容量を事前確保
s := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
    s = append(s, i)
}
// 再割り当て回数:0回

5.4 スライスのコピー

🔑 copyは要素を深くコピーします。

src := []int{1, 2, 3, 4, 5}
dst := make([]int, 3)
n := copy(dst, src)  // 3要素をコピー

fmt.Println(dst)  // [1, 2, 3]
fmt.Println(n)    // 3(コピーした要素数)

メモリ図:

src
┌──────────┬─────┬─────┐
│ 0x1000   │ 5   │ 5   │
└────┬─────┴─────┴─────┘
     │
     └──> ┌───┬───┬───┬───┬───┐
          │ 1 │ 2 │ 3 │ 4 │ 5 │
          └───┴───┴───┴───┴───┘
          0x1000

copy操作(memcpy)
     ↓ ↓ ↓

dst
┌──────────┬─────┬─────┐
│ 0x2000   │ 3   │ 3   │
└────┬─────┴─────┴─────┘
     │
     └──> ┌───┬───┬───┐
          │ 1 │ 2 │ 3 │  独立したメモリ
          └───┴───┴───┘
          0x2000

dst[0]を変更してもsrcは影響を受けない

---

6. マップの内部実装

6.1 マップの構造

🔑 マップはハッシュテーブルで実装されています。

マップの内部構造(簡略版):

runtime.hmap(マップのヘッダ)
┌─────────────┬─────────┬──────────┬────────┐
│  count      │ buckets │ oldbuckets│ hash0  │
│ (要素数)   │(配列)  │(拡張用) │(seed)│
└─────────────┴────┬────┴──────────┴────────┘
                    │
                    └──> バケット配列
                         ┌────────┬────────┬────────┐
                         │bucket 0│bucket 1│bucket 2│...
                         └────────┴────────┴────────┘
                              │
                              └──> バケット構造
                                   ┌──────────────────┐
                                   │ tophash[8]       │
                                   ├──────────────────┤
                                   │ keys[8]          │
                                   ├──────────────────┤
                                   │ values[8]        │
                                   ├──────────────────┤
                                   │ overflow pointer │
                                   └──────────────────┘

ハッシュ関数の流れ:

m := make(map[string]int)
m["hello"] = 42

1. キーをハッシュ化
   "hello" → hash("hello") → 0x7A8B9C1D2E3F4567

2. バケット番号を計算(下位ビット使用)
   hash & (bucketCount - 1) → bucket 7

3. tophashを計算(上位ビット使用)
   hash >> 56 → 0x7A

4. バケットに格納
   bucket[7]
   ┌──────────┐
   │ tophash  │ [0x7A, ...]
   ├──────────┤
   │ keys     │ ["hello", ...]
   ├──────────┤
   │ values   │ [42, ...]
   └──────────┘

6.2 マップの検索

🔑 マップの検索はO(1)の平均時間計算量です。

value, ok := m["hello"]

検索プロセス:

1. キーのハッシュを計算
   "hello" → 0x7A8B9C1D2E3F4567

2. バケット番号を計算
   hash & (bucketCount - 1) → bucket 7

3. tophashを計算
   hash >> 56 → 0x7A

4. バケット内を線形探索
   for i := 0; i < 8; i++ {
       if tophash[i] == 0x7A {
           if keys[i] == "hello" {
               return values[i], true
           }
       }
   }

5. オーバーフローバケットをチェック
   if overflow != nil {
       // オーバーフローバケットも探索
   }

6. 見つからない場合
   return zeroValue, false

6.3 マップの拡張

🔑 マップは負荷率が高くなると自動的に拡張されます。

拡張のトリガー:

負荷率(load factor)= 要素数 / (バケット数 × 8)

拡張条件:
1. 負荷率 > 6.5  → 2倍に拡張
2. オーバーフローバケットが多すぎる → 同じサイズで再編成

拡張プロセス:

拡張前(4バケット):
┌────┬────┬────┬────┐
│ b0 │ b1 │ b2 │ b3 │  各バケットに6-8要素
└────┴────┴────┴────┘  負荷率 > 6.5

拡張開始:
┌────┬────┬────┬────┐
│ b0 │ b1 │ b2 │ b3 │  oldbuckets
└────┴────┴────┴────┘
       ↓ 段階的に移行(incremental)
┌────┬────┬────┬────┬────┬────┬────┬────┐
│ b0 │ b1 │ b2 │ b3 │ b4 │ b5 │ b6 │ b7 │  新しいbuckets
└────┴────┴────┴────┴────┴────┴────┴────┘

拡張完了:
┌────┬────┬────┬────┬────┬────┬────┬────┐
│ b0 │ b1 │ b2 │ b3 │ b4 │ b5 │ b6 │ b7 │  8バケット
└────┴────┴────┴────┴────┴────┴────┴────┘

💡 段階的拡張(Incremental Resizing):

Goのマップは一度に全て移行せず、アクセスごとに少しずつ移行:

map操作ごとに:
1. 要求された操作を実行
2. 最大2つのバケットを旧配列から新配列に移行
3. すべて移行完了するまで繰り返す

メリット:
- 大きな遅延を回避
- 一時的なメモリ使用量を削減

6.4 マップの削除

delete(m, "hello")

削除プロセス:

1. キーを検索(検索と同じプロセス)

2. 見つかった場合:
   - tophash[i] を空のマーカーに設定
   - keys[i] をゼロ値にクリア
   - values[i] をゼロ値にクリア

3. 見つからない場合:
   - 何もしない(エラーにならない)

メモリ図:
削除前:
┌─────────┐
│ tophash │ [0x7A, 0x3B, 0x9F, ...]
├─────────┤
│ keys    │ ["hello", "world", "foo", ...]
├─────────┤
│ values  │ [42, 100, 200, ...]
└─────────┘

delete(m, "hello")後:
┌─────────┐
│ tophash │ [0x00, 0x3B, 0x9F, ...]  空マーカー
├─────────┤
│ keys    │ ["", "world", "foo", ...]  ゼロ値
├─────────┤
│ values  │ [0, 100, 200, ...]         ゼロ値
└─────────┘

---

7. 型変換と型アサーション

7.1 暗黙的変換がない理由

🔑 Goは意図しない型変換によるバグを防ぐため、暗黙的変換を禁止しています。

var i int = 42
var f float64 = i  // ❌ エラー!

// 明示的変換が必要
var f float64 = float64(i)  // ✅ OK

暗黙的変換の危険性(C言語の例):

// C言語では暗黙的変換が許可される
int x = 300;
char c = x;  // 暗黙的に変換(データ損失)
printf("%d\n", c);  // 44(300 & 0xFF = 44)

💡 Goの設計思想:明示性優先

// 各変換は意図を明示
var i int64 = 42
var i32 int32 = int32(i)    // 明示的に縮小
var f float64 = float64(i)  // 明示的に型変更

// コンパイラが意図を確認できる
var x int16 = 32767
var y int16 = 1
// var z int16 = x + y  // オーバーフローの可能性を認識

7.2 型変換の内部動作

数値型の変換:

var i int64 = 1000
var i32 int32 = int32(i)

メモリレベル:

i(int64):
┌────────────────────────────────────────────────────────────────┐
│ 0x00000000000003E8                                             │
└────────────────────────────────────────────────────────────────┘
 64 bits

int32(i)の変換:
下位32ビットを切り出す
┌────────────────────────────────────┐
│ 0x000003E8                         │
└────────────────────────────────────┘
 32 bits

i32(int32):
┌────────────────────────────────────┐
│ 0x000003E8 = 1000                  │
└────────────────────────────────────┘

⚠️ オーバーフローの危険性:

var i int64 = 0x1FFFFFFFF  // 8,589,934,591
var i32 int32 = int32(i)

fmt.Printf("i64: %d (0x%X)\n", i, i)
fmt.Printf("i32: %d (0x%X)\n", i32, i32)

// 出力:
// i64: 8589934591 (0x1FFFFFFFF)
// i32: -1 (0xFFFFFFFF)  ← 上位ビットが切り捨てられる

浮動小数点への変換:

var i int = 42
var f float64 = float64(i)

変換プロセス:

1. 整数値(42)をIEEE 754形式に変換
   42 (10進) = 101010 (2進) = 1.0101 × 2^5

2. IEEE 754形式で表現
   符号:0(正)
   指数:5 + 1023 = 1028 = 10000000100 (2進)
   仮数:0101000...(52ビット)

3. float64メモリに格納
   ┌─┬───────────┬────────────────────────────────────────────┐
   │0│10000000100│0101000000000000000000000000000000000000000│
   └─┴───────────┴────────────────────────────────────────────┘

7.3 型アサーションの内部動作

🔑 型アサーションはインターフェース値の動的型をチェックします。

var i interface{} = 42

// 型アサーション
value, ok := i.(int)

インターフェース値の構造:

interface{}の内部表現(runtime.eface)
┌──────────────┬─────────────────┐
│  _type       │     data        │
│ (型情報)      │  (値へのポインタ)│
└──────┬───────┴────────┬────────┘
       │                │
       └──> *_type      └──> 実際の値
            ┌────────┐       ┌────┐
            │ int型  │       │ 42 │
            └────────┘       └────┘

型アサーションのプロセス:

value, ok := i.(int)

1. インターフェース値の型情報を取得
   _type → *int型

2. 要求された型と比較
   要求: int型
   実際: int型
   → 一致!

3. データポインタから値をコピー
   value = *data  // 42

4. 成功フラグを設定
   ok = true

メモリ操作:
┌─────────┬────────┐
│ _type   │ data   │  interface{}
└────┬────┴───┬────┘
     │        │
     ↓        ↓
  確認:int?  値をコピー
     YES!    value = 42
            ok = true

型アサーション失敗の場合:

var i interface{} = "hello"
value, ok := i.(int)

1. 型情報を比較
   要求: int型
   実際: string型
   → 不一致!

2. ゼロ値を返す
   value = 0  // intのゼロ値

3. 失敗フラグを設定
   ok = false

7.4 型スイッチの実装

func describe(i interface{}) {
    switch v := i.(type) {
    case int:
        fmt.Printf("整数: %d\n", v)
    case string:
        fmt.Printf("文字列: %s\n", v)
    default:
        fmt.Printf("不明な型: %T\n", v)
    }
}

型スイッチの内部動作:

コンパイル後の疑似コード:

func describe(i interface{}) {
    // インターフェースの型情報を取得
    _type := i._type

    // 各caseで型を比較
    if _type == typeof(int) {
        v := *(*int)(i.data)
        fmt.Printf("整数: %d\n", v)
        return
    }

    if _type == typeof(string) {
        v := *(*string)(i.data)
        fmt.Printf("文字列: %s\n", v)
        return
    }

    // default
    fmt.Printf("不明な型: %T\n", i)
}

---

8. 名前付き型とエイリアス

8.1 type定義の違い

🔑 Goには2種類の型定義があります。

名前付き型(Named Type):

type Celsius float64    // 新しい型を定義
type Fahrenheit float64

メモリ上は同じfloat64だが、型システムでは別物:

Celsius
┌─────────────────────────────────────────────────────────────┐
│  underlying type: float64                                   │
│  methods: 独自のメソッドを持てる                              │
└─────────────────────────────────────────────────────────────┘

Fahrenheit
┌─────────────────────────────────────────────────────────────┐
│  underlying type: float64                                   │
│  methods: Celsiusとは別のメソッド                            │
└─────────────────────────────────────────────────────────────┘

float64
┌─────────────────────────────────────────────────────────────┐
│  組み込み型                                                  │
└─────────────────────────────────────────────────────────────┘

互換性なし:
var c Celsius = 100.0
var f Fahrenheit = c  // ❌ エラー!型が異なる

型エイリアス(Type Alias):

type MyInt = int  // エイリアス(完全に同じ型)

MyIntとintは完全に同一:

MyInt = int
┌─────────────────────────────────────────────────────────────┐
│  同じ型(エイリアスは名前だけ異なる)                         │
│  methods: intのメソッドをすべて共有                          │
└─────────────────────────────────────────────────────────────┘

互換性あり:
var x MyInt = 42
var y int = x  // ✅ OK(同じ型)

8.2 underlying typeの概念

🔑 すべての名前付き型は「underlying type」を持ちます。

type Celsius float64

func main() {
    var c Celsius = 100.0

    // underlying typeへの変換
    var f float64 = float64(c)  // OK

    // underlying typeからの変換
    c = Celsius(98.6)  // OK
}

underlying typeの連鎖:

type A int
type B A
type C B

// underlying typeの連鎖
// C → B → A → int
// すべてのunderlying typeは「int」

型の階層:

C (型)
└─> underlying type: B
    └─> underlying type: A
        └─> underlying type: int (基本型)

変換の必要性:
var a A = 10
var b B = a     // ❌ エラー(AとBは異なる型)
var b B = B(a)  // ✅ OK(明示的変換)

var c C = C(b)  // ✅ OK

// intへの変換
var i int = int(c)  // ✅ OK(underlying typeへの変換)

8.3 メソッドの継承

🔑 名前付き型は独自のメソッドを持てますが、継承はありません。

type Celsius float64

func (c Celsius) ToFahrenheit() Fahrenheit {
    return Fahrenheit(c*9/5 + 32)
}

func main() {
    c := Celsius(100)
    f := c.ToFahrenheit()  // Celsiusのメソッド

    // float64には変換できるが、メソッドは失われる
    var x float64 = float64(c)
    // x.ToFahrenheit()  // ❌ エラー!メソッドなし
}

メソッドの添付:

Celsius型
┌─────────────────────────────────────────┐
│ underlying type: float64                │
│                                         │
│ methods:                                │
│   - ToFahrenheit() Fahrenheit           │
│   - String() string                     │
└─────────────────────────────────────────┘

float64への変換
         ↓
┌─────────────────────────────────────────┐
│ float64                                 │
│                                         │
│ methods:                                │
│   (メソッドなし)                         │
└─────────────────────────────────────────┘

メソッドは型に紐づく!

---

9. ゼロ値の設計思想

9.1 ゼロ値とは

🔑 Goのすべての型には「ゼロ値」があります。

各型のゼロ値一覧:

┌──────────────────┬─────────────────────────┐
│      型          │       ゼロ値             │
├──────────────────┼─────────────────────────┤
│ bool             │ false                   │
│ 数値型           │ 0                       │
│ string           │ ""(空文字列)           │
│ pointer          │ nil                     │
│ slice            │ nil                     │
│ map              │ nil                     │
│ channel          │ nil                     │
│ interface        │ nil                     │
│ function         │ nil                     │
│ struct           │ 各フィールドのゼロ値      │
│ array            │ 各要素のゼロ値           │
└──────────────────┴─────────────────────────┘

ゼロ値の初期化:

var i int         // 0
var f float64     // 0.0
var s string      // ""
var b bool        // false
var p *int        // nil
var sl []int      // nil
var m map[string]int  // nil

// 構造体のゼロ値
type Person struct {
    Name string
    Age  int
}
var person Person  // Person{Name: "", Age: 0}

9.2 ゼロ値が有用な理由

💡 ゼロ値はそのまま使える値として設計されています。

例1:sync.Mutex

type SafeCounter struct {
    mu    sync.Mutex  // ゼロ値で使える!
    count int
}

func (c *SafeCounter) Inc() {
    c.mu.Lock()    // 初期化不要
    c.count++
    c.mu.Unlock()
}

func main() {
    var counter SafeCounter  // ゼロ値で初期化
    counter.Inc()            // そのまま使える
}

例2:bytes.Buffer

var buf bytes.Buffer  // ゼロ値で使える

buf.WriteString("Hello")  // 初期化不要
buf.WriteString(" World")
fmt.Println(buf.String())  // "Hello World"

例3:スライスのnil

var s []int  // nil

// nilスライスはappendできる
s = append(s, 1, 2, 3)  // OK

// nilスライスのlenとcapは0
fmt.Println(len(s))  // 0 → 3(append後)
fmt.Println(cap(s))  // 0 → 4(append後)

9.3 ゼロ値のメモリ表現

基本型のゼロ値:

int(ゼロ値: 0)
┌────────────────────────────────────────────────────────────────┐
│ 0x0000000000000000                                             │
└────────────────────────────────────────────────────────────────┘
 64 bits すべて0

bool(ゼロ値: false)
┌────┐
│ 0  │
└────┘
 8 bits

string(ゼロ値: "")
┌──────────────────┬─────────┐
│  pointer = nil   │ len = 0 │
│  0x0000000000    │    0    │
└──────────────────┴─────────┘

slice(ゼロ値: nil)
┌──────────────────┬─────────┬─────────┐
│  array = nil     │ len = 0 │ cap = 0 │
│  0x0000000000    │    0    │    0    │
└──────────────────┴─────────┴─────────┘

構造体のゼロ値:

type Point struct {
    X int
    Y int
}

var p Point  // ゼロ値

メモリレイアウト:
┌─────────────┬─────────────┐
│  X = 0      │  Y = 0      │
│  8 bytes    │  8 bytes    │
└─────────────┴─────────────┘

すべてのフィールドがゼロ値で初期化される
→ メモリはゼロクリアされる

9.4 nilの特殊性

🔑 nilはポインタ型、スライス、マップ、チャネル、インターフェース、関数のゼロ値です。

nilの挙動の違い:

// スライス:nilでもappend可能
var s []int  // nil
s = append(s, 1)  // ✅ OK

// マップ:nilに書き込み不可
var m map[string]int  // nil
// m["key"] = 1  // ❌ panic: assignment to entry in nil map
m = make(map[string]int)
m["key"] = 1  // ✅ OK

// ポインタ:nilをデリファレンス不可
var p *int  // nil
// x := *p  // ❌ panic: nil pointer dereference

nilインターフェースの罠:

func returnsError() error {
    var p *MyError = nil
    return p  // 非nilインターフェース!
}

func main() {
    err := returnsError()
    fmt.Println(err == nil)  // false(予想外!)
}

なぜfalseなのか?

interface{}の内部構造:
┌──────────────┬─────────────────┐
│  _type       │     data        │
└──────────────┴─────────────────┘

returnsError()が返す値:
┌──────────────┬─────────────────┐
│ *MyError型   │     nil         │
└──────────────┴─────────────────┘
     ↑
  型情報あり!→ nilではない

真のnilインターフェース:
┌──────────────┬─────────────────┐
│    nil       │     nil         │
└──────────────┴─────────────────┘

正しい書き方:

func returnsError() error {
    var p *MyError = nil
    if p != nil {
        return p
    }
    return nil  // ✅ 真のnilを返す
}

---

10. 実践的な型の使い方

10.1 型の選択基準

整数型の選択:

// ✅ 推奨:通常の整数
age := 25  // int(デフォルト)

// ✅ 推奨:バイトデータ
data := []byte{0x48, 0x65, 0x6C, 0x6C, 0x6F}

// ✅ 推奨:Unicode文字
char := 'A'  // rune(int32)

// ❌ 避ける:過剰な最適化
var age int8 = 25  // intで十分

浮動小数点の選択:

// ✅ 推奨:科学計算、グラフィックス
var distance float64 = 384400.0  // km

// ❌ 避ける:金額計算
var price float64 = 19.99  // 誤差の危険性

// ✅ 推奨:金額は整数(セント単位)
var priceInCents int = 1999  // 19.99ドル

文字列 vs バイトスライス:

// ✅ 文字列:テキストデータ、不変
name := "Alice"

// ✅ バイトスライス:バイナリデータ、可変
data := []byte{0xFF, 0xFE, 0xFD}

// 変換
s := "Hello"
b := []byte(s)  // stringからbyte sliceへ(コピー)
s = string(b)   // byte sliceからstringへ(コピー)

10.2 パフォーマンスの考慮

スライスの事前確保:

// ❌ 非効率
var result []int
for i := 0; i < 1000; i++ {
    result = append(result, i)
}
// 再割り当て回数:約10回(0→1→2→4→8→16→...→1024)

// ✅ 効率的
result := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
    result = append(result, i)
}
// 再割り当て回数:0回

マップの事前確保:

// ❌ 非効率
m := make(map[string]int)

// ✅ 効率的(要素数がわかっている場合)
m := make(map[string]int, 1000)

文字列の結合:

// ❌ 非効率(多数の結合)
var result string
for i := 0; i < 1000; i++ {
    result += fmt.Sprintf("%d,", i)  // 毎回新しい文字列を作成
}

// ✅ 効率的
var builder strings.Builder
builder.Grow(5000)  // 容量を事前確保
for i := 0; i < 1000; i++ {
    builder.WriteString(fmt.Sprintf("%d,", i))
}
result := builder.String()

10.3 型安全性の活用

名前付き型で意図を明示:

type UserID int
type ProductID int

func getUser(id UserID) User { /* ... */ }
func getProduct(id ProductID) Product { /* ... */ }

func main() {
    userID := UserID(123)
    productID := ProductID(456)

    user := getUser(userID)  // ✅ OK
    // user := getUser(productID)  // ❌ コンパイルエラー!
}

カスタム型でバリデーション:

type Email string

func NewEmail(s string) (Email, error) {
    if !strings.Contains(s, "@") {
        return "", errors.New("invalid email")
    }
    return Email(s), nil
}

func sendEmail(to Email) error {
    // toは必ず有効なメール形式
    // ...
}

---

11. 自己チェック問題

問題1:型システムの基礎

🔑 Q: Goが静的型付け言語であることのメリットを3つ挙げてください。

解答を見る

A:

  • 型エラーをコンパイル時に検出:実行前にバグを発見できる
  • パフォーマンスの向上:実行時の型チェックが不要
  • コードの可読性向上:型がドキュメントとして機能

問題2:整数のオーバーフロー

🔑 Q: 以下のコードの出力を予測してください。

var x int8 = 127
x = x + 1
fmt.Println(x)

解答を見る

A: -128

理由:

  • int8の範囲:-128~127
  • 127 + 1 = 128はオーバーフローし、-128にラップアラウンド
  • ビットレベル:01111111 + 1 = 10000000(2の補数表現で-128)

問題3:浮動小数点の精度

🔑 Q: なぜ 0.1 + 0.2 == 0.3false になるのですか?

解答を見る

A: 0.1と0.2は二進数で正確に表現できないため。

詳細:

  • 0.1 (10進) = 0.0001100110011...(2進、無限小数)
  • IEEE 754形式で丸め誤差が発生
  • 結果:0.30000000000000004

解決策:

epsilon := 0.00001
if math.Abs((0.1 + 0.2) - 0.3) < epsilon {
    // ほぼ等しい
}

問題4:文字列の長さ

🔑 Q: 以下のコードの出力を予測してください。

s := "こんにちは"
fmt.Println(len(s))
fmt.Println(len([]rune(s)))

解答を見る

A:

15
5

理由:

  • len(s) はバイト数を返す(UTF-8で日本語は3バイト × 5文字 = 15)
  • len([]rune(s)) は文字数を返す(5文字)

問題5:配列とスライスの違い

🔑 Q: 以下のコードの出力を予測してください。

a := [3]int{1, 2, 3}
b := a
b[0] = 100

fmt.Println(a[0])
fmt.Println(b[0])

解答を見る

A:

1
100

理由:

  • 配列は値型なので、b := a で全体がコピーされる
  • bを変更してもaは影響を受けない

問題6:スライスの容量

🔑 Q: 以下のコードで、スライス s の長さと容量を答えてください。

s := make([]int, 3, 5)
s = append(s, 10, 20)

解答を見る

A:

  • 長さ(len):5
  • 容量(cap):5

理由:

  • 初期:len=3, cap=5
  • append後:len=5(3+2要素), cap=5(拡張なし)

問題7:スライスの共有

🔑 Q: 以下のコードの出力を予測してください。

original := []int{1, 2, 3, 4, 5}
slice1 := original[1:4]
slice1[0] = 100

fmt.Println(original)
fmt.Println(slice1)

解答を見る

A:

[1 100 3 4 5]
[100 3 4]

理由:

  • スライシングは内部配列を共有する
  • slice1[0]original[1] と同じメモリを参照
  • 片方を変更すると、もう片方も変更される

問題8:マップのゼロ値

🔑 Q: 以下のコードは正しく動作しますか?エラーになる場合、理由を説明してください。

var m map[string]int
value := m["key"]
fmt.Println(value)

解答を見る

A: 正しく動作します。出力は 0

理由:

  • nilマップからの読み取りは許可されている
  • 存在しないキーのゼロ値(intの場合は0)を返す
  • 書き込みはpanic(m["key"] = 1 は不可)

問題9:型アサーション

🔑 Q: 以下のコードの出力を予測してください。

var i interface{} = "hello"
value, ok := i.(int)
fmt.Println(value, ok)

解答を見る

A:

0 false

理由:

  • i は実際にはstring型
  • int型への型アサーションは失敗
  • ゼロ値(0)とfalseが返される

問題10:nilインターフェース

🔑 Q: 以下のコードの出力を予測してください。

func getError() error {
    var err *os.PathError = nil
    return err
}

func main() {
    err := getError()
    fmt.Println(err == nil)
}

解答を見る

A: false

理由:

  • インターフェースは(型情報、値)のペア
  • 返されるerrorは(*os.PathError型、nil値)
  • 型情報があるため、nilインターフェースではない
  • 真のnilは(nil型、nil値)

問題11:appendの再割り当て

🔑 Q: 以下のコードで、新しい配列が確保されるのは何回目のappendですか?

s := make([]int, 0, 4)
s = append(s, 1)  // 1回目
s = append(s, 2)  // 2回目
s = append(s, 3)  // 3回目
s = append(s, 4)  // 4回目
s = append(s, 5)  // 5回目

解答を見る

A: 5回目

理由:

  • 初期容量:4
  • 1~4回目:容量内なので再割り当てなし
  • 5回目:容量不足(len=4, cap=4)→新しい配列確保(cap=8に拡張)

問題12:型の互換性

🔑 Q: 以下のコードはコンパイルできますか?

type Celsius float64
type Fahrenheit float64

var c Celsius = 100.0
var f Fahrenheit = c

解答を見る

A: コンパイルエラー

理由:

  • CelsiusとFahrenheitは異なる名前付き型
  • underlying typeは同じ(float64)だが、型は互換性がない
  • 明示的変換が必要:var f Fahrenheit = Fahrenheit(c)

問題13:型エイリアス

🔑 Q: 以下の2つの型定義の違いを説明してください。

type MyInt1 int   // (1)
type MyInt2 = int // (2)

解答を見る

A:

  • (1) 名前付き型:intとは異なる新しい型。独自のメソッドを持てる。
  • (2) 型エイリアス:intの別名。完全に同じ型として扱われる。

互換性:

var i1 MyInt1 = 10
var i2 MyInt2 = 20
var i int = 30

// i = i1  // ❌ エラー(異なる型)
i = i2     // ✅ OK(同じ型)

問題14:ゼロ値の有用性

🔑 Q: 以下のコードは正しく動作しますか?理由を説明してください。

var mu sync.Mutex
mu.Lock()
// クリティカルセクション
mu.Unlock()

解答を見る

A: 正しく動作します。

理由:

  • sync.Mutex のゼロ値はそのまま使える(Unlocked状態)
  • 明示的な初期化が不要
  • Goの「ゼロ値は有用」という設計思想の好例

問題15:パフォーマンス最適化

🔑 Q: 10,000要素をappendする場合、以下のどちらが効率的ですか?

// A
var s []int
for i := 0; i < 10000; i++ {
    s = append(s, i)
}

// B
s := make([]int, 0, 10000)
for i := 0; i < 10000; i++ {
    s = append(s, i)
}

解答を見る

A: Bが効率的

理由:

  • A: 容量不足のたびに再割り当て(約14回)、毎回コピーが発生
  • B: 容量を事前確保、再割り当てなし、コピーなし

パフォーマンス差:

  • A: O(n log n) の時間(再割り当てとコピー)
  • B: O(n) の時間(追加のみ)

---

まとめ

この章では、Goの型システムをメモリレベルから深く理解しました。

重要ポイント

🔑 型システムの設計:

  • 静的型付け:コンパイル時にすべての型をチェック
  • 型安全性:意図しない型変換を防ぐ
  • 明示的変換:暗黙的変換は禁止

🔑 メモリレイアウト:

  • 整数:固定サイズ、2の補数表現
  • 浮動小数点:IEEE 754形式、精度に注意
  • 文字列:不変、UTF-8、ポインタ+長さ
  • スライス:ポインタ+長さ+容量の構造体
  • マップ:ハッシュテーブル、段階的拡張

🔑 型変換:

  • 明示的変換type(value) 形式
  • 型アサーション:インターフェースの動的型チェック
  • 名前付き型:underlying typeは共有、メソッドは独立

🔑 ゼロ値:

  • すべての型にゼロ値:未初期化でも安全
  • ゼロ値は有用:そのまま使える設計
  • nil:ポインタ型、スライス、マップ、インターフェースのゼロ値

実践での使い分け

推奨パターン:

  • 整数:デフォルトは int、用途に応じて byterune
  • 浮動小数点:科学計算は float64、金額は整数
  • 文字列:テキストは string、バイナリは []byte
  • コレクション:可変長は slice、キー検索は map
  • 型安全性:名前付き型で意図を明示

⚠️ 避けるべきパターン:

  • 過剰な最適化(int8 の乱用)
  • 浮動小数点での金額計算
  • 容量を確保しない大量の append
  • nilマップへの書き込み

次のステップ

次の章では、制御構造(if、for、switch)を学びます。型システムの知識を活かして、より安全で効率的なコードを書けるようになりましょう。

💡 継続学習のヒント:

  • 各型のメモリサイズを unsafe.Sizeof() で確認
  • スライスの容量変化を観察
  • 型アサーションと型スイッチを使い分ける
  • カスタム型を活用して型安全性を高める