1-2. 変数と型 - var, :=, 基本型, const, iota, nil

所要時間: 30-50分(がっつりなら2セッション分) コミット内容: ~/learn/go/day02/types/ 一式


このレッスンのゴール

  • var:= を場面で使い分けられる
  • 基本型(int, float64, string, bool, byte, rune)とゼロ値が頭に入る
  • 型変換が明示的に書ける(float64(i) のように)
  • constiota で enum 風の定数群を作れる
  • Go の nil と JS の null/undefined の違いを説明できる

なぜ学ぶか

TS で null チェック忘れて undefined エラー、を Go で繰り返したくない」「JS の "5" + 1 === "51" のような暗黙変換バグを防ぎたい」「enum 風の値(ステータスコード、優先度)をきれいに表現したい」。型と変数の扱いは 本章だけで全レッスンの 80% を支える土台 になる。ここで var/:=/ゼロ値/nil の地肌を固めないと、後の *User ポインタも error インターフェースも理解しづらい。

前章とのつながり

1-1_HelloWorldfmt.Println("Hello, Go!") と書いたが、そこには何の型も登場しなかった。本章でその裏で動いていた「文字列リテラルは string 型」「Println...interface{} を受け取る」という型の世界に踏み込む。

これができると何が嬉しいか

  • TS の let/const と「同じ感覚で書ける」コーナーがある → 移行コストが下がる
  • ゼロ値を活用した「コンストラクタ不要」コード が書ける → 軽量で美しい
  • strconv.Atoi でユーザー入力を安全に変換できる → CLI / API の入口で必須

大前提: Go の型は「静的・厳格・暗黙の変換なし」

TypeScript も静的型付けだったが、TS は実行時には型情報が消える「型は飾り」言語。Go は違う。

  • 実行時にも型が残る: 動的言語のような型エラーが起きない
  • 暗黙の型変換が無い: int + float64コンパイルエラー。明示的に float64(x) と書く必要がある
  • ゼロ値が必ず決まっている: 宣言だけで初期化を書かなくても、その型の「ゼロ」が入る
  • null / undefined の区別が無い: 値の不在は nil だけ

これらは最初は窮屈だが、実務で「想定外の null」「型混在による桁落ち」みたいなバグが激減する。保守性を取りに行った言語


セッション①: 変数の宣言(25-30分)

0. 録画スタート&作業ディレクトリ

mkdir -p ~/log ~/learn/go/day02/types
cd ~/learn/go/day02/types
go mod init example.com/types
script ~/log/go_day02.log

main.go を作って以下を書きながら進める。

package main
 
import "fmt"
 
func main() {
	// ここに書いていく
}

1. var で変数を宣言する

var name string = "Takato"
var age int = 32
 
// 型を省略(推論される)
var city = "Tokyo"
 
// 値を省略(ゼロ値が入る)
var count int
 
// 複数まとめて
var (
	host    = "localhost"
	port    = 8080
	enabled = true
)
 
fmt.Println(name, age, city, count, host, port, enabled)

var の本質

「変数を宣言する」公式の構文。var <名前> <型> = <値> という3点セットだが、型か値のどちらかは省略できる。両方省略は不可。

書き方のバリエーション

  • var x int = 10 : 全部書く
  • var x = 10 : 型を省略(型推論)
  • var x int : 値を省略(ゼロ値 が入る、後述)

複数まとめて宣言: var (...) ブロックで複数を一気に宣言できる。パッケージレベル(関数の外)でよく使う。

var の使い所

  • パッケージレベル変数: 関数の外で宣言する変数は var でしか書けない(:= は関数内専用)
  • ゼロ値で初期化したい時: var counter int のように「とりあえず 0 で始める」
  • 型を明示したい時: var price float64 のように、後で代入する値の型を強制したい場合

JS で言うと: let に近い。ただし型注釈の位置は逆(JS は let x: number、Go は var x int)。

2. := で宣言と代入を同時に

func main() {
	// 関数内で書ける短縮形
	name := "Takato"
	age := 32
	pi := 3.14
 
	fmt.Println(name, age, pi)
}

:= (short variable declaration) の本質

var x = 10 のシンタックスシュガー。関数内でしか使えない。型は値から推論される。

同じ結果になる:

var x = 10
x := 10  // ↑と同じ意味

