1-2. 変数と型 - var, :=, 基本型, const, iota, nil
所要時間: 30-50分(がっつりなら2セッション分) コミット内容:
~/learn/go/day02/types/一式
このレッスンのゴール
-
varと:=を場面で使い分けられる - 基本型(int, float64, string, bool, byte, rune)とゼロ値が頭に入る
- 型変換が明示的に書ける(
float64(i)のように) -
constとiotaで enum 風の定数群を作れる - Go の
nilと JS のnull/undefinedの違いを説明できる
なぜ学ぶか
「TS で null チェック忘れて undefined エラー、を Go で繰り返したくない」「JS の "5" + 1 === "51" のような暗黙変換バグを防ぎたい」「enum 風の値(ステータスコード、優先度)をきれいに表現したい」。型と変数の扱いは 本章だけで全レッスンの 80% を支える土台 になる。ここで var/:=/ゼロ値/nil の地肌を固めないと、後の *User ポインタも error インターフェースも理解しづらい。
前章とのつながり
1-1_HelloWorld で fmt.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.logmain.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を使え」と言っている落とし穴:
int32とint64は 別の型。intとint32も別物(環境依存だから)。代入・比較・演算する時は変換が必要。
浮動小数: 基本は
float64Go には
float32とfloat64の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解説)。
byteとruneは数値型の別名(エイリアス)
byte=uint8(0-255)。バイナリ・ASCII を扱う時に使うrune=int32。Unicode コードポイント1個分。日本語1文字もrune1個で表せる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 → int strconv.Atoi(s)int → string strconv.Itoa(n)string → float64 strconv.ParseFloat(s, 64)float64 → string strconv.FormatFloat(f, 'f', 2, 64)string → bool strconv.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/float640string""(空文字)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 との違い
言語 「値の不在」表現 JS null(意図的)、undefined(未定義)の2つTS 同上(より厳格に区別する) Go nilのみ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.Mutexはvar 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だけの行を増やしてゼロ値を確認iotaを1 << iotaに変えてビットフラグを観察
締め: 振り返り(10分)
1. セッション録画を終了
exit2. 今日の発見(このノートに追記)
- 一番「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.300000000000000041円のズレが本番で発覚すると経理が血の涙を流す。金額は
int64で銭・cent 単位 か decimal ライブラリ。
対比表で違いを明確化
varvs:=の使い分け早見表
観点 var x intvar x = 10x := 10型 明示 推論 推論 値 ゼロ値 必須 必須 関数の外 OK OK NG 関数の中 OK OK 推奨 既存変数の再代入 x = 5(=のみ)同左 x = 5(=のみ)用途 後で代入する 初期値で型を推論 関数内のデフォルト
nilの意味は型ごとに違う
型 nil の意味 nil でできること ポインタ *T何も指していない == nil比較のみ。デリファレンスはパニックスライス []T未初期化 len/cap/range/appendOKマップ map[K]V未初期化 読み取りのみ OK、書き込みパニック チャネル chan T未初期化 送受信ブロック(永久に待つ) インターフェース 実装なし == nil比較。メソッド呼出しパニック関数 func()未割当 == nil比較。呼び出しパニック
自己評価チェックリスト
手を動かせた
-
var/:=両方で変数を宣言した -
intとfloat64をfloat64(i)で明示変換した -
strconv.Atoi/Itoaを使った -
constとiotaで連番を作った -
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 → int | strconv.Atoi(s) |
| int → string | strconv.Itoa(n) |
| nil 判定 | if x == nil |
| 整形出力 | fmt.Printf("%s %d\n", s, n) |
「実務OK」基準
:=が無意識に出る: 関数内では脊髄反射でこっち- 型変換でエラーが出てもパニックしない:
float64(x)の書き方が出てくる - ゼロ値の存在を信頼してコードを書く: 「未初期化」を気にしない安心感
iotaで enum を表現できる: TS のenum代替パターンが書けるstrconvの存在を知っている: 文字列⇔数値変換で迷わない
次のレッスン
1-3. 制御構文 へ。
if、for、switch、そして Go 特有の defer を扱う。Go の for は1個でwhileも無限ループも兼ねるという独特の哲学にも触れる。
つながりの予告
- 本章で書いた
strconv.Atoi(s)のresult, err :=パターンは、次章の 「if err != nil パターン」の基礎になる nilの理解は次章の 「nil ポインタチェック」「nil エラー判定」 に直結- ゼロ値の概念は次章の early return 設計 で再活用