第3章: 基本構文と変数

学習目標

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

  • Goプログラムの基本構造を理解できる
  • 変数の宣言と初期化の方法を使い分けられる
  • 定数とiotaの使い方をマスターできる
  • 型推論の仕組みを理解できる
  • メモリレベルで変数がどのように管理されるかを理解できる
  • Goプログラムの基本構造

    最小のプログラム

    package main
    
    import "fmt"
    
    func main() {
        fmt.Println("Hello, World!")
    }
    

    各行の説明:

  • package main: このファイルが属するパッケージを宣言
- mainパッケージは実行可能なプログラムを示す - ライブラリの場合は別の名前(例: package utils

  • import "fmt": 標準ライブラリのfmtパッケージをインポート
- フォーマット済みI/O機能を提供

  • func main(): プログラムのエントリーポイント
- すべての実行可能プログラムにはmain関数が必須

  • fmt.Println(): 標準出力に文字列を出力

💡 コンパイルプロセスの内部

Goプログラムがどのように実行可能ファイルになるかを見てみましょう:

ソースコード (.go)
    ↓
字句解析 (Lexer)
    ↓
構文解析 (Parser)
    ↓
抽象構文木 (AST)
    ↓
型チェック
    ↓
中間表現 (SSA)
    ↓
最適化
    ↓
機械語生成
    ↓
リンク
    ↓
実行可能ファイル

🔑 重要: Goは静的リンクされるため、実行可能ファイルに全ての依存関係が含まれます。

パッケージ宣言

// 実行可能プログラム
package main

// ライブラリパッケージ
package mathutils

// ネストされたパッケージ(ディレクトリ構造に対応)
package myapp/internal/database

import文の書き方

// 単一インポート
import "fmt"

// 複数インポート(個別)
import "fmt"
import "os"
import "time"

// 複数インポート(グループ化) - 推奨
import (
    "fmt"
    "os"
    "time"
)

// エイリアス
import (
    f "fmt"
    "math/rand"
)

// 使用例
f.Println("Hello")  // fmt.Println の代わり

// ドットインポート(非推奨)
import . "fmt"
Println("Hello")  // パッケージ名なしで使用可能

// ブランクインポート(副作用のみ)
import _ "image/png"  // init関数を実行するだけ

標準ライブラリと外部パッケージの分離(慣習):

import (
    // 標準ライブラリ
    "fmt"
    "net/http"
    "os"

    // 空行で分離

    // 外部パッケージ
    "github.com/gorilla/mux"
    "github.com/sirupsen/logrus"
)

変数とメモリレイアウト

メモリの基本概念

プログラムが実行されると、OSはプロセスに専用のメモリ空間を割り当てます:

┌─────────────────────────┐ 高位アドレス (0xFFFFFFFF...)
│      スタック           │ ← ローカル変数、関数呼び出し
│         ↓               │
│                         │
│      空き領域           │
│                         │
│         ↑               │
│       ヒープ            │ ← 動的メモリ割り当て (new, make)
├─────────────────────────┤
│     .bss セグメント     │ ← 初期化されていないグローバル変数
├─────────────────────────┤
│     .data セグメント    │ ← 初期化済みグローバル変数
├─────────────────────────┤
│     .text セグメント    │ ← プログラムコード(機械語)
└─────────────────────────┘ 低位アドレス (0x00000000...)

🔑 重要な違い:

  • スタック: 高速だが容量が限られている(通常1-8MB)、自動解放
  • ヒープ: 大容量だが遅い、手動管理(Goではガベージコレクタが自動管理)

変数宣言とメモリ配置

var宣言

最も基本的な変数宣言方法です。

// 型を明示
var name string = "Gopher"
var age int = 10
var price float64 = 99.99

// 型推論
var name = "Gopher"     // string型と推論
var age = 10            // int型と推論
var price = 99.99       // float64型と推論

// ゼロ値で初期化
var count int           // 0
var message string      // ""
var isActive bool       // false
var pointer *int        // nil

複数変数の宣言:

// 同じ行
var x, y int = 1, 2
var a, b, c = 1, "hello", true  // 異なる型も可能

// グループ化
var (
    name    string = "Gopher"
    age     int    = 10
    isAdmin bool   = false
)

💡 メモリレベルでの変数宣言

変数を宣言すると、メモリ上に領域が確保されます:

var x int = 42

スタック上のメモリレイアウト:

スタックフレーム(main関数):
┌──────────────────────┐
│  リターンアドレス    │ ← 関数終了後の戻り先
├──────────────────────┤
│  フレームポインタ    │ ← ベースポインタ (BP)
├──────────────────────┤
│  x: 0x0000002A       │ ← int (8バイト, 値: 42)
│  (42 in hex)         │
└──────────────────────┘
     スタックポインタ (SP) ←

🔑 CPUレジスタとの関係:

  • SP (Stack Pointer): 現在のスタックトップを指す
  • BP (Base Pointer): 現在の関数フレームのベースを指す
  • 変数アクセス時は BP + offset で計算

実際のアセンブリコード例:

; var x int = 42
MOVQ    $42, -8(SP)     ; スタックに42を配置

; x を読み取り
MOVQ    -8(SP), AX      ; スタックからAXレジスタにロード

短縮宣言(:=)

関数内でのみ使用できる簡潔な記法です。

func main() {
    // := で宣言と初期化を同時に
    name := "Gopher"
    age := 10
    price := 99.99

    // 複数変数
    x, y := 1, 2
    width, height := 100, 200

    // 型推論が働く
    i := 42           // int
    f := 3.14         // float64
    s := "hello"      // string
    b := true         // bool
    c := 'A'          // rune (int32)
}

varと:=の使い分け:

シチュエーション 推奨 理由
関数内の変数 := 簡潔で読みやすい
パッケージレベル変数 var := は使用不可
ゼロ値で初期化 var 意図が明確
型を明示したい var 型変換のコストが見える

// パッケージレベル - var のみ
var GlobalConfig string = "production"

func main() {
    // 関数内 - := 推奨
    localVar := "local"

    // ゼロ値で初期化 - var が明確
    var counter int  // 0 で初期化したいことが明確

    // 型を明示 - var が適切
    var pi float64 = 3.14159265359  // 高精度が必要
}

💡 型推論の内部動作

コンパイラは、右辺の式を評価して型を決定します:

x := 42

コンパイラの処理:

1. 右辺の式 "42" を解析
2. リテラルの型を判定 → int (デフォルト整数型)
3. 変数 x に int 型を割り当て
4. メモリを確保 (8バイト on 64bit)
5. 値 42 を書き込み

型推論の優先順位:

// 整数リテラル
var a = 42          // int (プラットフォーム依存のサイズ)

// 浮動小数点リテラル
var b = 3.14        // float64 (常に64bit)

// 複素数リテラル
var c = 1 + 2i      // complex128

// 文字リテラル
var d = 'A'         // rune (int32)

// 文字列リテラル
var e = "hello"     // string

⚠️ 注意: 型推論は「最も広い型」を選択します(例: int, float64)

再宣言と多重代入

// 通常の代入
x := 10
x = 20  // OK

// x := 30  // エラー!既に宣言済み

// 多重代入で一部が新しい変数なら OK
x, y := 1, 2
x, z := 3, 4  // OK(zが新しい変数)

ゼロ値とメモリ初期化

Goでは、変数を初期化しないと「ゼロ値」が自動的に割り当てられます。

var i int        // 0
var f float64    // 0.0
var b bool       // false
var s string     // "" (空文字列)
var p *int       // nil
var sl []int     // nil
var m map[string]int  // nil

fmt.Println(i, f, b, s, p, sl, m)
// 出力: 0 0 false  <nil> [] map[]

💡 ゼロ値のメモリレベル実装

Cとは異なり、Goは変数宣言時に必ずゼロ値で初期化します:

// C言語(危険)
int x;  // 未初期化 - ガベージ値(メモリの残骸)
printf("%d\n", x);  // 不定値(何が出るか分からない)

// Go(安全)
var x int  // 必ず 0 に初期化される
fmt.Println(x)  // 0(予測可能)

メモリの様子:

// C言語(未初期化)
メモリアドレス: 0x1000
┌────────────┐
│ 0xDEADBEEF │ ← 前回のゴミデータ
└────────────┘

// Go(ゼロ値初期化)
メモリアドレス: 0x1000
┌────────────┐
│ 0x00000000 │ ← 確実に 0
└────────────┘

🔑 実装詳細: Goランタイムは変数割り当て時に MEMCLR (メモリクリア) 命令を実行します。

ゼロ値の利点:

// C言語の問題
// int x;  // 未初期化 - 不定値(危険)
// printf("%d\n", x);  // 何が出力されるか不明

// Goの安全性
var x int  // 0(常に予測可能)
fmt.Println(x)  // 0

ゼロ値の活用:

type Counter struct {
    count int  // ゼロ値: 0
}

func main() {
    var c Counter
    fmt.Println(c.count)  // 0(初期化不要)

    c.count++
    fmt.Println(c.count)  // 1
}

⚠️ ゼロ値の落とし穴

// nil スライスとマップに注意
var s []int       // nil(書き込み可能)
s = append(s, 1)  // OK

var m map[string]int  // nil(書き込み不可)
// m["key"] = 1       // panic! nil マップへの書き込み

// make で初期化が必要
m = make(map[string]int)
m["key"] = 1  // OK

定数

変更できない値を定義します。

基本的な定数

const Pi = 3.14159
const Greeting = "Hello"
const MaxConnections = 100

// 型を明示
const Port int = 8080
const Message string = "Ready"

// グループ化
const (
    StatusOK       = 200
    StatusNotFound = 404
    StatusError    = 500
)

💡 定数のコンパイル時評価

定数はコンパイル時に評価され、実行時にはメモリを消費しません:

const Pi = 3.14159
x := Pi * 2  // コンパイル時に 6.28318 に置換

コンパイル後の機械語:

; 定数 Pi はメモリに存在しない
; 直接値が埋め込まれる (immediate value)
MOVSD   $6.28318, XMM0

🔑 即値 (Immediate Value): 機械語命令に直接埋め込まれた値

型なし定数

定数は「型なし」で宣言でき、使用時に適切な型に変換されます。

const n = 500000000

// 異なる型に自動変換
var i int = n
var f float64 = n
var u uint = n

fmt.Println(i, f, u)  // 500000000 5e+08 500000000

高精度な定数:

// 定数は任意精度の演算が可能
const Big = 1 << 100  // 変数では表現できない大きな数

// 使用時に適切な型に切り詰め
// var x int = Big  // エラー!intの範囲を超える
const SmallBig = Big >> 99
var x int = SmallBig  // OK

💡 定数の任意精度演算

Goコンパイラは、定数演算を任意精度で実行します:

const (
    // これは int64 の範囲を超えるが、定数なので OK
    Big = 1 << 100
    // Big = 1267650600228229401496703205376 (39桁)
)

コンパイラの処理:

1. Big の値を big.Int (多倍長整数) で計算
2. 使用箇所で型に合わせて切り詰め
3. 範囲外ならコンパイルエラー

iota による列挙型

iotaは連番を生成する特殊な定数です。

// 基本的な使い方
const (
    Sunday = iota  // 0
    Monday         // 1
    Tuesday        // 2
    Wednesday      // 3
    Thursday       // 4
    Friday         // 5
    Saturday       // 6
)

// 開始値を変更
const (
    January = iota + 1  // 1
    February            // 2
    March               // 3
    // ...
)

// スキップ
const (
    _ = iota  // 0 をスキップ
    KB = 1 << (10 * iota)  // 1024
    MB                     // 1048576
    GB                     // 1073741824
    TB                     // 1099511627776
)

fmt.Println(KB, MB, GB, TB)
// 出力: 1024 1048576 1073741824 1099511627776

ビットフラグの定義:

const (
    FlagNone   = 0
    FlagRead   = 1 << iota  // 1 (001)
    FlagWrite               // 2 (010)
    FlagExecute             // 4 (100)
)

// 複数権限の組み合わせ
permissions := FlagRead | FlagWrite
fmt.Printf("%b\n", permissions)  // 11 (Read + Write)

// 権限チェック
if permissions&FlagRead != 0 {
    fmt.Println("読み取り権限あり")
}

💡 iota の内部動作

iota はコンパイラのカウンタです:

const (
    A = iota  // 0
    B         // 1
    C         // 2
)

コンパイラの処理:

1. const ブロック開始時に iota = 0
2. 各行ごとに iota++
3. 値をコンパイル時に計算して埋め込む

ビットシフトとの組み合わせ:

const (
    KB = 1 << (10 * iota)  // 1 << 0  = 1
    MB                      // 1 << 10 = 1024
    GB                      // 1 << 20 = 1048576
)

展開後:

KB = 1 << 0  = 1
MB = 1 << 10 = 1024
GB = 1 << 20 = 1048576

型推論

Goは静的型付け言語ですが、強力な型推論機能を持ちます。

基本型の推論

// 整数リテラル
x := 42         // int
y := int64(42)  // 明示的にint64

// 浮動小数点リテラル
f := 3.14       // float64
g := float32(3.14)  // float32

// 文字列リテラル
s := "hello"    // string

// ブールリテラル
b := true       // bool

// rune(文字)リテラル
r := 'A'        // rune (int32)

複雑な型の推論

// スライス
numbers := []int{1, 2, 3}  // []int型

// マップ
ages := map[string]int{    // map[string]int型
    "Alice": 25,
    "Bob":   30,
}

// 構造体
type Person struct {
    Name string
    Age  int
}

p := Person{Name: "Alice", Age: 25}  // Person型

関数戻り値の型推論

func getValue() int {
    return 42
}

// 戻り値の型は推論される
result := getValue()  // int型

💡 型推論とパフォーマンス

型推論はコンパイル時に行われるため、実行時のオーバーヘッドはありません:

x := 42  // コンパイル時に int と決定

生成される機械語は同じ:

; var x int = 42
MOVQ    $42, -8(SP)

; x := 42
MOVQ    $42, -8(SP)  ; 全く同じ命令

🔑 ゼロコスト抽象化: 型推論は便利だが、実行時コストはゼロ

変数のスコープ

パッケージレベル

package main

import "fmt"

// パッケージレベル変数(全関数からアクセス可能)
var globalCounter int = 0

func main() {
    globalCounter++
    fmt.Println(globalCounter)  // 1
}

💡 パッケージレベル変数のメモリ配置

パッケージレベル変数は .data または .bss セグメントに配置されます:

var GlobalCounter int = 100  // .data (初期化済み)
var Uninitialized int        // .bss (ゼロ値)

メモリレイアウト:

.data セグメント (読み書き可能):
┌───────────────────────────┐
│ GlobalCounter: 100        │ ← 0x4000 番地
└───────────────────────────┘

.bss セグメント (ゼロ初期化):
┌───────────────────────────┐
│ Uninitialized: 0          │ ← 0x5000 番地
└───────────────────────────┘

🔑 静的リンク: パッケージレベル変数のアドレスはコンパイル時に決定

関数レベル

func example() {
    // 関数スコープ
    x := 10

    if x > 5 {
        // ブロックスコープ
        y := 20
        fmt.Println(x, y)  // 10 20
    }

    // fmt.Println(y)  // エラー!yはこのスコープにない
}

💡 スコープとスタックフレーム

各スコープはスタックフレーム内で管理されます:

func example() {
    x := 10      // スタックに配置
    if x > 5 {
        y := 20  // スタックに配置(さらに深い位置)
    }
    // y のメモリは解放(スタックポインタを戻すだけ)
}

スタックの様子:

関数開始時:
┌──────────────┐
│ example()    │
│  x: 10       │ ← SP
└──────────────┘

if ブロック内:
┌──────────────┐
│ example()    │
│  x: 10       │
│  y: 20       │ ← SP (深くなる)
└──────────────┘

if ブロック終了後:
┌──────────────┐
│ example()    │
│  x: 10       │ ← SP (戻る)
│  (y は無効)  │
└──────────────┘

🔑 スコープの終了 = スタックポインタを戻すだけ(非常に高速)

シャドーイング

var x int = 10  // パッケージレベル

func main() {
    fmt.Println(x)  // 10

    x := 20  // 新しいローカル変数(シャドーイング)
    fmt.Println(x)  // 20

    {
        x := 30  // さらに内側のブロックでシャドーイング
        fmt.Println(x)  // 30
    }

    fmt.Println(x)  // 20
}

// パッケージレベルの x は変更されていない

💡 シャドーイングのメモリ配置

シャドーイングは、異なるメモリ位置に新しい変数を作成します:

var x int = 10  // グローバル (.data)

func main() {
    x := 20     // ローカル (スタック)
}

メモリ配置:

グローバル領域 (.data):
┌──────────────┐
│ x: 10        │ ← 0x4000 (グローバルのx)
└──────────────┘

スタック:
┌──────────────┐
│ main()       │
│  x: 20       │ ← 0x7fff1234 (ローカルのx)
└──────────────┘

🔑 名前は同じでも、全く別のメモリ位置

ポインタの基礎

メモリアドレスを扱う型です。

// ポインタ変数の宣言
var p *int

// アドレス演算子 &
x := 42
p = &x  // xのアドレスを取得

// デリファレンス演算子 *
fmt.Println(*p)  // 42(pが指す値)

// ポインタ経由で値を変更
*p = 100
fmt.Println(x)  // 100(xが変更された)

💡 ポインタのメモリレベル動作

ポインタは「メモリアドレス」を格納する変数です:

x := 42
p := &x
*p = 100

メモリレイアウト:

スタックフレーム:
┌────────────────────────┐
│ x:  100                │ ← 0x7fff1000 番地
├────────────────────────┤
│ p:  0x7fff1000         │ ← 0x7fff1008 番地(アドレスを格納)
└────────────────────────┘

アセンブリコード:

; x := 42
MOVQ    $42, -8(SP)         ; スタックに 42 を配置

; p := &x
LEAQ    -8(SP), AX          ; x のアドレスを計算
MOVQ    AX, -16(SP)         ; p に保存

; *p = 100
MOVQ    -16(SP), AX         ; p (アドレス) をロード
MOVQ    $100, 0(AX)         ; アドレスが指す場所に 100 を書き込み

🔑 命令の解説:

  • LEAQ: Load Effective Address(アドレスを計算)
  • MOVQ: Move Quadword(8バイト転送)
  • 0(AX): AXレジスタが指すアドレス

ポインタのゼロ値:

var p *int
fmt.Println(p)  // <nil>

if p == nil {
    fmt.Println("ポインタは初期化されていません")
}

// nil デリファレンスはパニック
// fmt.Println(*p)  // panic: invalid memory address

💡 nil ポインタとセグメンテーション違反

nil ポインタをデリファレンスすると、CPUが例外を発生させます:

var p *int       // nil (0x0000000000000000)
fmt.Println(*p)  // panic!

CPUレベルの動作:

1. MOVQ 命令でアドレス 0x0 からロード試行
2. MMU (Memory Management Unit) が検出
3. セグメンテーション違反 (Segmentation Fault)
4. OS がシグナル (SIGSEGV) を送信
5. Go ランタイムがキャッチ
6. panic を発生

⚠️ メモリアドレス 0x0 は常に無効(OSが予約)

new 関数:

// new は型のゼロ値へのポインタを返す
p := new(int)
fmt.Println(*p)  // 0

*p = 42
fmt.Println(*p)  // 42

💡 new vs リテラル

new はヒープにメモリを確保します:

p := new(int)  // ヒープに確保
x := 42        // スタックに確保(エスケープ解析による)

メモリ配置:

スタック:
┌──────────────┐
│ x: 42        │ ← スタックに直接配置
└──────────────┘

ヒープ:
┌──────────────┐
│ (new で確保) │
│  0           │ ← ヒープに配置、ポインタで参照
└──────────────┘
     ↑
     p

🔑 エスケープ解析: Goコンパイラが自動的にスタック/ヒープを選択

型変換

Goでは暗黙の型変換は許可されていません。明示的な変換が必要です。

// 数値型の変換
var i int = 42
var f float64 = float64(i)  // 明示的変換
var u uint = uint(f)

// 暗黙の変換はエラー
// var x float64 = i  // エラー!

💡 型変換の機械語レベル

型変換は、CPU命令で実装されます:

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

アセンブリコード:

; i := 42
MOVQ    $42, -8(SP)         ; 整数として保存

; f := float64(i)
MOVQ    -8(SP), AX          ; 整数をロード
CVTSI2SDQ AX, XMM0          ; 整数→浮動小数点変換
MOVSD   XMM0, -16(SP)       ; 浮動小数点として保存

🔑 CVTSI2SDQ: Convert Signed Integer to Scalar Double Quadword

  • SI: Signed Integer(符号付き整数)
  • SD: Scalar Double(倍精度浮動小数点)

文字列と数値の変換:

import "strconv"

// 文字列 → 数値
i, err := strconv.Atoi("42")
f, err := strconv.ParseFloat("3.14", 64)
b, err := strconv.ParseBool("true")

// 数値 → 文字列
s1 := strconv.Itoa(42)
s2 := strconv.FormatFloat(3.14, 'f', 2, 64)
s3 := strconv.FormatBool(true)

byte と rune:

// string → []byte
s := "hello"
bytes := []byte(s)

// []byte → string
s2 := string(bytes)

// string → []rune
runes := []rune("こんにちは")
fmt.Println(len(runes))  // 5(文字数)

💡 文字列と []byte の変換コスト

文字列と []byte の変換はコピーを伴います:

s := "hello"
b := []byte(s)  // メモリコピー発生

メモリレイアウト:

元の文字列 (不変):
┌───────────────────────────┐
│ "hello" (read-only)       │ ← 0x1000
└───────────────────────────┘

[]byte への変換後:
┌───────────────────────────┐
│ ['h','e','l','l','o']     │ ← 0x2000 (新しいメモリ)
└───────────────────────────┘

🔑 文字列は不変なので、変更可能な []byte に変換するにはコピーが必要

⚠️ 大きな文字列の変換は高コスト → 可能な限り避ける

実践的なメモリ最適化テクニック

1. 変数のスコープを最小化

// 悪い例: スコープが広すぎる
func bad() {
    var buffer [1024]byte  // 1KB のバッファ
    // ... 100行のコード ...
    fmt.Println("Hello")   // buffer は未使用
    // ... さらに100行 ...
}

// 良い例: 必要な場所でのみ宣言
func good() {
    fmt.Println("Hello")
    // ... コード ...
    {
        var buffer [1024]byte  // 必要な時だけ
        // buffer を使用
    }
    // buffer のメモリは解放済み
}

2. 大きな構造体はポインタで渡す

type LargeStruct struct {
    data [1000]int  // 8KB
}

// 悪い例: 値渡し(8KB コピー)
func processBad(ls LargeStruct) {
    // ...
}

// 良い例: ポインタ渡し(8バイトのみ)
func processGood(ls *LargeStruct) {
    // ...
}

メモリコピーの比較:

値渡し:
呼び出し側のスタック    引数のコピー         関数のスタック
┌────────────┐    memcpy    ┌────────────┐
│ 8KB データ │ ────────────> │ 8KB データ │
└────────────┘    (遅い)     └────────────┘

ポインタ渡し:
呼び出し側のスタック    引数のコピー         関数のスタック
┌────────────┐    コピー     ┌──────┐
│ 8KB データ │ <────────── │ 8バイト│ (アドレスのみ)
└────────────┘              └──────┘
                            (高速)

3. 文字列の結合に strings.Builder を使う

// 悪い例: + での結合(毎回新しい文字列を作成)
func bad() string {
    s := ""
    for i := 0; i < 1000; i++ {
        s += "x"  // 毎回メモリコピー
    }
    return s
}

// 良い例: strings.Builder(効率的なバッファ管理)
func good() string {
    var sb strings.Builder
    sb.Grow(1000)  // 事前にメモリ確保
    for i := 0; i < 1000; i++ {
        sb.WriteString("x")
    }
    return sb.String()
}

パフォーマンス比較:

+ での結合: O(n²) - 毎回全体をコピー
strings.Builder: O(n) - バッファに追記

自習問題

以下の問題で理解を深めましょう:

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

以下のコードのメモリレイアウトを図示してください:

var global int = 100

func main() {
    local := 200
    ptr := &local
}

解答

.data セグメント:
┌──────────────┐
│ global: 100  │ ← 0x4000
└──────────────┘

スタック:
┌──────────────┐
│ main()       │
│  local: 200  │ ← 0x7fff1000
│  ptr: 0x...  │ ← 0x7fff1008 (local のアドレス)
└──────────────┘

問題2: 型推論

以下のコードの各変数の型を答えてください:

a := 42
b := 3.14
c := "hello"
d := 'A'
e := []int{1, 2, 3}

解答

  • a: int
  • b: float64
  • c: string
  • d: rune (= int32)
  • e: []int

問題3: ポインタの値

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

x := 10
p := &x
*p = 20
y := x
x = 30
fmt.Println(x, y, *p)

解答

30 20 30

説明:

  • p = 20 で x が 20 になる
  • y := x で y に 20 がコピー
  • x = 30 で x が 30 になる(y は変わらない)
  • p は x を指すので 30

問題4: ゼロ値

以下の型のゼロ値を答えてください:

var a int
var b string
var c bool
var d *int
var e []int
var f map[string]int

解答

  • a: 0
  • b: "" (空文字列)
  • c: false
  • d: nil
  • e: nil
  • f: nil

問題5: iota の計算

以下の定数の値を答えてください:

const (
    A = iota * 10
    B
    C
)

解答

  • A: 0 (0 10)
  • B: 10 (1 10)
  • C: 20 (2 10)

問題6: スコープとシャドーイング

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

var x int = 10

func main() {
    fmt.Println(x)
    x := 20
    fmt.Println(x)
    {
        x := 30
        fmt.Println(x)
    }
    fmt.Println(x)
}

解答

10
20
30
20

説明: 各スコープで新しい変数 x が作成される

問題7: 型変換のコスト

以下のコードのうち、最もコストが高い操作はどれですか?

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

// B
s := "hello world"
b := []byte(s)

// C
x := 10
p := &x

解答

B が最もコストが高い

理由:

  • A: CPU 命令1つ (CVTSI2SDQ)
  • B: メモリコピー (文字列の長さに比例)
  • C: アドレス計算のみ (LEAQ)

問題8: メモリの効率

以下の2つの関数のうち、どちらがメモリ効率的ですか?

// A
func processA(data [1000]int) {
    // ...
}

// B
func processB(data *[1000]int) {
    // ...
}

解答

B が効率的

理由:

  • A: 8000バイトのコピー (1000 8)
  • B: 8バイトのポインタのみ

問題9: 定数のメモリ

以下のコードで、実行時にメモリを消費する定数はいくつありますか?

const (
    Pi = 3.14159
    MaxSize = 1000
    Greeting = "Hello"
)

解答

0個

理由: すべてコンパイル時に評価され、機械語に直接埋め込まれる(即値)

問題10: ポインタとエスケープ

以下のコードで、変数 x はスタックとヒープのどちらに配置されますか?

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

解答

ヒープ

理由: x のアドレスが関数外に返されるため、エスケープ解析によりヒープに配置される。スタックだと関数終了時に無効になるため。

まとめ

この章では、Goの基本構文と変数について、メモリレベルの動作を含めて学びました。

重要ポイント:

  • プログラム構造: package宣言、import、main関数
  • 変数宣言: var(汎用)、:=(関数内の簡潔な記法)
  • メモリレイアウト: スタック、ヒープ、.data/.bss セグメント
  • ゼロ値: 未初期化変数は安全なデフォルト値を持つ(Cとの違い)
  • 定数: const、iota による列挙型、コンパイル時評価
  • 型推論: 簡潔でありながら型安全、ゼロコスト抽象化
  • スコープ: パッケージ、関数、ブロック、スタックフレームとの関係
  • ポインタ: メモリアドレスを安全に扱う、nil チェック必須
  • 型変換: 明示的変換が必須、CPU命令レベルの理解

Goらしい書き方:

  • 関数内では := を積極的に使う
  • ゼロ値を活用して初期化コードを減らす
  • 定数には大文字始まりの名前を付ける
  • 型変換は常に明示的に行う
  • 大きな構造体はポインタで渡す
  • スコープを最小化してメモリ効率を向上

機械レベルの理解:

  • 🔑 変数はスタックまたはヒープに配置される
  • 🔑 型推論はコンパイル時、実行時コストはゼロ
  • 🔑 ポインタはメモリアドレス(8バイト on 64bit)
  • 🔑 関数呼び出しはスタックフレームを使用
  • 🔑 エスケープ解析がスタック/ヒープを自動選択

次の章では、Goの型システムをより深く、メモリレイアウトやCPU命令レベルで学びます。