使い所

  • 関数の中で新しい変数を作る時の デフォルト
  • 既存変数を使い回すなら = (等号のみ)

JS で言うと: const x = 10 の型推論版。ただし Go の := は const ではない(再代入可能)。

:= の落とし穴: 既存変数の再宣言エラー

name := "Taro"
name := "Hanako"  // コンパイルエラー: no new variables on left side of :=

同じ変数を := で2回宣言するとエラー。2回目は = だけにする:

name := "Taro"
name = "Hanako"  // OK

ただし例外: 複数代入で少なくとも1つが新しい変数なら := で書ける(よく使う):

result, err := doSomething()
// err が既にあっても、result が新しければ := で書ける
result2, err := doSomethingElse()  // OK

シャドーイング(変数の影武者)の罠

内側のスコープで := を使うと、**外側の同名変数を「上書き」したつもりで「別物を新規作成」**してしまう:

err := doA()
if true {
	err := doB()  // 内側で新しい err を作っただけ。外の err は変わらない
	fmt.Println(err)
}
fmt.Println(err)  // ← doA() の err

静的解析ツール go vet で検出できる。エディタの Go 拡張は警告を出してくれることが多い。

3. いつ var、いつ :=?

実務での使い分け(ざっくりルール)

シーン推奨
関数の中で新規変数:=
パッケージレベル変数var
ゼロ値で宣言したい(後で代入)var
型を明示したいvar x 型
複数変数まとめて宣言var (...)

基本は :=、書けない/明示したい時だけ var と覚えればOK。


セッション②: 基本型・型変換・const・nil(25-30分)

4. 基本型のラインナップ

// 整数(符号あり)
var i8 int8 = 127           // -128 ~ 127
var i16 int16 = 32767
var i32 int32 = 2147483647
var i64 int64 = 9223372036854775807
 
// 環境依存(32 or 64 bit)
var i int = 100
 
// 浮動小数
var f32 float32 = 3.14
var f64 float64 = 3.14159265358979
 
// 文字列
var s string = "hello"
 
// 真偽値
var b bool = true
 
// byte = uint8 のエイリアス(バイト1個 = 0-255)
var by byte = 'A'
 
// rune = int32 のエイリアス(Unicodeコードポイント)
var r rune = ''
 
fmt.Printf("%d %d %d %d %d %f %f %s %t %d %d\n",
	i8, i16, i32, i64, i, f32, f64, s, b, by, r)

整数型の選び方

結論: 基本は int を使う。サイズが問題になる時だけ明示する。

  • int / uint : 環境依存(64bit OS なら 64bit)。普段使う型
  • int64 : 巨大な値(タイムスタンプ、ID等)を扱う時。DB の BIGINT と互換
  • int32 : 32bit OS との互換、ファイルフォーマット仕様で決まっている時
  • int8, int16 : メモリ最適化が必要な特殊用途(ほぼ使わない)
  • uint : 「絶対マイナスにならない」と示したい時。ただし計算で簡単に桁あふれするので Go 公式は「特別な理由がなければ int を使え」と言っている

落とし穴: int32int64別の型intint32 も別物(環境依存だから)。代入・比較・演算する時は変換が必要。

浮動小数: 基本は float64

Go には float32float64 の2種類。float64 が標準。

  • float64 が普段使う型。math パッケージも float64 前提
  • float32 は GPU や組み込み用途、メモリ削減が必要な場合のみ

落とし穴: 浮動小数の罠は Go も他言語と同じ。0.1 + 0.2 != 0.3 は普通に起きる。金額計算には絶対に使わない(代わりに int64 で「銭/cent」単位で扱う、または shopspring/decimal のようなライブラリ)

string は不変(immutable)

Go の文字列は 読み取り専用のバイト列s[0] = 'X' のように書き換えできない。

s := "hello"
// s[0] = 'H'  // コンパイルエラー
 
// 連結はできる
s = s + " world"  // OK(新しい文字列を作って代入)

JS で言うと: JS の文字列も不変。同じ感覚。ただし Go の文字列は UTF-8 バイト列で、日本語などマルチバイト文字を s[0] で取るとバイト単位で切れてしまう(後で rune 解説)。

byterune は数値型の別名(エイリアス)

  • byte = uint8(0-255)。バイナリ・ASCII を扱う時に使う
  • rune = int32Unicode コードポイント1個分。日本語1文字も rune 1個で表せる
