Day 1: Go言語の核心 - 解説
学習目標
このDay 1では、他言語経験者がGoの本質を理解するための核心概念を学びます:
- 型システムの哲学: なぜGoは暗黙の変換を禁止するのか
- メモリモデルの明示性: 値とポインタの使い分け
- スライスの内部構造: 参照型の仕組みとメモリ効率
- ゼロ値の設計: 初期化不要で使える型の設計哲学
- C++のビルド時間が長すぎる(数時間)
- 動的言語(Python等)は開発は速いが実行が遅い
- 並行処理が難しい(マルチコアCPUを活かせない)
- コードの可読性が低い(言語機能が多すぎる)
本日のまとめ
今日学んだGo言語の核心:
| 概念 | 説明 | 重要度 | 他言語との違い |
|---|---|---|---|
| 設計思想 | シンプルさと効率性の追求 | ★★★ | 機能を削ぎ落とす哲学 |
| 型システム | 静的型付け、暗黙の変換なし | ★★★ | 型安全性を最優先 |
| ゼロ値 | すべての変数は初期化される | ★★★ | nil/undefinedがない |
| ポインタ | 明示的だが安全なメモリアクセス | ★★★ | ポインタ演算禁止 |
| 参照型 | スライス、マップ、チャネル | ★★☆ | 内部構造が明確 |
| エラー処理 | 例外ではなく値として扱う | ★★★ | try-catchなし |
---
Goの設計哲学: なぜこの言語が生まれたのか
背景: Googleが抱えていた問題
2007年、Googleのエンジニアは次の問題に直面していました:
Goの設計目標
| 目標 | アプローチ | 結果 |
|---|---|---|
| 高速なコンパイル | 依存関係の明示化、シンプルな文法 | 数秒でビルド完了 |
| 実行速度 | ネイティブコード生成、効率的なGC | C/C++に近い速度 |
| 並行処理 | goroutineとchannel | 簡単に並行処理を記述 |
| 可読性 | 機能を最小限に、統一されたフォーマット | 誰が書いても同じコード |
他言語との比較マトリクス
複雑さ vs パフォーマンス
複雑さ
↑
│ C++
│ ●
│
│ Java
│ ●
│
│ Go ●
│
│ Python
│ ●
│ JavaScript
│ ●
│
└─────────────────────────→ パフォーマンス
遅い 速い
Goは「シンプルさ」と「高パフォーマンス」の両立を目指しています。
Goの「引き算の哲学」
多くの言語が機能を追加していく中、Goは意図的に機能を削ぎ落としました:
| 機能 | 他言語 | Go | 理由 |
|---|---|---|---|
| 関数オーバーロード | ◯ | × | 名前の曖昧さを排除 |
| 演算子オーバーロード | ◯ | × | 予期しない動作を防ぐ |
| 継承 | ◯ | × | コンポジションを推奨 |
| 例外 | ◯ | × | エラーを明示的に |
| ジェネリクス | ◯ | △ (1.18以降) | 必要最小限の実装 |
| マクロ | ◯ | × | コードの透明性 |
「少ないほど多い(Less is more)」- Rob Pike
---
メンタルモデル: Goプログラマーの思考法
1. 明示性を重視する
他言語(暗黙的):
# Python - 多くのことが暗黙的に行われる
def process(data):
return data * 2 # int/str/list、何でも動く
result = process(5) # 10
result = process("Hi") # "HiHi"
result = process([1, 2]) # [1, 2, 1, 2]
Go(明示的):
// Go - 型を明示、意図を明確に
func processInt(data int) int {
return data * 2 // intのみ
}
func processString(data string) string {
return data + data // stringのみ
}
// 使用時に型が明確
result1 := processInt(5) // int
result2 := processString("Hi") // string
メンタルモデル:
- 「動くかもしれない」より「間違いなく動く」
- 型エラーは開発時に検出したい(ランタイムではなく)
- コードを読む人(未来の自分を含む)のために明示する
- IDEの補完が正確に効く
2. コピーか参照かを意識する
他言語(隠蔽):
# Python - 実装の詳細が隠されている
def modify(lst):
lst.append(1) # 元のリストが変わる
def modify_num(n):
n = n + 1 # 元の数値は変わらない
my_list = []
modify(my_list) # [1] - 変わる
my_num = 5
modify_num(my_num) # 5 - 変わらない
Go(明示):
// Go - コピーか参照かを明示的に選ぶ
func modifySlice(s []int) []int {
s = append(s, 1)
return s // appendは新しいスライスを返す可能性がある
}
func modifySlicePointer(s *[]int) {
*s = append(*s, 1) // ポインタ経由で確実に変更
}
// 構造体の場合
type User struct {
Name string
Age int
}
func modifyByValue(u User) User {
u.Age++
return u // コピーを返す
}
func modifyByPointer(u *User) {
u.Age++ // 元を変更
}
メンタルモデル:
- 小さい値(int, float, 小さいstruct)→ 値渡し(コピー)
- 大きい値、変更が必要 → ポインタ渡し(参照)
- スライス、マップ → 既に参照的な性質を持つ(内部でポインタを使用)
- 関数シグネチャを見れば、変更されるかどうかわかる
3. ゼロ値で有効な設計
他言語(初期化必須):
// Java - 必ず初期化が必要
List<Integer> list = new ArrayList<>(); // 初期化必須
Map<String, Integer> map = new HashMap<>(); // 初期化必須
// 初期化を忘れるとNullPointerException
String s;
System.out.println(s.length()); // エラー!
Go(ゼロ値で有効):
// Go - ゼロ値で使える設計
var mu sync.Mutex // 初期化不要
mu.Lock()
mu.Unlock()
var buf bytes.Buffer // 初期化不要
buf.WriteString("hello")
fmt.Println(buf.String())
var wg sync.WaitGroup // 初期化不要
wg.Add(1)
go func() {
defer wg.Done()
// ...
}()
wg.Wait()
メンタルモデル:
- 型を設計する際、ゼロ値で有効にできないか考える
make()やnew()が不要なら、より使いやすいAPI- 初期化を忘れるバグを防ぐ
- シンプルなAPIを提供する
ゼロ値の一覧:
var i int // 0
var f float64 // 0.0
var b bool // false
var s string // ""
var p *int // nil
var sl []int // nil (でもappendは使える)
var m map[string]int // nil (読み込みはOK、書き込みはNG)
var fn func() // nil
4. エラーは値として扱う
他言語(例外):
# Python - try-catchで制御フロー
try:
file = open("data.txt")
data = file.read()
except FileNotFoundError:
data = ""
except IOError as e:
log.error(f"IO error: {e}")
data = ""
finally:
if file:
file.close()
// JavaScript - Promise/async-awaitでエラー処理
async function loadData() {
try {
const response = await fetch('data.json');
const data = await response.json();
return data;
} catch (error) {
console.error(error);
return null;
}
}
Go(エラー値):
// Go - エラーを明示的にチェック
file, err := os.Open("data.txt")
if err != nil {
// エラー処理
log.Printf("failed to open file: %v", err)
return
}
defer file.Close() // 必ず実行される
data, err := io.ReadAll(file)
if err != nil {
log.Printf("failed to read file: %v", err)
return
}
// dataを使用
fmt.Println(string(data))
メンタルモデル:
- エラーは例外的な状況ではなく、通常の結果の一つ
- エラー処理を強制することで、堅牢なコードを書く
- 制御フローが追いやすい(例外で飛ばない)
- エラーハンドリングがコードの一部として可視化される
---
詳細なアルゴリズム分析
1. 型システムとパフォーマンス
型チェックのコスト比較
動的型付け言語(Python):
def add(a, b):
return a + b
# 内部で実行される処理:
# 1. a の型オブジェクトを取得(メモリアクセス)
# 2. b の型オブジェクトを取得(メモリアクセス)
# 3. a.__add__ メソッドを探索(辞書検索)
# 4. 型が一致するかチェック
# 5. 適切な加算関数を呼び出し(関数呼び出しオーバーヘッド)
# 合計: 約100-200ns(実装による)
静的型付け(Go):
func add(a, b int) int {
return a + b
}
// コンパイル時に生成されるアセンブリ:
// MOV AX, [a] ; a をレジスタに読み込み
// ADD AX, [b] ; b を加算
// RET ; 結果を返す
// 合計: 約0.3-0.5ns
パフォーマンス比較実測:
Python (CPython 3.11): 約150ns/op
JavaScript (V8): 約 10ns/op (JIT最適化後)
Java (HotSpot): 約 2ns/op (JIT最適化後)
Go: 約 0.5ns/op
C: 約 0.3ns/op
結論: Goの型システムはC並みのパフォーマンス
ジェネリクスの実装戦略
Go 1.18以降のジェネリクスは「GCShape Stenciling」という手法を使用:
// ジェネリクス関数
func Add[T int | int64 | float64](a, b T) T {
return a + b
}
// コンパイラの処理:
// 1. 型パラメータを解析
// 2. 使用されている具体的な型を特定
// 3. 型ごとに専用コードを生成(モノモーフィゼーション)
// 生成される関数(簡略化):
func Add_int(a, b int) int { return a + b }
func Add_int64(a, b int64) int64 { return a + b }
func Add_float64(a, b float64) float64 { return a + b }
GCShape Stenciling:
- 同じメモリレイアウトの型は同じコードを共有
- 例: int32とfloat32は同じ4バイト → 同じコード
- バイナリサイズとパフォーマンスのバランス
他言語との比較:
| 言語 | ジェネリクス手法 | パフォーマンス | バイナリサイズ |
|---|---|---|---|
| C++ | テンプレート(完全特殊化) | 最速 | 大きい |
| Java | 型消去(Type Erasure) | 遅い(キャスト必要) | 小さい |
| Go | GCShape Stenciling | 速い | 中程度 |
| Rust | モノモーフィゼーション | 最速 | 大きい |
2. スライスの成長アルゴリズム詳細
Go 1.18以降の成長戦略(実装)
// runtime/slice.go からの抜粋(簡略化)
func growslice(et *_type, old slice, cap int) slice {
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
// 必要な容量が倍より大きい場合、その容量を使う
newcap = cap
} else {
const threshold = 256
if old.cap < threshold {
// 小さいスライスは倍々で成長
newcap = doublecap
} else {
// 大きいスライスは約1.25倍で成長
for 0 < newcap && newcap < cap {
// 成長式: new = old + (old + 3*256) / 4
newcap += (newcap + 3*threshold) / 4
}
if newcap <= 0 {
newcap = cap
}
}
}
// メモリアライメントとサイズクラスの調整
// mallocgcの割り当てサイズに合わせて最適化
var overflow bool
var lenmem, newlenmem, capmem uintptr
switch {
case et.size == 1:
lenmem = uintptr(old.len)
newlenmem = uintptr(cap)
capmem = roundupsize(uintptr(newcap))
overflow = uintptr(newcap) > maxAlloc
newcap = int(capmem)
case et.size == sys.PtrSize:
lenmem = uintptr(old.len) * sys.PtrSize
newlenmem = uintptr(cap) * sys.PtrSize
capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
newcap = int(capmem / sys.PtrSize)
// その他のサイズ...
}
// 新しい配列を割り当て、古いデータをコピー
var p unsafe.Pointer
if et.ptrdata == 0 {
p = mallocgc(capmem, nil, false)
memmove(p, old.array, lenmem)
} else {
p = mallocgc(capmem, et, true)
if writeBarrier.enabled {
bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem)
}
memmove(p, old.array, lenmem)
}
return slice{p, old.len, newcap}
}
成長パターンの実測
初期容量: 1
append回数 容量 成長率
1 1 -
2 2 2.00x
3 4 2.00x
5 8 2.00x
9 16 2.00x
17 32 2.00x
33 64 2.00x
65 128 2.00x
129 256 2.00x
257 512 2.00x ← ここから変化
513 848 1.66x
849 1280 1.51x
1281 1792 1.40x
1793 2560 1.43x
なぜ256で戦略が変わるのか?:
- 小さいスライス: 再割り当て回数を減らす(2倍で急成長)
- 大きいスライス: メモリ浪費を防ぐ(1.25倍で緩やか)
- 256という閾値: 経験的に決定された最適値
メモリ使用量とパフォーマンス
例: 1000要素を追加する3つのパターン
【パターンA: 事前割り当てなし】
var s []int
for i := 0; i < 1000; i++ {
s = append(s, i)
}
結果:
- 再割り当て: 10回
- ピークメモリ: 約16KB
- 無駄なメモリ: 約8KB (容量1024、使用1000)
- 実行時間: 約25μs
【パターンB: 事前に容量確保】
s := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
s = append(s, i)
}
結果:
- 再割り当て: 0回
- ピークメモリ: 8KB
- 無駄なメモリ: 0KB
- 実行時間: 約5μs (5倍高速!)
【パターンC: 長さも指定】
s := make([]int, 1000)
for i := 0; i < 1000; i++ {
s[i] = i
}
結果:
- 再割り当て: 0回
- ピークメモリ: 8KB
- 無駄なメモリ: 0KB
- 実行時間: 約3μs (8倍高速!)
3. ポインタとエスケープ分析
エスケープ分析の仕組み
Goコンパイラは静的解析で変数の生存期間を判定:
// ケース1: スタックに割り当て
func localVar() int {
x := 42 // x は関数内でのみ使用
y := x * 2
return y // 値のみ返す → スタック
}
// 生成されるコード:
// スタックフレーム内で完結、ヒープ割り当てなし
// ケース2: ヒープにエスケープ
func escapeVar() *int {
x := 42 // x のポインタが返される
return &x // → ヒープに割り当て必要
}
// 生成されるコード:
// runtime.newobject() でヒープに割り当て
エスケープ分析の確認方法:
# エスケープ分析の詳細を表示
go build -gcflags="-m -m" main.go
# 出力例:
# ./main.go:6:2: x escapes to heap:
# ./main.go:6:2: flow: ~r0 = &x:
# ./main.go:6:2: from &x (address-of) at ./main.go:7:9
# ./main.go:6:2: from return &x (return) at ./main.go:7:2
スタック vs ヒープのパフォーマンス
スタック割り当て:
- 確保: SP(スタックポインタ)を動かすだけ(1命令)
- 解放: 関数終了時に自動(SP を戻すだけ)
- メモリ局所性: 高い(キャッシュに乗りやすい)
- GCの負荷: ゼロ
- コスト: 約0.5-1ns
ヒープ割り当て:
- 確保: mallocgc() 呼び出し(複雑な処理)
- 解放: GCが必要(スキャン、マーク、スイープ)
- メモリ局所性: 低い(散在する可能性)
- GCの負荷: あり(GC pause の原因)
- コスト: 約50-100ns
結論: スタック割り当ては100倍以上高速
エスケープを避けるテクニック
// 悪い例: 不要なポインタ返却
func getConfig() *Config {
return &Config{ // ヒープにエスケープ
Timeout: 30,
MaxConn: 100,
}
}
// 良い例: 値で返す
func getConfig() Config {
return Config{ // スタックで完結
Timeout: 30,
MaxConn: 100,
}
}
// 悪い例: クロージャでの変数キャプチャ
func makeCounter() func() int {
count := 0 // エスケープ(クロージャが参照)
return func() int {
count++
return count
}
}
// 良い例: ポインタレシーバーで明示的に管理
type Counter struct {
count int
}
func (c *Counter) Increment() int {
c.count++
return c.count
}
---
ビジュアル図解: メモリレイアウトとデータフロー
1. スライスの内部構造
スライスの定義:
s := []int{10, 20, 30, 40, 50}
メモリレイアウト:
【スライスヘッダ】(スタック上、24バイト)
┌──────────────┬──────┬──────┐
│ ptr (8 bytes)│ len │ cap │
│ 0x00400000 │ 5 │ 5 │
└──────┬───────┴──────┴──────┘
│
│ ポインタが指す先
↓
【配列データ】(ヒープ上、40バイト)
┌────┬────┬────┬────┬────┐
│ 10 │ 20 │ 30 │ 40 │ 50 │ (各8バイト)
└────┴────┴────┴────┴────┘
0x400000 408 410 418 420
部分スライスの作成:
sub := s[1:4]
【新しいスライスヘッダ】(スタック上)
┌──────────────┬──────┬──────┐
│ ptr (8 bytes)│ len │ cap │
│ 0x00400008 │ 3 │ 4 │
└──────┬───────┴──────┴──────┘
│
│ 同じ配列の別の位置を指す
↓
【共有される配列データ】
┌────┬────┬────┬────┬────┐
│ 10 │ 20 │ 30 │ 40 │ 50 │
└────┴─┬──┴────┴────┴────┘
└─ sub の開始位置
重要: 部分スライスは元の配列を共有する!
2. append時の再割り当て
初期状態:
s := make([]int, 3, 5)
s[0], s[1], s[2] = 1, 2, 3
【スライス s】
┌──────┬─────┬─────┐
│ ptr │ len │ cap │
│0x1000│ 3 │ 5 │
└───┬──┴─────┴─────┘
│
↓
【配列】(容量5)
┌───┬───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 0 │ 0 │ ← 未使用領域
└───┴───┴───┴───┴───┘
容量内でappend:
s = append(s, 4)
【スライス s】(ポインタは変わらない)
┌──────┬─────┬─────┐
│ ptr │ len │ cap │
│0x1000│ 4 │ 5 │
└───┬──┴─────┴─────┘
│
↓
【配列】(同じ配列)
┌───┬───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │ 0 │
└───┴───┴───┴───┴───┘
容量を超えるappend:
s = append(s, 5, 6)
【新しいスライス s】
┌──────┬─────┬─────┐
│ ptr │ len │ cap │
│0x2000│ 6 │ 10 │ ← 新しいアドレス、容量2倍
└───┬──┴─────┴─────┘
│
↓
【新しい配列】(容量10)
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 0 │ 0 │ 0 │ 0 │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
【古い配列】(GCの対象)
┌───┬───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │ 5 │ ← もう使われていない
└───┴───┴───┴───┴───┘
3. 値渡しとポインタ渡しの違い
構造体の定義:
type User struct {
Name string // 16 bytes (stringヘッダ)
Age int // 8 bytes
}
値渡し:
func updateAge(u User) {
u.Age++ // コピーを変更
}
【呼び出し元】 【関数内】
main() updateAge()
┌────────────┐ ┌────────────┐
│ user │ │ u (copy) │
│ Name:"Alice" ────コピー───→ Name:"Alice"│
│ Age: 25 │ │ Age: 26 │ ← 変更
└────────────┘ └────────────┘
↑ ↓
└─── 変更されない GCされる
ポインタ渡し:
func updateAge(u *User) {
u.Age++ // 元を変更
}
【呼び出し元】 【関数内】
main() updateAge()
┌────────────┐ ┌────────┐
│ user │←─ポインタ─│ u (*User)│
│ Name:"Alice"│ │0x1234 │
│ Age: 26 │ ← 変更 └────────┘
└────────────┘
0x1234
コピーされるのはポインタ(8バイト)のみ
4. インターフェース値の内部構造
インターフェースの定義:
type Writer interface {
Write([]byte) (int, error)
}
具体的な型:
type FileWriter struct {
path string
}
func (f *FileWriter) Write(data []byte) (int, error) {
// ...
}
インターフェース値の作成:
var w Writer = &FileWriter{path: "file.txt"}
【インターフェース値 w】(16バイト)
┌──────────┬──────────┐
│ tab │ data │
│ (8 bytes)│ (8 bytes)│
└────┬─────┴────┬─────┘
│ │
│ └─→ 【FileWriter のインスタンス】
│ ┌──────────────┐
│ │ path: "file.txt"│
│ └──────────────┘
│ 0x2000
│
└─→ 【itab (interface table)】
┌─────────────────┐
│ inter: *Writer │ ← インターフェース型情報
│ _type: *FileWriter│ ← 実際の型情報
│ fun[0]: Write │ ← メソッドポインタ
└─────────────────┘
メソッド呼び出し:
w.Write(data)
実際の処理:
1. itab から Write メソッドのポインタを取得
2. data ポインタを引数として渡す
3. 間接呼び出し(仮想関数テーブル方式)
コスト: 直接呼び出しより約2-3ns遅い
---
さらなる探求: 学習リソース
公式ドキュメント
- インタラクティブなチュートリアル - ブラウザで実行可能 - 初心者から経験者まで必須 - Goらしいコードの書き方 - イディオム集 - 標準ライブラリの設計思想 - 公式ブログ - 言語の設計判断の背景 - 新機能の詳細解説 - 言語仕様の正式ドキュメント - 曖昧な挙動の確認に推奨書籍
- "The Go Programming Language" (Donovan & Kernighan)
- "Go in Action" (William Kennedy)
- "Learning Go" (Jon Bodner)
オンラインリソース
- 実例ベースの学習 - コピー&ペーストで試せる - 週次のニュースレター - 最新情報をキャッチアップ - パッケージのドキュメント検索 - 使用例とサンプルコード - ブラウザでGoを実行 - コードの共有に便利コミュニティ
- 活発なコミュニティ - 質問と回答 - Redditのコミュニティ - ニュースと議論 - 公式フォーラム - 詳しい議論---
セルフチェック質問
これらの質問に答えられるか確認してください。答えられない場合は、該当セクションを復習しましょう。
レベル1: 基礎理解
- 型システム
int と int32 を加算するとどうなりますか?
- なぜGoは暗黙の型変換を禁止しているのですか?
- ジェネリクス(Go 1.18+)を使う利点は何ですか?- ゼロ値
int, string, []int, map[string]int, *int
- なぜGoはすべての変数をゼロ値で初期化するのですか?
- ゼロ値で使える型を設計する際のポイントは?- ポインタ
レベル2: 実践的理解
- スライス
append() は新しいスライスを返す場合と返さない場合があります。その違いは?
- 次のコードの出力は何ですか?
s := []int{1, 2, 3}
sub := s[1:]
sub[0] = 99
fmt.Println(s) // 何が出力される?
- メモリ管理
func f1() int { x := 42; return x }
func f2() *int { x := 42; return &x }
- パフォーマンス
make([]int, 0, 1000))はなぜ速いのですか?
- 小さな構造体(16バイト以下)はポインタ渡しと値渡しのどちらが速いですか?レベル3: 設計思想の理解
- Goの哲学
- メンタルモデル
- トレードオフ
解答のヒント
クリックして解答のヒントを表示
- コンパイルエラー。明示的に
int32(x) + yのように変換が必要。 - バグの早期発見、意図の明確化、予期しない動作の防止。
- ゼロ値: 0, "", nil, nil, nil。理由: 初期化忘れを防ぐ、安全性。
- 値渡し=コピー、ポインタ渡し=参照。大きい構造体や変更が必要ならポインタ。
- スライス: ヘッダ(ptr, len, cap)+ 実際の配列。
- 容量を超えた場合は新しい配列を割り当て、新しいスライスを返す。
[1, 99, 3]- subはsと配列を共有しているため。- スタック=高速、関数終了で自動解放。ヒープ=GC必要、エスケープ時。
- f2がエスケープ(ポインタを返すため)。
- 再割り当てが不要なため。小さい構造体は値渡しの方が速い(レジスタに収まる)。
- 命名規則とエクスポート
---
明日の予習: Day 2 - イディオマティックGo
Day 2では、Goらしいコードの書き方(イディオム)を学びます:
予定トピック
- エラーハンドリングのパターン
errors.Is, errors.As
- カスタムエラー型の設計
- センチネルエラー vs エラー型- defer, panic, recover
- インターフェースの活用
- テストの書き方
予習課題
次のコードを読んで、何が問題か考えてみてください:
// これは良いGoコード?それとも改善の余地がある?
type MyInterface interface {
DoEverything(a int, b string, c float64) (int, string, error)
ProcessData(data []byte) error
ValidateInput(input string) bool
GetName() string
SetName(name string)
GetAge() int
SetAge(age int)
}
func HandleError(err error) {
if err != nil {
panic(err) // これは適切?
}
}
func ReadFile(path string) string {
data, _ := os.ReadFile(path) // エラーを無視
return string(data)
}
答えは明日!
---
おめでとうございます!Day 1を完了しました。
今日学んだGoの核心概念:
- ✅ 型システムの明示性
- ✅ ポインタとメモリ効率
- ✅ スライスの内部構造
- ✅ ゼロ値の設計哲学
これらの基礎の上に、明日はGoらしいコードの書き方を学びます。
復習のポイント:
- 型変換は常に明示的に
- 値渡しとポインタ渡しを意識する
- スライスは容量を事前確保すると高速
- ゼロ値で使える型を設計する
それでは、明日もお楽しみに!