s := "あいう"
fmt.Println(len(s))         // 9(UTF-8 で3バイト×3文字)
fmt.Println(len([]rune(s))) // 3(文字数)

使い分け

  • ファイル読み書き・ネットワーク通信・暗号: byte[]byte がよく出る)
  • 文字数カウント・文字単位の処理: rune[]rune に変換)

JS には byte の概念は無い(Uint8Array が近い)。バックエンドではバイナリ操作が頻出するので []byte は早めに慣れる

5. 型変換は明示的にしか書けない

var i int = 10
var f float64 = 3.14
 
// NG: コンパイルエラー
// sum := i + f
 
// OK: 明示的に変換
sum := float64(i) + f
fmt.Println(sum) // 13.14
 
// 文字列 ⇔ 数値は strconv パッケージ
import "strconv"
 
n, err := strconv.Atoi("42")    // string → int
s := strconv.Itoa(100)          // int → string
fmt.Println(n, s, err)

なぜ暗黙の型変換が無いのか

Go は「思いがけない挙動」を嫌う。JS の "5" + 1 == "51" のような罠を防ぐ。

メリット

  • バグが減る(型違いの演算がコンパイル時に分かる
  • コードレビューで意図が見える(float64(x) と書いてあるから「ここで変換してるな」と一目で分かる)

デメリット

  • タイプ量が増える
  • 数値リテラル混在で i + 1 のような単純な式は書けるが、変数同士になるとうるさい

数値 ⇔ 文字列は型変換ではなく「変換関数」

n := int("42")    // NG: コンパイルエラー(数値→文字列の単純な変換は型変換ではない)

文字列と数値は 別世界 なので、変換には strconv パッケージを使う:

やりたいこと関数
string → intstrconv.Atoi(s)
int → stringstrconv.Itoa(n)
string → float64strconv.ParseFloat(s, 64)
float64 → stringstrconv.FormatFloat(f, 'f', 2, 64)
string → boolstrconv.ParseBool(s)

エラーを返すものが多い("abc" を int には出来ないので)。n, err := strconv.Atoi(s) の形で必ずエラーチェック

6. 定数 const

const MaxRetry = 3
const Pi = 3.14159
const AppName = "MyApp"
 
// 複数まとめて
const (
	StatusOK    = 200
	StatusError = 500
)
 
// 型を明示
const Timeout int = 30

const の本質

コンパイル時に値が確定する不変な値。実行時には変更できない。

JS の const との違い

  • JS の const : 再代入できない(が、オブジェクトの中身は変えられる)
  • Go の const : コンパイル時定数。配列・スライス・マップ・構造体は const にできない(プリミティブのみ
// NG: コンパイルエラー(スライスは const にできない)
// const list = []int{1, 2, 3}
 
// OK: var で代用
var list = []int{1, 2, 3}

使い所

  • 設定値(タイムアウト秒数、リトライ回数)
  • 物理定数・数学定数
  • エラーメッセージ・状態コードの定数

7. iota - 連番定数を一発で

const (
	Sunday = iota  // 0
	Monday         // 1
	Tuesday        // 2
	Wednesday      // 3
	Thursday       // 4
	Friday         // 5
	Saturday       // 6
)
 
// 1から始めたい・倍数にしたい
const (
	_  = iota             // 0は捨てる
	KB = 1 << (10 * iota) // 1 << 10 = 1024
	MB                    // 1 << 20
	GB                    // 1 << 30
)
fmt.Println(KB, MB, GB) // 1024 1048576 1073741824

iota の本質

const ( ... ) ブロックの中で使える「自動連番カウンタ」const ブロックの先頭で0、行ごとに1ずつ増える。

使い所

  • enum 風の定数群: 曜日、状態、優先度など
  • ビットフラグ: 1 << iota でフラグ用ビット
  • 単位定数: KB / MB / GB のようなサイズ単位

JS で言うと: TypeScript の enum に相当する書き方がGoでは「const + iota」になる。Goには専用 enum 構文が無い。

iota の落とし穴

  • 行を空けると連番が崩れる: コメント行は数えないが、空行や別の const ブロックではリセットされる
  • 連番をスキップしたいなら _ 受け: _ = iota で1行潰す
  • 値を直接書くと止まらない: iota を使っているブロック内で =値 を書いてもカウンタは進む

8. ゼロ値 - 「未初期化」が存在しない

var s string  // ""
var i int     // 0
var f float64 // 0.0
var b bool    // false
var p *int    // nil
var arr []int // nil (空ではなくnil)
var m map[string]int // nil
fmt.Printf("%q %d %f %t %v %v %v\n", s, i, f, b, p, arr, m)

ゼロ値の本質

Go では var x int だけ書いて値を入れなくても その型の「ゼロ値」が自動で入る。「未初期化」「undefined」みたいな状態は 存在しない

ゼロ値
int / float640
string""(空文字)
boolfalse
ポインタ / スライス / マップ / チャネル / 関数 / インターフェースnil
構造体各フィールドがゼロ値の構造体

JS との違い: JS は let x;undefined。Go では確実に「0」「空文字」「false」など意味のある初期値になる。if x != 0 で安全に判定できる。

9. nil と JS の null/undefined の違い

var p *int  // nil
fmt.Println(p == nil) // true
 
var s []int // nil
fmt.Println(s == nil) // true
fmt.Println(len(s))   // 0(nil なスライスでも len は呼べる)
 
var m map[string]int // nil
// m["a"] = 1 // ランタイムパニック!: assignment to entry in nil map

nil とは何か

「値が無い」を表す唯一の特別な値。型ごとに意味が違うので注意。

  • ポインタの nil: 「何も指していない」
  • スライス / マップ / チャネルの nil: 「未初期化」
  • インターフェースの nil: 「実装が何も入っていない」

JS との違い

言語「値の不在」表現
JSnull (意図的)、undefined(未定義)の2つ
TS同上(より厳格に区別する)
Gonil のみ

Goでは「意図的に空」も「未初期化」も nil ひとつで表す。シンプル。

nil の落とし穴

  • nil マップに代入はパニック: var m map[string]int; m["a"] = 1 はランタイムクラッシュ。make(map[string]int) で初期化が必要
  • nil スライスに append は OK: var s []int; s = append(s, 1) は問題なく動く
  • nil ポインタの参照はパニック: var p *int; fmt.Println(*p) はクラッシュ
  • nil インターフェースの罠: 「型は決まっているが値が nil」と「インターフェース自体が nil」は別物(深淵なので入門時は気にしなくていい)

ゼロ値を活用する設計

Go ではゼロ値が便利なように API を設計するのが流儀。

例: sync.Mutexvar m sync.Mutex だけで使える(コンストラクタ不要)。「ゼロ値で意味があるならコンストラクタを書くな」がGo流

JS で new Mutex() のように毎回 new する必要がない、と覚えるとイメージが付く。


練習課題

// main.go を書き換えて以下を実装
 
package main
 
import (
	"fmt"
	"strconv"
)
 
const (
	AppName    = "MyCalc"
	AppVersion = 1
)
 
const (
	Add = iota
	Sub
	Mul
	Div
)
 
func main() {
	// 1. 自己紹介
	name := "Takato"
	age := 32
	height := 173.5
	isEngineer := true
	fmt.Printf("名前: %s / 年齢: %d / 身長: %.1f / エンジニア: %t\n",
		name, age, height, isEngineer)
 
	// 2. 文字列を数値に変換
	s := "100"
	n, err := strconv.Atoi(s)
	if err != nil {
		fmt.Println("変換失敗:", err)
		return
	}
	fmt.Println("変換成功:", n+1)
 
	// 3. アプリ情報
	fmt.Printf("%s v%d\n", AppName, AppVersion)
 
	// 4. iota の確認
	fmt.Println("Add, Sub, Mul, Div =", Add, Sub, Mul, Div)
 
	// 5. ゼロ値の観察
	var zeroInt int
	var zeroStr string
	var zeroSlice []int
	fmt.Printf("ゼロ値: %d %q %v(nil? %t)\n",
		zeroInt, zeroStr, zeroSlice, zeroSlice == nil)
}
go run .

動いたら以下を試してみる

  • s := "abc" に変えて strconv.Atoi のエラーを観察
  • var x int だけの行を増やしてゼロ値を確認
  • iota1 << iota に変えてビットフラグを観察

締め: 振り返り(10分)

1. セッション録画を終了

exit

2. 今日の発見(このノートに追記)

- 一番「Go っぽいな」と思った仕様:
- JS/TS と違って戸惑った点:
- iota の動きで掴めたこと:
- 明日やりたいこと:

アンチパターン集 - やらかし事例

実際にチームで見たやらかし

1. _ でエラーを握りつぶす

n, _ := strconv.Atoi(userInput)  // 失敗時 n=0 のまま気付かない

「ユーザーが数字以外を入力した」場合、n=0見かけ上は動くが意味的にバグっているコードが本番に出る。後から障害解析する人が地獄を見る。

2. nil マップに代入してパニック

var counts map[string]int  // nil のまま
counts["a"]++              // ランタイムクラッシュ

var で「宣言だけ」をマップに対してやると nil。マップは必ず make か リテラルで初期化

3. シャドーイングでループ内の err が外に伝わらない

var err error
for _, item := range items {
    err := process(item)  // ← := で内側に新しい err 作成
    if err != nil { /* 内側のスコープでだけ判定、外には伝わらない */ }
}
// ここで err は常に nil

エラーを呼び出し元に return したいのに、:= でシャドーイングして握りつぶす定番ミス。

4. 浮動小数で金額計算

total := 0.1 + 0.2  // 0.30000000000000004

1円のズレが本番で発覚すると経理が血の涙を流す。金額は int64 で銭・cent 単位 か decimal ライブラリ。

対比表で違いを明確化

var vs := の使い分け早見表

観点var x intvar x = 10x := 10
明示推論推論
ゼロ値必須必須
関数の外OKOKNG
関数の中OKOK推奨
既存変数の再代入x = 5(=のみ)同左x = 5(=のみ)
用途後で代入する初期値で型を推論関数内のデフォルト

nil の意味は型ごとに違う

nil の意味nil でできること
ポインタ *T何も指していない== nil 比較のみ。デリファレンスはパニック
スライス []T未初期化len/cap/range/append OK
マップ map[K]V未初期化読み取りのみ OK、書き込みパニック
チャネル chan T未初期化送受信ブロック(永久に待つ)
インターフェース実装なし== nil 比較。メソッド呼出しパニック
関数 func()未割当== nil 比較。呼び出しパニック

自己評価チェックリスト

手を動かせた

  • var / := 両方で変数を宣言した
  • intfloat64float64(i) で明示変換した
  • strconv.Atoi / Itoa を使った
  • constiota で連番を作った
  • var x int でゼロ値を確認した

説明できる

  • 「迷ったら :=、書けない時だけ var」を理由付きで言える
  • 整数は基本 int で、特殊な時だけ int64 等にする理由
  • ゼロ値の存在が「未初期化バグ」を消す仕組み
  • nil と JS の null/undefined の違い

やらかし回避

  • _ でエラーを捨てるアンチパターンを認識した
  • nil マップへの代入がパニックすることを確認した
  • シャドーイングの罠を体感した(go vet で検出可能)

詰まった時のチートシート

やりたいこと書き方
関数内で新規変数x := 10
パッケージレベル変数var x = 10
型だけ指定(ゼロ値)var x int
複数まとめてvar (a = 1; b = 2)
定数const X = 10
連番定数const (A = iota; B; C)
型変換float64(i), int(f)
string → intstrconv.Atoi(s)
int → stringstrconv.Itoa(n)
nil 判定if x == nil
整形出力fmt.Printf("%s %d\n", s, n)

「実務OK」基準

  • := が無意識に出る: 関数内では脊髄反射でこっち
  • 型変換でエラーが出てもパニックしない: float64(x) の書き方が出てくる
  • ゼロ値の存在を信頼してコードを書く: 「未初期化」を気にしない安心感
  • iota で enum を表現できる: TS の enum 代替パターンが書ける
  • strconv の存在を知っている: 文字列⇔数値変換で迷わない

次のレッスン

1-3. 制御構文 へ。

ifforswitch、そして Go 特有の defer を扱う。Go の for は1個でwhileも無限ループも兼ねるという独特の哲学にも触れる。

つながりの予告

  • 本章で書いた strconv.Atoi(s)result, err := パターンは、次章の 「if err != nil パターン」の基礎になる
  • nil の理解は次章の 「nil ポインタチェック」「nil エラー判定」 に直結
  • ゼロ値の概念は次章の early return 設計 で再活用