Go
目次
主要項目のみを表示しています。詳細な小見出しは本文内で確認できます。
- 概要
- 1. Goとは何か
- 2. 実行環境とツールチェイン
- 3. 型と変数
- 4. 制御フローと関数
- 5. コレクション(slice / map / array)
- 6. structとmethods
- 7. interface
- 8. エラーハンドリング
- 9. GoroutineとChannel
- 10. Generics(Go 1.18+)
- 11. モジュールとパッケージ
- 12. 標準ライブラリの主要要素
- 13. テスト・ベンチマーク・プロファイリング
- 14. Go 1.18〜1.23の進化
- 15. よくある落とし穴FAQ
- 16. 図解: goroutine / channel / GC
- 17. 学習ロードマップ(30日)
- 18. 用語集
- 発展: Goらしい設計
- 19. goroutineとchannel完全版
- 20. interface深掘り
- 21. errorハンドリング深掘り
- 22. structとmethods深掘り
- 23. 標準ライブラリ詳細
- 24. Generics完全版
- 25. テストとプロファイル深掘り
- 26. デプロイとビルド
- 27. Webフレームワーク詳細
- 28. データベース連携
- 29. CLIツール構築
- 30. ロギングと観測
- 応用: サーバ開発と運用
- Go の実践的な設計パターン
- まとめ
- 参考文献
概要
まず、この章の中心構造を図で確認します。細部に入る前に、どの概念がどこへつながるかをつかむための地図です。
コード例は、そのまま写すためだけのものではありません。直前の本文で「何を確かめる例か」を押さえ、直後の説明で「どの性質が見えるか」を確認してください。実務では、ここに入力の境界、失敗時の挙動、依存する実行環境を足して読むと判断しやすくなります。
Goは、単純な構文、明快な並行処理、ツールチェインの一体感を重視する言語です。
このページでは、型、interface、goroutine、channel、module、標準ライブラリを、Goらしい設計の背景とあわせて整理します。
1. Goとは何か
Go(Golang)は、「シンプル・高速・並行性が言語に組み込まれた」 プログラミング言語。Googleが 「現代のクラウドサーバ向け」に設計した言語で、Kubernetes、Docker、Terraformなどのインフラ系ツールの実装言語として圧倒的なシェアを持ちます。
主な用途:
- クラウドインフラ: Kubernetes、Docker、Terraform、Prometheus
- マイクロサービスバックエンド: gRPC、APIゲートウェイ
- CLIツール: GitHub CLI、Hugo、Cobraベースの大量のツール
- DevOps: コンテナオーケストレーション、サービスメッシュ
1-1. Goの歴史(誕生からGo 1.23まで)
Googleでの誕生(2007〜2009)
2007年、Googleで Robert Griesemer、Rob Pike、Ken Thompson が新しい言語の設計を始めました。動機は、「C++ のビルドが遅すぎる」「Javaは冗長すぎる」「Pythonは遅い」というGoogle社内の不満を解消したいというものでした。
Rob Pike: Plan 9・Unixの設計に関わった伝説的なプログラマ
Ken Thompson: Unix・C言語・UTF-8の共同設計者
Robert Griesemer: V8 / HotSpotに関わった
3人の経歴を見ると、Goが 「シンプル・高速・並行重視」 な性格を持つのが必然だったと感じます。
Go 1.0(2012)
2009年に公開、2012年に Go 1.0 リリース。「後方互換性を保証する」という大きな約束(Go 1 promise)が掲げられました。
2009 Go公開
2012 Go 1.0
2014 Go 1.4(runtimeをGoで書き直す)
2016 Go 1.6(HTTP/2標準サポート)
2018 Go 1.11(go modules導入)
2022 Go 1.18(Generics導入)
2023 Go 1.21(min/max/clear、log/slog)
2024 Go 1.22(forループ変数の挙動変更)
2024 Go 1.23(イテレータ関数)
Generics導入(Go 1.18、2022)
Go 1.18で ジェネリクスが導入されました。それまで「ジェネリクスを意図的に避けてきた」 Goが、10年越しで設計に組み込んだ大型変更でした。
Go 1.22のループ変数バグ修正
Go 1.22で「forループの変数が反復ごとに新しいスコープを持つ」ように仕様変更されました。これは長年バグの温床だった for x := range items { go func() { fmt.Println(x) }() } を解消する 破壊的に近い変更で、しかし「Go 1 promiseの精神を保つ範囲」でした。
1-2. 設計哲学(Less is more)
Goの哲学は 「Less is more(少ないほど豊か)」。Rob Pikeの有名な発言:
“Less is exponentially more.” ─ Rob Pike
機能は足し算ではなく引き算で
Goは他の言語が持つ多くの機能を 意図的に省略しています。
Goにあるもの:
goroutine / channel / interface / struct / 関数
Goに「ない」もの:
クラス・継承(is-a関係)
例外(try/catch)
オーバーロード
デフォルト引数
三項演算子
ジェネリクス(1.18まで)
enum(iotaで代替)
これらは 「意図的な不在」で、設計者は議論の末に省きました。シンプルで予測可能なコードを書きやすくする狙いです。
規約による統一
Goは 書き方を強制することで生産性を上げます。
gofmt: 公式フォーマッタ。すべてのGoコードが同じスタイルになるgo vet: 標準の静的解析ツール- 未使用import / 未使用変数はコンパイルエラー
- 公開・非公開は名前の最初の文字の大小(大文字 = public)
「書き方の自由が少ないからこそ、チーム開発で一貫したコードになる」という哲学です。
1-3. なぜGoはシンプルなのか
Google規模の開発で抱えていた問題:
- 数千人の開発者
- 数百万行のコードベース
- 入れ替わりの激しい開発者
そこで求められたのは、
- 新人がすぐ読める言語
- ビルドが高速
- 並行処理が組み込み
- 標準化されたスタイル
JavaやC++ は 「機能は豊富だが学習コストが高い」、Pythonは 「書きやすいが遅い」。その両者を超える「シンプルだが速い」というポジションをGoが埋めました。
1-4. 互換性の保証(Go 1.x promise)
Go 1.0リリース時に 「Go 1互換性ガイドライン」が打ち立てられ、Go 1.xの間は既存のGoプログラムが動き続けることが保証されています。
これはPython 2→3の悪夢を見て育った言語の判断で、コミュニティから高く評価されています。実際、2012年のGo 1.0のコードが今も動きます。
1-5. このセクションのまとめ
Goの出自:
2007年にGoogleで開発開始
Pike / Thompson / Griesemer
C/Unixの系譜の現代版を目指す
哲学:
Less is more(意図的に機能を絞る)
gofmt / go vetで規約を強制
公開・非公開は名前の最初の文字
歴史:
Go 1.0 (2012)
Go 1.11 modules (2018)
Go 1.18 Generics (2022)
Go 1.22ループ変数修正 (2024)
互換性:
Go 1.xの間は既存コードが動き続ける
破壊的変更を避ける文化
次のセクションでは、Goの実行環境とツールチェインを扱います。
2. 実行環境とツールチェイン
Goは 「単一バイナリ + 静的リンク」で配布される。実行に JVMのようなVMが不要で、起動が瞬時。これがクラウド・コンテナ時代にGoが選ばれる理由のひとつです。
2-1. Goコンパイラとランタイム
go build main.go # mainをネイティブバイナリにコンパイル
./main # 実行(依存なし、静的リンク)
Goは AOT(Ahead-of-Time)コンパイラで、機械語に直接変換します。JVMのようなランタイムは不要ですが、Goプログラムには 小さなランタイム(GC、goroutineスケジューラ)がバイナリに含まれます。
バイナリのサイズ
Hello Worldで 約2MB。標準ライブラリやランタイムが入っているためCより大きいですが、JavaやNodeの依存込みサイズより遥かに小さい。
内部スケジューラ(M:Nモデル)
Goのgoroutineは OSスレッドではなく、Goランタイムが管理する軽量タスク。M個のgoroutineをN個のOSスレッドで動かすM:Nスケジューラを持ちます。
goroutine 1 ──┐
goroutine 2 ──┤
goroutine 3 ──┼─→ OSスレッド (P)
... 数百万 ──┘
I/O待ちで自動的に他のgoroutineに切り替わる
ブロックするsyscallは別OSスレッドへ移動
2-2. ガベージコレクション
GoのGCは 「並行・非世代別・低レイテンシ」が特徴。Javaのような世代別GCではなく、低い停止時間を最優先しています。
- ストップ・ザ・ワールド時間: 通常1ms未満
- アプリと並行にGCが走る(concurrent mark-and-sweep)
- 世代別ではない(割り切り)
- 低レイテンシ重視
GOGC 環境変数でGCの頻度を調整できます(デフォルト100 = ヒープが2倍になるまでGCしない)。
2-3. クロスコンパイル
Goの強みのひとつが 「他OS / アーキテクチャ向けバイナリを簡単に作れる」こと。
GOOS=linux GOARCH=amd64 go build -o myapp-linux
GOOS=darwin GOARCH=arm64 go build -o myapp-mac-arm
GOOS=windows GOARCH=amd64 go build -o myapp.exe
GOOS と GOARCH を指定するだけで、Linux上でmacOS / Windows / FreeBSD / ARM向けバイナリが作れる。これがクラウドネイティブツールでGoが支配的な理由のひとつです。
2-4. goツールチェインの全体像
go build # ビルド
go run main.go # ビルドして実行
go test # テスト
go test -bench . # ベンチマーク
go vet # 静的解析
go fmt # フォーマット(gofmtと同等)
go mod init # モジュール初期化
go mod tidy # 依存整理
go get example.com/pkg # 依存追加
go install # $GOPATH/binにインストール
go doc fmt.Println # ドキュメント参照
go env # 環境変数表示
go version
go コマンドはビルド・テスト・依存管理・ドキュメント生成・フォーマット・配布まで すべての操作の統一エントリポイント。「サードパーティツールが不要」というのがGoの体験の良さです。
2-5. このセクションのまとめ
コンパイル:
AOTコンパイル、ネイティブバイナリ
小さなランタイム(GC、scheduler)が同梱
単一バイナリで配布可能
GC:
並行・低レイテンシ重視
停止時間1ms未満
世代別ではない(割り切り)
クロスコンパイル:
GOOS / GOARCHの組み合わせ
Linux上でWindows / macOSバイナリも作れる
goツール:
build / run / test / vet / fmt / mod / doc全部入り
サードパーティ不要
次のセクションでは、Goの型と変数を扱います。
3. 型と変数
Goの型システムは 静的型付け だが、シンプルかつ厳格。Javaのようなクラス階層・継承はなく、struct + interface で表現します。
3-1. プリミティブ型
| 型 | 説明 |
|---|---|
bool |
真偽値 |
int / int8 / int16 / int32 / int64 |
符号付き整数 |
uint / uint8 / … / uint64 |
符号なし |
byte |
uint8のエイリアス |
rune |
int32のエイリアス(Unicodeコードポイント) |
float32 / float64 |
浮動小数点 |
complex64 / complex128 |
複素数 |
string |
文字列(イミュータブル、UTF-8) |
intは処理系依存
int のサイズは プラットフォーム依存(多くの場合64bit)。サイズを明示したいときは int32 int64 を使う。
stringはUTF-8
s := "こんにちは"
len(s) // 15(バイト数)
for i, r := range s {
fmt.Println(i, string(r)) // iはbyte index、rはrune(コードポイント)
}
Goのstringは UTF-8のバイト列。len はバイト数、range はrune(コードポイント)単位で取り出します。
3-2. 変数宣言と:=
var x int = 10
var y = 10 // 型推論
var z int // ゼロ値で初期化(z = 0)
x := 10 // 関数内での短縮宣言(var + 型推論)
var (
a = 1
b = "hello"
c = true
)
:= は関数内のみ
package main
x := 10 // エラー!関数外では := が使えない
var x = 10 // OK
トップレベルでは var、関数内では := が慣習。
未使用変数はエラー
func main() {
x := 10 // エラー: declared and not used
}
「未使用変数 / importはコンパイルエラー」というのがGoの特徴的なルール。死コードを防ぎ、コードベースを綺麗に保つ意図です。
3-3. ゼロ値
Goでは **すべての型に「ゼロ値」**があり、明示的に初期化しなくても安全な値が入る。
| 型 | ゼロ値 |
|---|---|
| 数値 | 0 |
bool |
false |
string |
"" |
| ポインタ | nil |
| slice / map / channel / func / interface | nil |
| struct | 各フィールドのゼロ値 |
var x int // 0
var s string // ""
var p *int // nil
var arr []int // nil(空sliceではない、注意)
「未初期化変数でUB」というCと違い、Goは安全側に倒した設計。
3-4. 型変換は明示
Goは 暗黙の型変換を許しません。違う型同士を演算するには明示的に変換する必要があります。
i := 10
f := 3.14
sum := i + f // コンパイルエラー!
sum := float64(i) + f // OK
これが「JavaやJSの暗黙変換のバグを防ぐ」 Goの選択です。
3-5. 定数とiota
const Pi = 3.14159
const Greeting = "hello"
const (
StatusOK = 200
StatusErr = 500
)
iota(連番定数)
const (
Sunday = iota // 0
Monday // 1
Tuesday // 2
Wednesday // 3
Thursday // 4
Friday // 5
Saturday // 6
)
iota はconstブロック内で0から順にインクリメントされる特別な識別子。enum相当として使われます。
const (
KB = 1 << (10 * (iota + 1)) // 1024
MB // 1048576
GB // 1073741824
)
3-6. このセクションのまとめ
プリミティブ:
intは処理系依存(int32 / int64で固定)
stringはUTF-8、lenはバイト数、rangeはrune
byte = uint8、rune = int32
変数:
var / := / 未使用エラー
関数外ではvar、関数内では :=
ゼロ値:
すべての型にゼロ値あり、安全
数値=0、string=""、ポインタ・slice・map=nil
型変換:
暗黙変換なし
float64(i) のように明示
定数:
const、iotaで連番
enum相当として頻用
次のセクションでは、Goの制御フローと関数を扱います。
4. 制御フローと関数
Goの制御フローは シンプル。if、for、switch の3つで、while も do-while もありません。for がすべてを兼ねます。
4-1. if / for / switch
if
if x > 0 {
fmt.Println("positive")
} else if x < 0 {
fmt.Println("negative")
} else {
fmt.Println("zero")
}
// 初期化文付き(イディオム)
if err := doSomething(); err != nil {
return err
}
// errはこのスコープ内のみ
「初期化文付きif」がGoの特徴的なイディオム。err をifスコープに閉じ込めます。
for(唯一のループ)
// C風
for i := 0; i < 10; i++ {
fmt.Println(i)
}
// while相当
for x > 0 {
x--
}
// 無限ループ
for {
if done() { break }
}
// range
for i, v := range slice {
fmt.Println(i, v)
}
while という単語はなく、for が条件付きループも兼ねる。シンプル。
switch
switch day {
case 1, 2, 3, 4, 5:
fmt.Println("weekday")
case 6, 7:
fmt.Println("weekend")
default:
fmt.Println("invalid")
}
Goの switch は break 不要(自動的に抜ける)。fall-throughしたいなら明示的に fallthrough キーワード。
// 型スイッチ
switch v := x.(type) {
case int:
fmt.Println("int:", v)
case string:
fmt.Println("string:", v)
default:
fmt.Println("other")
}
4-2. 関数と多値返却
func add(a, b int) int {
return a + b
}
// 同じ型なら省略可
func swap(a, b int) (int, int) {
return b, a
}
// 多値返却(Goの文化)
func divmod(a, b int) (int, int) {
return a / b, a % b
}
q, r := divmod(10, 3)
名前付き戻り値
func divmod(a, b int) (q, r int) {
q = a / b
r = a % b
return // naked return
}
短い関数なら名前付き戻り値で簡潔に書けますが、長い関数では混乱を招きます。
errorを返すイディオム
func readFile(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return data, nil
}
data, err := readFile("data.txt")
if err != nil {
return err
}
Goでは「最後の戻り値をerrorにする」のが標準。例外の代わりにこのパターンが普及しています。
4-3. defer
defer は 「関数を抜けるときに実行する」を予約する仕組み。リソース解放に使う。
func readFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close() // 関数を抜けるとき必ずCloseされる
return io.ReadAll(f)
}
複数deferはLIFO
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
// 出力: 3, 2, 1(逆順)
落とし穴
for _, f := range files {
file, _ := os.Open(f)
defer file.Close() // ループの中でdeferすると、関数終了まで溜まる!
}
// 多数のファイルが開きっぱなし
defer は 関数スコープで実行されるので、ループ内で書くと全てのdeferが末尾に集約されます。ループ内ではクロージャか別関数に切り出す。
4-4. クロージャ
func makeCounter() func() int {
count := 0
return func() int {
count++
return count
}
}
c := makeCounter()
c() // 1
c() // 2
c() // 3
外側のスコープの変数を捕捉するのは他言語と同じ。
Go 1.22でループ変数の挙動が変わった
// Go 1.21まで: ループ変数iがすべてのgoroutineで共有される
for i := 0; i < 3; i++ {
go func() { fmt.Println(i) }()
}
// 出力例: 3, 3, 3になることが多い
// Go 1.22+: 反復ごとに新しいi
// 出力: 0, 1, 2(順不同)
これは長年バグの温床だった「ループ変数の捕捉」問題を解消する 歴史的な変更でした。
4-5. このセクションのまとめ
制御:
if / for / switchのみ(whileなし)
ifには初期化文付きイディオム(if err := ...; err != nil)
forがすべてのループを兼ねる
switchはbreak不要、caseに複数値、型スイッチ
関数:
多値返却が標準(特に (T, error))
名前付き戻り値も可(短い関数で)
最後の戻り値をerrorにするイディオム
defer:
関数を抜けるとき実行
リソース解放に多用
LIFO(後勝ち)
ループ内deferは危険
クロージャ:
Go 1.22でループ変数の挙動が変更された
次のセクションでは、Goのコレクション(slice / map / array)を扱います。
5. コレクション(slice / map / array)
Goのコレクションは array(固定長)・slice(可変長)・map の3つ。sliceの内部構造を理解するのが鍵です。
5-1. arrayとslice
array(固定長)
var arr [5]int // [0, 0, 0, 0, 0]
arr := [5]int{1, 2, 3, 4, 5}
arr := [...]int{1, 2, 3} // サイズは推論
len(arr) // 5
arrayは 値型。代入や関数引数でコピーされる。サイズは型の一部([5]int と [6]int は別の型)。
slice(可変長、実用的にはこちらが主流)
s := []int{1, 2, 3, 4, 5}
s = append(s, 6) // [1, 2, 3, 4, 5, 6]
sub := s[1:3] // [2, 3]
len(s) // 6
cap(s) // 容量
// makeで作成
s := make([]int, 5) // 長さ5、容量5
s := make([]int, 5, 10) // 長さ5、容量10
5-2. sliceの内部構造
sliceは 「ポインタ + 長さ + 容量」のヘッダ。基底配列を共有します。
slice ┌─────────┐
│ pointer │ ──→ 基底配列のどこかを指す
│ len │
│ cap │
└─────────┘
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:3] // [2, 3]
s[0] = 999
fmt.Println(arr) // [1, 999, 3, 4, 5] ← 元の配列も変わる!
sliceは 基底配列のビューなので、元の配列を変更すると覚える。これが嵌りどころ。
appendの挙動
s := []int{1, 2, 3}
s2 := append(s, 4) // 容量が足りなければ新しい基底配列が作られる
appendが容量を超えた場合、新しい配列にコピーしてから追加。これにより s と s2 が異なる配列を指すこともあれば、同じ配列を共有することも(容量に余裕があれば共有のまま)。
copy
src := []int{1, 2, 3}
dst := make([]int, 3)
copy(dst, src) // 完全コピー
「完全に独立なsliceが欲しい」なら copy。
5-3. map
m := map[string]int{
"a": 1,
"b": 2,
}
m["c"] = 3
delete(m, "a")
v := m["x"] // 存在しないキーはゼロ値(0)
v, ok := m["x"] // okでキーの有無を判定
len(m) // 要素数
// makeで作成
m := make(map[string]int)
m := make(map[string]int, 100) // 初期容量ヒント
落とし穴: nil map
var m map[string]int // ゼロ値はnil
m["a"] = 1 // panic!nil mapに書けない
make で初期化するか、リテラルで初期化する。
順序
mapのイテレーション順は ランダム。Goは意図的に順序を保証しません(隠された依存を防ぐため)。
for k, v := range m {
fmt.Println(k, v) // 順序は実行ごとに異なる
}
5-4. range
for i, v := range slice { // index, value
...
}
for k, v := range mapVar { // key, value
...
}
for ch := range channel { // channel
...
}
for r := range "hello" { // 文字列はruneを返す
...
}
_ で不要な値を無視:
for _, v := range slice {
...
}
5-5. このセクションのまとめ
array:
固定長、値型
サイズは型の一部
実用ではほぼ使われない
slice:
可変長、基底配列のビュー(pointer + len + cap)
appendで拡張、容量を超えると新配列
代入で参照を共有する点に注意
完全コピーはcopy
map:
キー・値マップ
ゼロ値はnil(書き込み不可)→ makeで初期化
v, ok := m["x"] で有無判定
順序は保証されない(意図的)
range:
for i, v := rangeのイディオム
index/keyを _ で無視
次のセクションでは、GoのOOPに相当するstructとmethodsを扱います。
6. structとmethods
Goには 「クラス」がありません。代わりに struct(データ) と メソッド(振る舞い) を組み合わせます。継承もありません。embedding(埋め込み) で再利用します。
6-1. struct
type Person struct {
Name string
Age int
}
p := Person{Name: "Alice", Age: 30}
p := Person{"Alice", 30} // 順序通り(推奨されない)
p := Person{} // ゼロ値("", 0)
p.Name // フィールドアクセス
p.Name = "Bob"
「フィールド名を明示」するのが慣習。順序依存は読みにくく、フィールド追加で壊れます。
匿名struct
config := struct {
Host string
Port int
}{
Host: "localhost",
Port: 8080,
}
一度しか使わないデータ構造に便利。
6-2. メソッドとレシーバ
type Person struct {
Name string
Age int
}
func (p Person) Greet() string {
return "Hi, I'm " + p.Name
}
p := Person{Name: "Alice"}
p.Greet() // "Hi, I'm Alice"
func (p Person) Greet() の (p Person) が レシーバ。「Person型の値pに対するメソッド」を意味します。
任意の型にメソッドを定義可能
type Celsius float64
func (c Celsius) ToFahrenheit() Celsius {
return c*9/5 + 32
}
c := Celsius(100)
c.ToFahrenheit() // 212
「自分のパッケージ内で定義した型」になら、組み込み型(の別名)にもメソッドを生やせる。
6-3. 値レシーバvsポインタレシーバ
// 値レシーバ: コピーが渡される
func (p Person) Greet() string { ... }
// ポインタレシーバ: ポインタが渡される(変更可能)
func (p *Person) Birthday() { p.Age++ }
使い分け
値レシーバ:
- 小さいstruct(数フィールド)
- イミュータブル
- メソッド内で変更しない
ポインタレシーバ:
- 大きいstruct(コピーコスト)
- 変更したい
- structが同期プリミティブを含む(コピー禁止)
「一貫性」のため、1つのstructのすべてのメソッドで統一するのが慣習。
自動デリファレンス
p := Person{}
p.Birthday() // OK: コンパイラが (&p).Birthday() に変換
(&p).Birthday() // 同等
6-4. embedding(埋め込み)
Goには 継承がない代わりに、埋め込み(embedding) で再利用します。
type Animal struct {
Name string
}
func (a Animal) Speak() string {
return a.Name + " makes a sound"
}
type Dog struct {
Animal // 埋め込み(フィールド名なし)
Breed string
}
d := Dog{Animal: Animal{Name: "Rex"}, Breed: "Labrador"}
d.Name // "Rex" ← 直接アクセス可能
d.Speak() // "Rex makes a sound" ← Animalのメソッドが透過的に呼べる
「DogはAnimalを含む(has-a)」関係ですが、外から見ればDogがAnimalのメソッドを持つように見えます。
継承との違い
これは継承とは違います。
- 多態性はinterfaceでのみ実現(DogをAnimalとして扱えるわけではない)
- オーバーライドは「同名メソッドを定義」する形
「継承より合成(composition over inheritance)」が現代OOPの指針ですが、Goはこれを言語レベルで強制しています。
6-5. このセクションのまとめ
struct:
type T struct { ... }
T{Field: value} で初期化
匿名structも書ける
メソッド:
func (r T) Method() ... のレシーバ構文
自分のパッケージで定義した型に生やせる
レシーバ:
値: コピーが渡される(小さい・イミュータブル)
ポインタ: 変更可能(大きい・mutable)
一貫性のため統一
embedding:
type Dog struct { Animal; ... }
継承ではなく合成
メソッドが透過的に見える
次のセクションでは、Goのinterfaceを扱います。
7. interface
Goのinterfaceは 「振る舞いの契約」。暗黙的に実装されるのが特徴で、Javaの implements 宣言は不要です。
7-1. interfaceの基本
type Animal interface {
Speak() string
}
type Dog struct{}
func (d Dog) Speak() string { return "Woof!" }
type Cat struct{}
func (c Cat) Speak() string { return "Meow!" }
var a Animal = Dog{}
a.Speak() // "Woof!"
a = Cat{}
a.Speak() // "Meow!"
interfaceは「この メソッドを持つ型なら何でも入る変数」。
7-2. 暗黙的実装
type Reader interface {
Read(p []byte) (n int, err error)
}
// File型がReadメソッドを持つなら、
// 自動的にReaderとして使える(implements不要)
「implements を書かなくていい」のがJavaと決定的に違う点。これにより、
- **「後からinterfaceを追加」**できる
- 依存方向の柔軟性: 利用者がinterfaceを定義できる
// interfaceは使う側が定義する(Goの流儀)
package consumer
type Storage interface {
Save(data []byte) error
}
func Process(s Storage) { ... }
これがGoの 「small interface(小さなインターフェース)」 文化の基盤。io.Reader io.Writer のように メソッド1つのinterface が標準ライブラリに大量にあります。
7-3. 空interfaceとany
// 空interfaceはどんな型でも受け入れる
var x interface{} = 42
x = "hello"
x = []int{1, 2, 3}
// Go 1.18+: anyはinterface{} のエイリアス
var x any = 42
any はGo 1.18で追加されたエイリアス。interface{} より読みやすいので推奨。ただし any を多用するのは「型システムを諦める」ことで、避けるべき場面が多い。
7-4. 型アサーションと型スイッチ
型アサーション
var x any = "hello"
s := x.(string) // s = "hello"
n, ok := x.(int) // n = 0, ok = false(パニックを避ける)
型スイッチ
func describe(v any) string {
switch x := v.(type) {
case int:
return fmt.Sprintf("int: %d", x)
case string:
return fmt.Sprintf("string: %s", x)
case []int:
return fmt.Sprintf("slice of size %d", len(x))
default:
return "unknown"
}
}
v.(type) はswitch文の中だけで使える特殊構文。
7-5. interfaceの落とし穴(nilの罠)
Goの有名な罠:
var p *MyError = nil
var err error = p // errはnilではない!
if err == nil {
// この分岐に来ない!
}
理由: interfaceは 「型 + 値」 のペアで構成され、型がnilでないとinterface自体がnilではない。
正しい書き方:
func doSomething() error {
var p *MyError
if cond {
p = &MyError{...}
}
return p // BAD: pがnilでもerrorはnon-nil
}
// 正解
func doSomething() error {
if cond {
return &MyError{...}
}
return nil
}
「ポインタ型のゼロ値をinterfaceとして返さない」のが鉄則。
7-6. このセクションのまとめ
interface:
メソッドの集合(契約)
暗黙的実装(implements不要)
small interface文化(io.Readerのように1メソッド推奨)
定義の方向性:
Javaは提供側がimplementsを書く
Goは利用側が必要なinterfaceを定義する
→ 依存方向が柔軟
anyと空interface:
Go 1.18+ でany(interface{} のエイリアス)
使いすぎは型システムを諦めること
型アサーション / 型スイッチ:
v.(T) でアサート
switch v := x.(type) で分岐
罠:
nilポインタをinterfaceに入れるとnon-nil扱い
ポインタ型のゼロ値を返さない
次のセクションでは、Goの特徴的なエラーハンドリングを扱います。
8. エラーハンドリング
Goには 例外(try/catch)がありません。代わりに 「errorを多値返却で返す」というシンプルなアプローチを採用しています。賛否両論ありますが、Goらしい設計判断です。
8-1. error型
error は組み込みのinterface:
type error interface {
Error() string
}
「Error() string を持てばerror」というシンプルな契約。
import "errors"
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
result, err := divide(10, 0)
if err != nil {
fmt.Println("error:", err)
}
errorチェックのイディオム
result, err := someCall()
if err != nil {
return err
}
// 続きの処理
このパターンが Goコードの圧倒的な比率を占めます。冗長と批判されることもありますが、「失敗ケースを必ず明示する」という規律を強制します。
8-2. errors.Is / errors.As
エラーを 特定の型・値かどうかを判定する標準API(Go 1.13+)。
var ErrNotFound = errors.New("not found")
result, err := find()
if errors.Is(err, ErrNotFound) {
// 特定エラーの判定
}
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println(pathErr.Path)
}
errors.Is: 「同じエラー値か」を判定(wrapされていても再帰的に辿る)。
errors.As: 「特定の型に変換可能か」を判定。
8-3. error wrapping (%w)
エラーチェインの仕組み(Go 1.13+)。
func loadConfig() error {
data, err := os.ReadFile("config.yml")
if err != nil {
return fmt.Errorf("loadConfig: %w", err) // %wでwrap
}
...
}
err := loadConfig()
fmt.Println(err) // "loadConfig: open config.yml: no such file"
errors.Is(err, os.ErrNotExist) // true(チェインを辿る)
%w で元のエラーを「チェインに繋ぐ」。受け取り側は errors.Is / errors.As で元の情報にアクセスできる。Javaの getCause() 相当。
8-4. panicとrecover
panic は 致命的エラーで、関数を即座に抜けます。recover でキャッチ可能。
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}()
panic("oops") // 上のdeferが捕まえる
使い所
panic は 本当に異常な状況でのみ使う。通常のエラーは error で。
panicを使う場面:
- 配列の境界外
- nilポインタ参照外し
- プログラムのバグ(assert相当)
panicを使わない:
- ネットワークエラー
- ファイルが存在しない
- パースエラー
→ これらはerrorで返す
recover はHTTPサーバなどで「リクエスト処理がパニックしてもサーバを落とさない」用途で使われます。
8-5. このセクションのまとめ
error型:
Error() stringを持つinterface
return value, errのイディオム
if err != nilの繰り返し
errors.Is / errors.As (Go 1.13+):
Is: 特定のerror値か(wrapも辿る)
As: 特定の型か
error wrapping:
fmt.Errorf("...: %w", err) でチェイン
errors.Isで元のエラーを判定可能
panic / recover:
panicは本当に異常な場合
通常のエラーはerror
recoverはサーバの安全網に
次のセクションでは、Goの最も特徴的な機能――GoroutineとChannelを扱います。
9. GoroutineとChannel
Goの最大の魅力は 言語に組み込まれた並行処理。goroutine(軽量スレッド) と channel(通信プリミティブ) で、「メモリ共有でなく通信で同期する」(“Don’t communicate by sharing memory; share memory by communicating”)哲学を実現します。
9-1. goroutine
go doSomething() // 別goroutineで起動
go func() {
fmt.Println("in goroutine")
}()
// 関数引数を渡せる
go fetch(url)
go キーワードを付けるだけで関数が 別のgoroutineで並行実行される。これがGoの象徴的な機能。
goroutineの特徴
for i := 0; i < 1_000_000; i++ {
go work(i) // 100万goroutine起動
}
9-2. channel
ch := make(chan int) // unbuffered
ch <- 42 // 送信
v := <-ch // 受信
ch := make(chan int, 10) // buffered(容量10)
unbuffered channel
「送信と受信が同時に揃わないとブロック」する。同期通信。
go func() {
ch <- 1
}()
v := <-ch // goroutineが送信するまでブロック
buffered channel
容量分まで非同期で貯められる。
ch := make(chan int, 3)
ch <- 1 // OK
ch <- 2 // OK
ch <- 3 // OK
ch <- 4 // ブロック(バッファが満杯)
close
close(ch)
// 受信側
for v := range ch {
fmt.Println(v)
}
// chが閉じられるまで受信し続ける、閉じたら終了
「送信側が閉じる」のが鉄則。閉じたchannelに送信するとpanic。
9-3. select
複数のchannel操作を待つ。
select {
case v := <-ch1:
fmt.Println("from ch1:", v)
case v := <-ch2:
fmt.Println("from ch2:", v)
case ch3 <- 42:
fmt.Println("sent to ch3")
case <-time.After(1 * time.Second):
fmt.Println("timeout")
default:
fmt.Println("nothing ready")
}
select は 「複数のchannelのうち動けるものを動かす」スイッチ。default があれば非ブロッキングに、time.After でタイムアウト処理ができる。Goの並行処理の核。
9-4. context.Context
長期処理のキャンセル・タイムアウト・値伝播を統一的に扱う標準API。
import "context"
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := longRunning(ctx); err != nil {
...
}
}
func longRunning(ctx context.Context) error {
select {
case <-time.After(10 * time.Second):
return nil
case <-ctx.Done():
return ctx.Err() // タイムアウトまたはキャンセル
}
}
context.Context は 「キャンセルシグナル + デッドライン + リクエストスコープの値」を運ぶ標準。HTTPサーバやDBクエリで第一引数に渡すのがGoの作法。
9-5. syncパッケージ(Mutex / WaitGroup / Once)
「channelが万能ではない」場面では従来の同期プリミティブも使う。
Mutex
import "sync"
var mu sync.Mutex
counter := 0
go func() {
mu.Lock()
defer mu.Unlock()
counter++
}()
RWMutex も。読み取りが多い場合に。
WaitGroup
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
work(i)
}(i)
}
wg.Wait() // すべてのgoroutine完了を待つ
Once
var once sync.Once
once.Do(initialize) // 初回だけ呼ばれる
once.Do(initialize) // 2回目以降はスキップ
シングルトン・遅延初期化に。
9-6. 並行パターン
Worker Pool
jobs := make(chan int, 100)
results := make(chan int, 100)
for w := 0; w < 5; w++ {
go func() {
for j := range jobs {
results <- process(j)
}
}()
}
for i := 0; i < 100; i++ {
jobs <- i
}
close(jobs)
Fan-out / Fan-in
// Fan-out: 1つのchannelから複数のgoroutine
// Fan-in: 複数のchannelから1つの結果に集約
errgroup(標準準拠のgolang.org/x)
import "golang.org/x/sync/errgroup"
g, ctx := errgroup.WithContext(ctx)
for _, url := range urls {
url := url
g.Go(func() error {
return fetch(ctx, url)
})
}
if err := g.Wait(); err != nil {
return err
}
複数goroutineの エラー収集 + キャンセル。実用上の頻出パターン。
9-7. このセクションのまとめ
goroutine:
go f() で起動
軽量(8KBスタック)、M:Nスケジュール
数百万が現実的
channel:
make(chan T) / make(chan T, n)
ch <- v / v := <-ch
closeで終了通知、rangeで受信
select:
複数channelを待つ
defaultで非ブロッキング、time.Afterでタイムアウト
context.Context:
キャンセル・タイムアウト・値伝播の標準
HTTP/DBの第一引数に渡す作法
sync:
Mutex / RWMutex / WaitGroup / Once
channelで書きにくい場面に
並行パターン:
Worker Pool / Fan-out-in / errgroup
次のセクションでは、Go 1.18で導入されたGenericsを扱います。
10. Generics(Go 1.18+)
Goは 長らくジェネリクスを「意図的に避けてきた」言語でした。しかし2022年のGo 1.18で 10年越しに導入。シンプルさを保ちつつ型安全な汎用コードが書けるようになりました。
10-1. 動機
それまでGoでは「コレクションを汎用化したい」場合、interface{}(any)を使うか、コード生成ツールを使うかしかありませんでした。
// Go 1.17まで
func Min(a, b int) int { ... }
func MinFloat(a, b float64) float64 { ... }
// 型ごとに同じコードを書く必要あり
ジェネリクスで:
// Go 1.18+
func Min[T cmp.Ordered](a, b T) T {
if a < b { return a }
return b
}
Min(1, 2) // int
Min(1.0, 2.0) // float64
Min("a", "b") // string
10-2. 型パラメータ
// 関数の型パラメータ
func Map[T, U any](s []T, f func(T) U) []U {
result := make([]U, len(s))
for i, v := range s {
result[i] = f(v)
}
return result
}
squared := Map([]int{1, 2, 3}, func(x int) int { return x * x })
// [1, 4, 9]
strings := Map([]int{1, 2, 3}, strconv.Itoa)
// ["1", "2", "3"]
型パラメータの型
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) {
s.items = append(s.items, v)
}
func (s *Stack[T]) Pop() T {
n := len(s.items) - 1
v := s.items[n]
s.items = s.items[:n]
return v
}
s := Stack[int]{}
s.Push(1)
s.Push(2)
s.Pop() // 2
10-3. 制約(constraint)
型パラメータには 制約 を指定できる。
// any: どんな型でも
func Print[T any](v T) { ... }
// comparable: == で比較可能
func Equal[T comparable](a, b T) bool { return a == b }
// cmp.Ordered(Go 1.21+): < で比較可能
func Min[T cmp.Ordered](a, b T) T { ... }
カスタム制約
type Numeric interface {
int | int32 | int64 | float32 | float64
}
func Sum[T Numeric](s []T) T {
var total T
for _, v := range s {
total += v
}
return total
}
| で型unionを表現する独特の構文。
10-4. 使うべきか
ジェネリクスは強力ですが、Goコミュニティでは慎重に使うのが推奨されています。
使うべき:
- 汎用コレクション(Map、Set、Stack)
- 型をまたぐアルゴリズム(Min、Max、Sort)
- ライブラリ作者が型安全なAPIを提供したい
避けるべき:
- anyでも書けるシンプルな処理
- 抽象化のために抽象化する場合
- 1か所でしか使わない型
「まずinterface、必要ならgenerics」が現代の指針。
10-5. このセクションのまとめ
動機:
any(interface{})の型安全性を補完
型パラメータ:
func Min[T any](a, b T) T
type Stack[T any] struct { ... }
制約:
any(任意)/ comparable(==)/ cmp.Ordered(<)
カスタム: type Numeric interface { int | float64 }
指針:
汎用コレクション・アルゴリズムには有効
「まずinterface、必要ならgenerics」
次のセクションでは、Goのモジュールとパッケージを扱います。
11. モジュールとパッケージ
Goの 「モジュール / パッケージ」 は他言語と少し違う設計。Go 1.11で導入された go modules が現代の標準です。
11-1. パッケージの基本
// foo/bar.go
package foo
func Hello() string { return "hello" }
// main.go
package main
import "myproject/foo"
func main() {
fmt.Println(foo.Hello())
}
「ディレクトリ = パッケージ」が原則。1ディレクトリに複数 .go ファイルがあっても、すべて同じパッケージ。
公開・非公開
package foo
func Public() {} // 大文字始まり = 公開
func private() {} // 小文字始まり = 非公開
「最初の文字の大小」で公開・非公開が決まる。Javaの public / private の代わり。
11-2. go modules
go mod init github.com/user/myproject # モジュール初期化
go mod tidy # 依存整理
go get github.com/foo/bar # 依存追加
go get -u # 全依存を更新
go mod download # 依存ダウンロード
go mod init でモジュールを開始すると go.mod が生成されます。
11-3. go.mod / go.sum
go.mod
module github.com/user/myproject
go 1.22
require (
github.com/gin-gonic/gin v1.9.1
github.com/stretchr/testify v1.8.4
)
go.sum
依存の 暗号化ハッシュを記録。改竄検知に。
github.com/gin-gonic/gin v1.9.1 h1:...
github.com/gin-gonic/gin v1.9.1/go.mod h1:...
両方ともGitにcommitする。
11-4. internalパッケージ
myproject/
├── api/
│ └── handler.go
├── internal/
│ └── secret/
│ └── helper.go
└── main.go
internal ディレクトリ配下のパッケージは 「同じモジュール内からしかimportできない」。これにより外部に公開したくない実装を隠せる。Goの標準的なカプセル化手段。
11-5. このセクションのまとめ
パッケージ:
ディレクトリ = パッケージ
最初の文字の大小で公開・非公開
package mainは実行可能、それ以外はライブラリ
go modules:
go mod init / tidy / get
go.modに依存、go.sumにハッシュ
両方をcommit
internal:
internal/ 配下は同じモジュール内のみ
実装隠蔽の標準パターン
次のセクションでは、Go標準ライブラリの主要要素を扱います。
12. 標準ライブラリの主要要素
Goの標準ライブラリは 「実用的で完結した」設計。Webサーバ・JSON・暗号など、多くの場合 サードパーティに頼らず標準だけで書けるのがGoの体験の良さです。
12-1. fmtとlog
import "fmt"
fmt.Println("hello")
fmt.Printf("%s is %d\n", name, age)
fmt.Sprintf("...") // 文字列にフォーマット
fmt.Errorf("error: %w", err) // %wでwrap
import "log"
log.Printf("info: %s", msg)
log.Fatal("fatal error") // Fatalはexit(1)
Printfの主なフォーマット
%vデフォルト
%+v structのフィールド名付き
%#v Go構文表現
%T型
%d整数
%s文字列
%qクォート付き文字列
%x 16進数
%w error wrap(fmt.Errorf専用)
12-2. ioとbufio
import (
"io"
"bufio"
"os"
)
f, _ := os.Open("data.txt")
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
io.Reader / io.Writer は Goの最重要interface。標準ライブラリ全体がこれをベースにしています。
// 任意のReaderからコピー
io.Copy(dst, src)
12-3. net/http
import "net/http"
// クライアント
resp, err := http.Get("https://example.com")
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
// サーバ
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello")
})
http.ListenAndServe(":8080", nil)
「標準ライブラリで本格的なWebサーバが書ける」のがGoの特徴。Java/Springのようなフレームワークが必須ではない。
12-4. encoding/json
import "encoding/json"
type User struct {
Name string `json:"name"`
Age int `json:"age,omitempty"`
}
// マーシャル
data, _ := json.Marshal(User{Name: "Alice", Age: 30})
// アンマーシャル
var u User
json.Unmarshal(data, &u)
json:"name" は struct tag。Goの メタデータ機構で、リフレクション経由で読まれます。
12-5. log/slog(structured logging)
Go 1.21で標準化された 構造化ロギング。
import "log/slog"
slog.Info("user signed in", "user", "alice", "ip", "1.2.3.4")
slog.Error("db error", "err", err, "query", q)
// JSONハンドラ
h := slog.NewJSONHandler(os.Stdout, nil)
logger := slog.New(h)
logger.Info("hello", "key", "value")
長らく logrus zerolog 等のサードパーティが主流でしたが、Go 1.21で 標準ライブラリ化されました。
12-6. このセクションのまとめ
fmt / log:
Println / Printf / Sprintf / Errorf
%wでerror wrap
io / bufio:
io.Reader / io.Writerが中核
bufio.Scannerで行ごと読み
net/http:
クライアントもサーバも標準で書ける
HandleFunc + ListenAndServe
encoding/json:
Marshal / Unmarshal
struct tagで項目名指定
log/slog(1.21+):
標準の構造化ロギング
JSONハンドラで本番向け
次のセクションでは、Goのテスト・ベンチマーク・プロファイリングを扱います。
13. テスト・ベンチマーク・プロファイリング
Goは 「テストを言語に組み込む」発想で、testing パッケージが標準でテスト・ベンチマーク・プロファイル取得を提供します。
13-1. testingパッケージ
// add.go
package mypkg
func Add(a, b int) int { return a + b }
// add_test.go
package mypkg
import "testing"
func TestAdd(t *testing.T) {
if got := Add(1, 2); got != 3 {
t.Errorf("Add(1, 2) = %d, want 3", got)
}
}
go test ./... # すべてのテスト
go test -v # 詳細
go test -run TestAdd # 名前マッチで実行
go test -cover # カバレッジ
go test -race # データレース検出
13-2. テーブルテスト
Goの文化として テーブルテストが標準的。
func TestAdd(t *testing.T) {
cases := []struct {
name string
a, b int
want int
}{
{"1+1", 1, 1, 2},
{"1+(-1)", 1, -1, 0},
{"big", 1000, 2000, 3000},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := Add(tc.a, tc.b); got != tc.want {
t.Errorf("got %d, want %d", got, tc.want)
}
})
}
}
t.Run でサブテストを作る。失敗時にどのケースが失敗したか分かりやすい。
13-3. ベンチマーク
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}
go test -bench .
# BenchmarkAdd-8 1000000000 0.5 ns/op
b.N は自動調整される反復数。
13-4. pprofとPGO
pprof
import _ "net/http/pprof"
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
CPU・メモリ・goroutineプロファイルが取れる。Go標準のプロファイラ。
PGO(Profile-Guided Optimization、Go 1.21+)
go test -cpuprofile=cpu.prof
go build -pgo=cpu.prof
実行プロファイルをコンパイラにフィードバックして最適化。実測で2〜10% 高速化が報告されています。
13-5. このセクションのまとめ
testing:
TestXxx(t *testing.T)
go test ./... / -cover / -race
テーブルテスト:
[]struct + t.Runでサブテスト
Go文化
ベンチマーク:
BenchmarkXxx(b *testing.B)
go test -bench .
pprof:
net/http/pprofでエンドポイント
CPU/メモリ/goroutineプロファイル
PGO(1.21+):
実行プロファイルでコンパイラ最適化
数% 速くなる
14. Go 1.18〜1.23の進化
1.18 (2022) Generics、fuzz testing、workspaces
1.19 (2022) メモリモデルの厳密化、go doc改善
1.20 (2023) PGOプレビュー、エラーチェイン拡張
1.21 (2023) PGO正式、log/slog、min/max/clear、cmpパッケージ
1.22 (2024) forループ変数の挙動修正、math/rand/v2、HTTP routing改善
1.23 (2024) range over function(イテレータ関数)、structured logging改善
15. よくある落とし穴FAQ
Q1. nil sliceと空sliceの違いは?
var s []int // nil
s := []int{} // 空(len 0、cap 0、non-nil)
両方とも len(s) == 0 はtrueだが、s == nil の挙動が違う。JSONエンコード時も違う(nilは null、空は [])。
Q2. mapのkeyの存在確認
v, ok := m["x"]
if ok { ... }
Q3. forループ変数のキャプチャ問題
Go 1.22で 修正された。それ以前のコードでは:
for i := 0; i < 3; i++ {
i := i // 新しいスコープに再宣言
go func() { fmt.Println(i) }()
}
Q4. goroutineリーク
channelが永遠に送受信を待つとgoroutineが解放されない。context.Context でキャンセルを伝える、または select + default で逃げる。
Q5. nil interfaceとnilポインタを混同
第7章7-5参照。ポインタ型のゼロ値を error に入れるとnil扱いされない。
Q6. deferのループ内使用
ループ内 defer は関数末尾まで溜まる。別関数に切り出す。
Q7. go vetを信じる
go vet は表面的なバグを検出する。CIで常時実行。
Q8. importが読みにくい
goimports で自動整理。エディタに統合するのが標準。
Q9. パニックを避けるべきか
通常のエラーは error で。panic はバグレベルの異常時のみ。
Q10. structタグの順序
type User struct {
Name string `json:"name" validate:"required"`
}
スペース区切り、複数のタグ。
16. 図解: goroutine / channel / GC
16-1. M:Nスケジューラ
goroutine(数百万)
G1 G2 G3 G4 G5 ... GN
↓ ランタイムスケジューラが割り当て
P (Processor、論理プロセッサ、GOMAXPROCSで制御)
↓
M (OSスレッド、必要に応じて作る)
↓
CPUコア
16-2. channelの動き
Sender Channel Receiver
│ │ │
│── ch <- 1 ──→ [送信] │ │
│ (バッファ満杯ならブロック) │
│ │ │
│ │ ←── <-ch ──── [受信]│
│ │ │
│ [unbufferedなら]
│ 送信と受信がrendezvous
16-3. GCのタイムライン
時間 →
アプリ: ▓▓▓▓▓▓▓▓▓▓▓▓░▓▓▓▓▓▓▓▓░▓▓▓▓▓▓
GC: ▓▓▓ ▓▓▓
↑ ↑
Stop-the-World
通常 < 1ms
並行マーク&スイープ
低レイテンシ重視
17. 学習ロードマップ(30日)
Week 1(基礎)
Week 2(OOP・並行)
- Day 8-9: メソッド・interface
- Day 10-11: errorハンドリング
- Day 12-13: goroutine / channel
- Day 14: 並行ワーカー
Week 3(モダン)
- Day 15-16: context.Context / sync
- Day 17-18: Generics
- Day 19: testing / table tests
- Day 20-21: 中規模アプリ(CLIまたはWeb API)
Week 4(実践)
- Day 22-23: net/httpでREST API
- Day 24: encoding/json
- Day 25: log/slog
- Day 26: pprofでプロファイル
- Day 27: モジュール公開
- Day 28-30: お気に入りOSSのコードを読む
18. 用語集
- goroutine: Goランタイムが管理する軽量タスク
- channel: goroutine間の通信プリミティブ
- interface: メソッドの集合(暗黙的実装)
- embedding: structに他のstruct/interfaceを埋め込む合成
- slice: 可変長配列のビュー(ポインタ + len + cap)
- defer: 関数を抜けるとき実行する登録機構
- panic / recover: 致命的エラーとrecovery
- goroutineリーク: 解放されないgoroutine
- GMP: Goランタイムスケジューラの単位(goroutine, machine, processor)
- PGO: Profile-Guided Optimization
- Go module: 依存管理単位(go.mod)
発展: Goらしい設計
ここからはGoの各機能を 実例とともに深掘りします。Goroutine、Channel、Context、Generics、標準ライブラリの各論、テスト、デプロイまで。
19. goroutineとchannel完全版
19-1. goroutineの内部
goroutine: ユーザレベルスレッド
- 初期スタック2KB(必要に応じて拡張)
- M:Nスケジューラ(M個のgoroutineをN個のOSスレッド)
- ランタイムが管理(OSは知らない)
- プリエンプティブ(Go 1.14+)
OS thread (M):
- OSが管理
- 数MBのスタック
- カーネル切替が重い
P (Processor):
- 論理プロセッサ
- GOMAXPROCS個(デフォルト = CPUコア数)
- ランキューを持つ
- Mに紐付いてgoroutineを実行
「G - M - P」モデルがGoランタイムの核心。
19-2. goroutineリーク
func leak() {
ch := make(chan int)
go func() {
ch <- 42 // 受信者がいなければ永遠にブロック
}()
// chを読まずにreturn
}
これが繰り返されると、goroutineが累積。runtime.NumGoroutine() で監視。
検出と対策
// pprofでプロファイル
import _ "net/http/pprof"
go http.ListenAndServe("localhost:6060", nil)
// http://localhost:6060/debug/pprof/goroutineで確認
「起動したgoroutineは確実に終わる」設計が必須。context.Context でキャンセル伝播。
19-3. channelの3形態
// unbuffered(同期)
ch := make(chan int)
ch <- 1 // 受信者が来るまでブロック
v := <-ch // 送信者が来るまでブロック
// buffered(非同期)
ch := make(chan int, 10)
ch <- 1 // 容量内なら即時OK
ch <- 2
// closed
close(ch)
v, ok := <-ch // ok = falseで空 + closed
for v := range ch { // closeまで読み続ける
process(v)
}
channelの方向
func producer(ch chan<- int) { // 送信専用
ch <- 42
}
func consumer(ch <-chan int) { // 受信専用
fmt.Println(<-ch)
}
ch := make(chan int)
go producer(ch)
consumer(ch)
引数で方向を指定すると、誤った使い方をコンパイル時に検出できる。
19-4. channelパターン
Worker pool
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
time.Sleep(time.Second)
results <- j * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
for a := 1; a <= 5; a++ {
fmt.Println(<-results)
}
}
Fan-out / Fan-in
// Fan-out: 1つの入力を複数のgoroutineで処理
func fanOut(input <-chan int, n int) []<-chan int {
outs := make([]<-chan int, n)
for i := 0; i < n; i++ {
out := make(chan int)
outs[i] = out
go func() {
defer close(out)
for v := range input {
out <- process(v)
}
}()
}
return outs
}
// Fan-in: 複数の出力を1つに統合
func fanIn(outs ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
for _, c := range outs {
wg.Add(1)
go func(c <-chan int) {
defer wg.Done()
for v := range c {
out <- v
}
}(c)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
Pipeline
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
out <- n
}
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for v := range in {
out <- v * v
}
}()
return out
}
func main() {
for n := range square(square(generate(2, 3))) {
fmt.Println(n) // 16, 81
}
}
「channelをパイプとして繋ぐ」Unixライクなデータ処理。
19-5. selectの応用
// タイムアウト
select {
case v := <-ch:
process(v)
case <-time.After(5 * time.Second):
return errors.New("timeout")
}
// キャンセル
select {
case v := <-ch:
process(v)
case <-ctx.Done():
return ctx.Err()
}
// non-blocking送受信
select {
case ch <- v:
// 送信できた
default:
// バッファ満杯
}
// 公平な選択
select {
case <-ch1:
...
case <-ch2:
...
case <-ch3:
...
}
// 複数readyなら、ランダムに選ばれる
19-6. context.Context完全版
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 子コンテキスト
ctx2, cancel2 := context.WithTimeout(ctx, 10*time.Second)
defer cancel2()
ctx3, cancel3 := context.WithDeadline(ctx, time.Now().Add(5*time.Second))
defer cancel3()
// 値の伝播
ctx = context.WithValue(ctx, "userID", 42)
userID := ctx.Value("userID").(int)
Contextの使い方
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() // リクエストのコンテキスト
if err := dbQuery(ctx, ...); err != nil {
return
}
}
func dbQuery(ctx context.Context, query string) (Result, error) {
return db.QueryContext(ctx, query) // キャンセル可能
}
HTTP / DB / その他の長期処理は必ずContextを受ける。
Contextのアンチパターン
- structのフィールドに保存しない(引数で渡す)
- nil contextを渡さない(context.TODO() / Background())
- valueで渡すのは「リクエストスコープの軽い情報」のみ(認証情報など)
- 必須引数をvalueで渡さない
19-7. このセクションのまとめ
- goroutineは超軽量(2KBから)
- M:Nスケジューラ(GMPモデル)
- channel: unbuffered / buffered / closed
- channel方向 (chan<- / <-chan)
- パターン: worker pool / fan-out-in / pipeline
- selectでタイムアウト・キャンセル
- context.Contextで長期処理を制御
20. interface深掘り
20-1. interfaceの正体
type Animal interface {
Speak() string
}
これは「Speak() stringメソッドを持つ任意の型を受け入れる」型。Goでは 暗黙的に実装される。
ifaceの内部表現
interface値 = (型情報, データへのポインタ)
^^^ ^^^
type pointer data pointer
具体型 ──→ interfaceに代入時 ──→ (type, data) のペアにラップ
これが nil interface の罠の原因:
var p *MyType = nil
var i Animal = p
i == nil // false! 型情報が入っているため
20-2. small interfaces
Goの文化として 「メソッド1つのinterface」が好まれる:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Stringer interface {
String() string
}
type Closer interface {
Close() error
}
type Error interface {
Error() string
}
標準ライブラリは小さなinterfaceを組み合わせる:
type ReadWriter interface {
Reader
Writer
}
type ReadCloser interface {
Reader
Closer
}
これがGoの 「Compose, don’t inherit」哲学。
20-3. interface satisfication
// 型は明示的に「このinterfaceを実装する」と宣言しない
type MyReader struct {}
func (r *MyReader) Read(p []byte) (int, error) { ... }
// 自動的にio.Readerを実装したことになる
var r io.Reader = &MyReader{}
「書きやすい」反面、「意図せずimplementしてしまう」こともある。
コンパイル時の確認
var _ io.Reader = (*MyReader)(nil) // コンパイル時にReaderを実装しているか確認
これがあると、型を変えたときに 「暗黙的にimplementしなくなった」ことを早期検出できる。
20-4. type assertionとtype switch
var i interface{} = "hello"
s := i.(string) // パニックof failed
s, ok := i.(string) // 安全版
fmt.Println(s, ok)
// type switch
switch v := i.(type) {
case string:
fmt.Println("string:", v)
case int:
fmt.Println("int:", v)
case nil:
fmt.Println("nil")
default:
fmt.Printf("type %T\n", v)
}
20-5. interfaceのサイズ
type Empty interface{} // = any(Go 1.18+)
type Value interface {
Foo()
}
unsafe.Sizeof(Empty(nil)) // 16 byte (type pointer + data pointer)
unsafe.Sizeof(Value(nil)) // 16 byte
interface値は 常に2ワード。
20-6. このセクションのまとめ
- interface = (型情報, データ) のペア
- 暗黙的実装(implements不要)
- 小さなinterface文化(io.Reader等)
- 「使う側が定義する」依存方向
- type assertion / type switch
- nil interfaceの罠(型がnilでないとnil扱いされない)
21. errorハンドリング深掘り
21-1. errorの正体
type error interface {
Error() string
}
たった1メソッドのinterface。任意の型がError() stringを持てばerror。
21-2. カスタムerror型
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
// 使用
return &ValidationError{Field: "email", Message: "invalid format"}
// 受け取り側
var verr *ValidationError
if errors.As(err, &verr) {
fmt.Println(verr.Field)
}
21-3. sentinel error
var ErrNotFound = errors.New("not found")
func find(id int) (*User, error) {
if !exists(id) {
return nil, ErrNotFound
}
return users[id], nil
}
// 受け取り側
user, err := find(1)
if errors.Is(err, ErrNotFound) {
// 特定のerror
}
io.EOF、sql.ErrNoRows など標準ライブラリで多用。
21-4. error wrapping
func loadUser(id int) (*User, error) {
user, err := findUser(id)
if err != nil {
return nil, fmt.Errorf("loadUser(%d): %w", id, err)
}
return user, nil
}
// errors.Isでチェイン辿る
if errors.Is(err, sql.ErrNoRows) {
// 元のエラーまで遡って判定
}
%w で エラーチェインを作る。
21-5. errorのレベル
- panic: 回復不能なバグ(プログラム終了)
- error: 通常のエラー(呼び出し側が処理)
- log: 警告・情報
「panicはバグ、エラーは想定の失敗」という区別。
21-6. defer + panic + recover
func safeRun() {
defer func() {
if r := recover(); r != nil {
log.Printf("recovered: %v", r)
}
}()
panic("oops")
}
HTTPハンドラなどで「1リクエストのpanicでサーバを落とさない」用途。
21-7. errorsの便利関数
errors.New("msg")
errors.Is(err, target)
errors.As(err, &target)
errors.Unwrap(err)
errors.Join(err1, err2) // Go 1.20+
fmt.Errorf("...: %w", err)
21-8. このセクションのまとめ
- errorはError() string interface
- sentinel error (var ErrXxx = errors.New(...))
- error wrapping (%w + errors.Is/As)
- panic/recoverは最終手段
- errors.Joinで複数エラー(Go 1.20+)
22. structとmethods深掘り
22-1. embedding詳細
type Animal struct {
Name string
}
func (a Animal) Greet() string {
return "I am " + a.Name
}
type Dog struct {
Animal
Breed string
}
d := Dog{
Animal: Animal{Name: "Rex"},
Breed: "Labrador",
}
d.Name // "Rex"(埋め込みフィールドへ直接アクセス)
d.Greet() // "I am Rex"(メソッドも継承的に)
d.Animal.Greet() // 明示的にアクセスも可
同名メソッドのオーバーライド
func (d Dog) Greet() string { // Animal.Greet() を隠す
return "Woof! " + d.Animal.Greet()
}
d.Greet() // "Woof! I am Rex"
d.Animal.Greet() // "I am Rex"
22-2. interfaceの埋め込み
type ReadWriter interface {
io.Reader
io.Writer
}
複数のinterfaceを 合成できる。
22-3. struct tag
type User struct {
Name string `json:"name" db:"user_name"`
Email string `json:"email,omitempty" db:"email" validate:"required,email"`
}
タグはリフレクションで読まれる。JSON、DB、バリデーションなどのライブラリで活用。
import "reflect"
t := reflect.TypeOf(User{})
field, _ := t.FieldByName("Name")
tag := field.Tag.Get("json") // "name"
22-4. ポインタレシーバvs値レシーバの選び方
type Config struct {
debug bool
}
// 値レシーバ(読み取りのみ)
func (c Config) IsDebug() bool { return c.debug }
// ポインタレシーバ(変更)
func (c *Config) SetDebug(v bool) { c.debug = v }
// 大きいstruct
type LargeData struct { /* 多数のフィールド */ }
func (d *LargeData) Process() { ... } // ポインタでコピー回避
一貫性
// Bad: メソッド毎にレシーバ型がバラバラ
func (u User) Foo() { }
func (u *User) Bar() { } // 混在は混乱の元
// Good: 統一
func (u *User) Foo() { }
func (u *User) Bar() { }
22-5. このセクションのまとめ
- embeddingで「has-a」関係を「is-a」風に表現
- 同名メソッドで「オーバーライド」
- struct tagで メタデータ(JSON / DB / Validation)
- レシーバはstructごとに統一
- 大きいstructはポインタレシーバ
23. 標準ライブラリ詳細
Goの標準ライブラリは極めて充実。Webサーバ・JSON・暗号などサードパーティ依存最小限。
23-1. fmt(フォーマット)
fmt.Println("hello")
fmt.Printf("%d %s\n", 42, "answer")
fmt.Sprintf("x=%d", 10)
fmt.Errorf("error: %w", err)
// 動詞
%d整数
%f浮動小数点
%s文字列
%vデフォルトフォーマット
%+v structのフィールド名付き
%#v Go構文
%T型
%t bool
%q引用符付き文字列
%x 16進
%pポインタ
%w error wrap
23-2. io / bufio
// io.Reader / io.Writerが中核
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// 便利関数
data, err := io.ReadAll(reader)
n, err := io.Copy(dst, src)
io.WriteString(w, "hello")
// bufio
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
writer := bufio.NewWriter(file)
writer.WriteString("hello")
writer.Flush()
23-3. os / os/exec
// 環境変数
os.Getenv("HOME")
os.Setenv("FOO", "bar")
os.LookupEnv("FOO") // value, exists
// 引数
fmt.Println(os.Args)
// 終了
os.Exit(1)
// ファイル操作
file, err := os.Open("data.txt")
defer file.Close()
os.ReadFile("data.txt")
os.WriteFile("out.txt", data, 0644)
// プロセス
cmd := exec.Command("ls", "-la")
output, err := cmd.Output()
23-4. encoding/json
type User struct {
Name string `json:"name"`
Age int `json:"age,omitempty"`
}
// マーシャル
data, err := json.Marshal(user)
data, err := json.MarshalIndent(user, "", " ")
// アンマーシャル
var u User
err := json.Unmarshal(data, &u)
// stream
encoder := json.NewEncoder(w)
encoder.Encode(user)
decoder := json.NewDecoder(r)
decoder.Decode(&u)
23-5. net/http完全版
// クライアント
resp, err := http.Get("https://example.com")
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
// 詳細制御
client := &http.Client{Timeout: 10 * time.Second}
req, _ := http.NewRequest("POST", url, body)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
// サーバ
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello")
})
http.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
json.NewEncoder(w).Encode(map[string]string{"hello": "world"})
})
http.ListenAndServe(":8080", nil)
http.ServeMux(Go 1.22+)
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", getUser)
mux.HandleFunc("POST /users", createUser)
http.ListenAndServe(":8080", mux)
{id} でパスパラメータ抽出。r.PathValue("id") で取得。Go 1.22で大幅強化。
23-6. log / log/slog
// 古いlog
log.Println("hi")
log.Fatalf("error: %v", err) // Print + os.Exit(1)
log.Panicln("oops") // Print + panic
// 構造化ログ(Go 1.21+)
import "log/slog"
slog.Info("user signed in", "user_id", 1, "ip", "1.2.3.4")
slog.Error("db error", "err", err, "query", q)
// JSONハンドラ
h := slog.NewJSONHandler(os.Stdout, nil)
slog.SetDefault(slog.New(h))
// LogValuerインターフェース
type User struct { Name, Email string }
func (u User) LogValue() slog.Value {
return slog.GroupValue(
slog.String("name", u.Name),
slog.String("email", u.Email),
)
}
slog で本番品質の構造化ロギングが標準。
23-7. このセクションのまとめ
- fmtで型安全フォーマット
- io.Reader/Writerが抽象化の中核
- net/httpで本格Webサーバ
- encoding/jsonでJSON
- log/slog(1.21+)で構造化ログ
- ServeMux(1.22+)でrouting強化
24. Generics完全版
24-1. 型パラメータの使い所
// before: interface{} を使う
func Map(items []interface{}, f func(interface{}) interface{}) []interface{} {
...
}
// after: generic
func Map[T, U any](items []T, f func(T) U) []U {
result := make([]U, len(items))
for i, item := range items {
result[i] = f(item)
}
return result
}
doubled := Map([]int{1, 2, 3}, func(x int) int { return x * 2 })
24-2. 型制約(type set)
type Number interface {
int | int32 | int64 | float32 | float64
}
func Sum[T Number](items []T) T {
var total T
for _, x := range items {
total += x
}
return total
}
// 標準: cmp.Ordered(Go 1.21+)
func Min[T cmp.Ordered](a, b T) T {
if a < b { return a }
return b
}
24-3. メソッドを持つ型制約
type Stringer interface {
String() string
}
func Print[T Stringer](items []T) {
for _, x := range items {
fmt.Println(x.String())
}
}
24-4. ジェネリック型
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) {
s.items = append(s.items, v)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
n := len(s.items) - 1
v := s.items[n]
s.items = s.items[:n]
return v, true
}
s := Stack[int]{}
s.Push(1)
v, ok := s.Pop()
24-5. 制約とインスタンス化
type Pair[K comparable, V any] struct {
Key K
Value V
}
func (p Pair[K, V]) String() string {
return fmt.Sprintf("%v=%v", p.Key, p.Value)
}
p := Pair[string, int]{Key: "age", Value: 30}
24-6. このセクションのまとめ
- T anyでなんでも、cmp.Orderedで比較可能
- type Foo interface { int | string } でunion制約
- メソッドを持つ制約も可能
- ジェネリック型 (type Stack[T any] struct)
- 「まずinterface、必要ならgenerics」
25. テストとプロファイル深掘り
25-1. testing基本
package math_test
import (
"math"
"testing"
)
func TestAdd(t *testing.T) {
if got := math.Add(1, 2); got != 3 {
t.Errorf("Add(1, 2) = %d, want 3", got)
}
}
// テーブルテスト
func TestAddTable(t *testing.T) {
cases := []struct {
name string
a, b int
expected int
}{
{"positive", 1, 2, 3},
{"negative", -1, -1, -2},
{"zero", 0, 0, 0},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := math.Add(tc.a, tc.b); got != tc.expected {
t.Errorf("Add(%d, %d) = %d, want %d", tc.a, tc.b, got, tc.expected)
}
})
}
}
go test # 全テスト
go test -v # 詳細
go test -run TestAdd # 名前マッチ
go test -cover # カバレッジ
go test -race # データレース検出
go test -bench=. # ベンチマーク
go test -count=10 # 繰り返し
25-2. ベンチマーク
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}
func BenchmarkAddParallel(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Add(1, 2)
}
})
}
// メモリプロファイル
func BenchmarkAlloc(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = make([]int, 100)
}
}
go test -bench=. -benchmem
# BenchmarkAdd-8 1000000000 0.5 ns/op 0 B/op 0 allocs/op
25-3. fuzzテスト(Go 1.18+)
func FuzzReverse(f *testing.F) {
f.Add("hello") // seed
f.Fuzz(func(t *testing.T, s string) {
rev := Reverse(s)
rev2 := Reverse(rev)
if s != rev2 {
t.Errorf("Reverse twice: %q != %q", s, rev2)
}
})
}
go test -fuzz=FuzzReverse
ランダム入力で性質を検証。エッジケースを自動発見。
25-4. mockパッケージ
Goではinterface経由で簡単にモック可能。
type Repo interface {
Find(id int) (*User, error)
}
type MockRepo struct {
FindFunc func(int) (*User, error)
}
func (m *MockRepo) Find(id int) (*User, error) {
return m.FindFunc(id)
}
func TestService(t *testing.T) {
mock := &MockRepo{
FindFunc: func(id int) (*User, error) {
return &User{Name: "Alice"}, nil
},
}
service := NewService(mock)
user, _ := service.GetUser(1)
if user.Name != "Alice" {
t.Errorf("got %s", user.Name)
}
}
gomock、testify/mock でツール化されたモックも可。
25-5. testify
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAdd(t *testing.T) {
assert.Equal(t, 3, Add(1, 2))
require.NoError(t, err) // 失敗時に即終了
assert.NotNil(t, user)
assert.Contains(t, list, "foo")
}
xUnit風のアサーション。Go標準testingより読みやすいので人気。
25-6. pprofプロファイル
import _ "net/http/pprof"
go http.ListenAndServe("localhost:6060", nil)
// CPUプロファイル
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
// メモリプロファイル
go tool pprof http://localhost:6060/debug/pprof/heap
// goroutineプロファイル
go tool pprof http://localhost:6060/debug/pprof/goroutine
(pprof) top10
(pprof) list FunctionName
(pprof) web # ブラウザで可視化
25-7. PGO(Profile-Guided Optimization、Go 1.21+)
# プロファイル収集
go test -cpuprofile=cpu.prof -bench=.
# または本番から
curl -o cpu.prof "http://server:6060/debug/pprof/profile?seconds=60"
# プロファイルでビルド
go build -pgo=cpu.prof
実行プロファイルをコンパイラにフィードバック。実測5〜10% 高速化が報告されている。
25-8. このセクションのまとめ
- testingでTestXxx / BenchmarkXxx
- テーブルテスト + t.Runでサブテスト
- -raceでデータレース検出
- fuzzテスト(1.18+)
- testifyでアサーション充実
- pprofでプロファイル
- PGO(1.21+)で本番最適化
26. デプロイとビルド
26-1. クロスコンパイル
GOOS=linux GOARCH=amd64 go build -o myapp-linux
GOOS=darwin GOARCH=arm64 go build -o myapp-mac
GOOS=windows GOARCH=amd64 go build -o myapp.exe
GOOS × GOARCH の組み合わせは go tool dist list で確認。1コマンドで全OSのバイナリを作れるのがGoの魅力。
26-2. ビルドフラグ
go build -ldflags="-s -w" -o app # シンボル削除でバイナリ小型化
go build -ldflags="-X main.version=$(git describe --tags)" -o app
go build -tags="integration" ./...
go build -trimpath ./... # ビルドパスを削る(再現性)
26-3. Docker
# multi-stage build
FROM golang:1.22 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /out/app
FROM gcr.io/distroless/static-debian12
COPY --from=build /out/app /app
ENTRYPOINT ["/app"]
distroless で 数MBの最小イメージ。CGO_ENABLED=0で完全に静的リンク。
26-4. 環境変数と設定
type Config struct {
Host string `env:"HOST" envDefault:"localhost"`
Port int `env:"PORT" envDefault:"8080"`
DbURL string `env:"DATABASE_URL,required"`
}
import "github.com/caarlos0/env/v10"
cfg := Config{}
if err := env.Parse(&cfg); err != nil {
log.Fatal(err)
}
12-Factor準拠。
26-5. Graceful shutdown
srv := &http.Server{Addr: ":8080", Handler: mux}
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}()
// シグナル待ち
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
<-sig
// 30秒猶予
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
srv.Shutdown(ctx)
log.Println("shutdown complete")
Kubernetes・systemdで重要なパターン。
26-6. このセクションのまとめ
- クロスコンパイル: GOOS / GOARCH
- ldflagsでメタデータ・最小化
- Docker multi-stage + distroless
- 環境変数で設定(12-Factor)
- graceful shutdownでゼロダウンタイム
27. Webフレームワーク詳細
27-1. 標準net/http vsフレームワーク
標準net/http(推奨):
- 十分な機能
- 学習コストなし
- サードパーティ依存なし
- Go 1.22+ でrouting強化
軽量フレームワーク:
chi: 標準互換、middleware豊富
gorilla/mux: 老舗、メンテ縮小
重量フレームワーク:
gin: 最も人気
echo: ginの代替
fiber: fasthttpベース、超高速
beego: フルスタック
27-2. chi(推奨の標準互換)
import "github.com/go-chi/chi/v5"
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Get("/users", listUsers)
r.Route("/users/{id}", func(r chi.Router) {
r.Get("/", getUser)
r.Put("/", updateUser)
r.Delete("/", deleteUser)
})
http.ListenAndServe(":8080", r)
http.Handler 互換で、標準ライブラリと密に統合。
27-3. gin
import "github.com/gin-gonic/gin"
r := gin.Default()
r.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
c.JSON(http.StatusOK, gin.H{"id": id})
})
r.Run(":8080")
最も人気。標準より速いベンチマーク。
27-4. echo
import "github.com/labstack/echo/v4"
e := echo.New()
e.GET("/users/:id", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"id": c.Param("id"),
})
})
e.Logger.Fatal(e.Start(":8080"))
ginと類似。好みで選ぶ。
27-5. このセクションのまとめ
- 標準net/httpで十分(特にGo 1.22+)
- chi: 軽量、標準互換
- gin / echo: 機能豊富、speed
- フレームワークの選択は重要だが、ロックインも考慮
28. データベース連携
28-1. database/sql
import "database/sql"
import _ "github.com/lib/pq" // 副作用import
db, err := sql.Open("postgres", "postgres://user:pass@localhost/dbname?sslmode=disable")
defer db.Close()
// 単一行
var name string
err := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id = $1", 1).Scan(&name)
// 複数行
rows, err := db.QueryContext(ctx, "SELECT id, name FROM users WHERE age >= $1", 18)
defer rows.Close()
for rows.Next() {
var id int
var name string
rows.Scan(&id, &name)
fmt.Println(id, name)
}
err = rows.Err()
// 更新
result, err := db.ExecContext(ctx, "UPDATE users SET name = $1 WHERE id = $2", "Alice", 1)
n, _ := result.RowsAffected()
Context 引数の Context系メソッドを必ず使う(タイムアウト・キャンセル可能)。
28-2. sqlx(拡張)
import "github.com/jmoiron/sqlx"
db, err := sqlx.Open("postgres", url)
type User struct {
ID int `db:"id"`
Name string `db:"name"`
}
var users []User
db.Select(&users, "SELECT * FROM users WHERE age >= $1", 18)
var user User
db.Get(&user, "SELECT * FROM users WHERE id = $1", 1)
structへの自動マッピングが便利。標準database/sqlの薄いラッパー。
28-3. sqlc(コード生成)
-- queries.sql
-- name: GetUser :one
SELECT * FROM users WHERE id = $1;
-- name: ListUsers :many
SELECT * FROM users WHERE age >= $1 ORDER BY name;
sqlc generate
から 型安全なGoコードを自動生成。SQLを書きつつ型安全を実現。
28-4. ORM系
GORM: 最も人気、ActiveRecord風
ent: Facebook製、コード生成、型安全
beego/orm: フルスタックフレームワーク内蔵
GORM例
import "gorm.io/gorm"
type User struct {
gorm.Model
Name string
Email string `gorm:"unique"`
}
db.Create(&user)
db.First(&user, 1)
db.Where("age >= ?", 18).Find(&users)
db.Save(&user)
db.Delete(&user)
ActiveRecord風。Goコミュニティの賛否は分かれる。
28-5. このセクションのまとめ
- database/sql標準で十分
- Contextメソッド必須
- sqlx / sqlcで楽に
- ORM(GORM / ent)は好み次第
29. CLIツール構築
29-1. flagパッケージ
import "flag"
var (
name = flag.String("name", "world", "name to greet")
verbose = flag.Bool("v", false, "verbose")
port = flag.Int("port", 8080, "server port")
)
func main() {
flag.Parse()
fmt.Printf("hello, %s\n", *name)
}
./app -name Alice -v -port 9000
29-2. cobra
import "github.com/spf13/cobra"
var rootCmd = &cobra.Command{
Use: "app",
Short: "My app",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("hello")
},
}
var greetCmd = &cobra.Command{
Use: "greet [name]",
Short: "Greet someone",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("hello, %s\n", args[0])
},
}
func main() {
rootCmd.AddCommand(greetCmd)
rootCmd.Execute()
}
docker、kubectl、hugo、gh などが採用する事実上の標準。サブコマンド・補完・ヘルプ自動生成。
29-3. このセクションのまとめ
- 標準flagでシンプルCLI
- cobraでサブコマンド (docker / kubectl流)
- urfave/cliも人気
- viperで設定ファイル
30. ロギングと観測
30-1. log/slog(Go 1.21+)
import "log/slog"
slog.Info("user signed in", "user", user.Name, "ip", req.RemoteAddr)
// ハンドラ設定
h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
AddSource: true, // ファイル名・行番号
})
slog.SetDefault(slog.New(h))
30-2. zerolog(高速)
import "github.com/rs/zerolog/log"
log.Info().Str("user", "alice").Int("age", 30).Msg("hello")
log.Error().Err(err).Msg("failed")
最速のロガー。アロケーションゼロを目指す設計。
30-3. zap(高速)
import "go.uber.org/zap"
logger, _ := zap.NewProduction()
defer logger.Sync()
logger.Info("hello", zap.String("user", "alice"), zap.Int("age", 30))
Uber製。ベンチマーク上位。
30-4. OpenTelemetry
import "go.opentelemetry.io/otel"
import "go.opentelemetry.io/otel/trace"
tracer := otel.Tracer("my-service")
ctx, span := tracer.Start(ctx, "operation")
defer span.End()
span.SetAttributes(attribute.String("user.id", userID))
分散トレーシング・メトリクスの業界標準。Datadog / Jaeger / Honeycombと連携。
30-5. このセクションのまとめ
- log/slog(1.21+)が標準
- zerolog / zapで更に高速
- OpenTelemetryで分散観測
- 構造化ロギングが基本
31. パフォーマンスチューニング
31-1. ベンチマークから始める
func BenchmarkParse(b *testing.B) {
input := generateLargeInput()
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = Parse(input)
}
}
go test -bench=. -benchmem -benchtime=10s
31-2. プロファイル分析
import _ "net/http/pprof"
go http.ListenAndServe("localhost:6060", nil)
// 30秒のCPUプロファイル
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
(pprof) top10 // 上位10関数
(pprof) list FunctionName // ソース表示
(pprof) tree // コールツリー
(pprof) web // ブラウザで可視化
31-3. メモリ最適化
- sync.Poolで短命オブジェクトを再利用
- []byte / string変換でアロケート
- map / sliceの容量事前確保
- structのフィールド順序(パディング)
- ポインタより値を返す(インライン化)
var bufPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func process() {
buf := bufPool.Get().(*bytes.Buffer)
defer bufPool.Put(buf)
buf.Reset()
// 使う
}
31-4. CPU最適化
- アロケーション削減(GC圧減)
- インライン化(小さい関数)
- バッファサイズの調整
- escape analysisの確認(go build -gcflags="-m")
- assembly出力確認(go tool compile -S)
31-5. このセクションのまとめ
- ベンチマークとpprof
- sync.Poolで再利用
- アロケーション削減が最重要
- escape analysisの理解
32. パッケージ設計
32-1. レイアウト規約(Standard Go Project Layout)
myproject/
├── cmd/
│ ├── server/
│ │ └── main.go
│ └── cli/
│ └── main.go
├── internal/ # 同モジュール内のみ
│ ├── domain/
│ ├── service/
│ └── repository/
├── pkg/ # 外部公開可
│ └── lib/
├── api/ # OpenAPI / proto定義
├── web/ # 静的アセット
├── configs/
├── scripts/
├── test/
├── go.mod
└── go.sum
internal/ は 同じモジュール内からしかimportできない。実装隠蔽の標準。
32-2. パッケージ命名
- 短く、意味が分かる
- 単数形(user、book)
- _ やcamelCaseを使わない(小文字のみ)
- util、common、helpersのような名前は避ける
32-3. インターフェースの定義場所
「使う側」が定義する。
// repository/user.go
type Repository interface {
Find(id int) (*User, error)
}
// service/user.go
type UserService struct {
repo repository.Repository
}
提供側ではなく利用側がinterfaceを持つことで、依存方向を制御。
32-4. このセクションのまとめ
- cmd/ にエントリポイント
- internal/ で隠蔽
- pkg/ で外部公開
- 短い、意味のあるパッケージ名
- interfaceは利用側で定義
33. Goプログラマの哲学
33-1. The Zen of Go
- 各パッケージは単一の目的
- 早期リターンで分岐を減らす
- 並行は重要だが並列ではない
- channelでメモリ共有を避ける、メモリ共有でchannelしない
- 大きなinterfaceより小さなinterface
- importを最小限に
- 規約はあなたを守るためにある
- ドキュメントは書く
- どんなプログラムも完璧でない
- ハッキーな解決策よりも明確で単純なコード
Dave CheneyによるGoの哲学集約。
33-2. errors are values
「例外ではなく、エラーは値」。Goの最大の哲学のひとつ。
result, err := operation()
if err != nil {
return err
}
煩雑だが、失敗ケースを必ず明示する規律を生む。
33-3. Don’t communicate by sharing memory; share memory by communicating
「メモリを共有して通信するのではなく、通信してメモリを共有する」。channelの哲学。
33-4. このセクションのまとめ
- 単純さの追求
- エラーは値
- channelで通信、共有メモリは最小限
- 小さなinterface
- ドキュメントの規律
34. Goの現実的な弱点
34-1. 詳細
- ジェネリクスが遅れて入った(1.18、2022)
- enumがない(const + iotaで代替)
- Sum type(タグ付き共用体)がない
- パッケージ管理の歴史的混乱(GOPATH → modules)
- エラーハンドリングが冗長
- メタプログラミング能力低い
- インライン化の制限
- GUI弱い
- 機械学習・データサイエンス弱い
これらを踏まえてもGoの 生産性とシンプルさは他言語に勝る場面が多い。
34-2. Go 1.22の重要変更(再掲)
// forループ変数のスコープ変更
for i, v := range items {
go func() {
fmt.Println(i, v) // Go 1.21以前: 全部最後の値
// Go 1.22+: 各反復で新しい値
}()
}
長年バグの温床だった挙動を 言語レベルで修正。Goの互換性ポリシーを部分的に破ったが、必要な変更だった。
応用: サーバ開発と運用
36. 大規模プロジェクト構成
myproject/
├── cmd/
│ ├── server/
│ │ └── main.go
│ └── cli/
│ └── main.go
├── internal/
│ ├── domain/
│ │ └── user.go
│ ├── usecase/
│ │ └── user_service.go
│ ├── infrastructure/
│ │ ├── persistence/
│ │ │ └── postgres.go
│ │ └── http/
│ │ └── handler.go
│ └── pkg/ # 内部utility
├── api/
│ └── openapi.yaml
├── pkg/
│ └── publiclib/
├── deployments/
│ ├── docker/
│ └── k8s/
├── scripts/
├── test/
├── go.mod
└── README.md
Clean Architecture / DDD風の分離。Goでは cmd/、internal/、pkg/ が事実上の標準ディレクトリ名。
37. Clean Architectureの実装
// domain/user.go
type User struct {
ID UserID
Name string
Email Email
}
type UserRepository interface {
Find(ctx context.Context, id UserID) (*User, error)
Save(ctx context.Context, user *User) error
}
// usecase/user_service.go
type UserService struct {
repo UserRepository
mailer Mailer
}
func (s *UserService) Register(ctx context.Context, name, email string) (*User, error) {
e, err := NewEmail(email)
if err != nil {
return nil, err
}
user := &User{Name: name, Email: e}
if err := s.repo.Save(ctx, user); err != nil {
return nil, err
}
s.mailer.SendWelcome(ctx, user)
return user, nil
}
// infrastructure/persistence/postgres.go
type PostgresUserRepo struct {
db *sql.DB
}
func (r *PostgresUserRepo) Find(ctx context.Context, id UserID) (*User, error) {
var u User
err := r.db.QueryRowContext(ctx, "SELECT id, name, email FROM users WHERE id = $1", id).
Scan(&u.ID, &u.Name, &u.Email)
return &u, err
}
// infrastructure/http/handler.go
type UserHandler struct {
service *UserService
}
func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) {
var req struct{ Name, Email string }
json.NewDecoder(r.Body).Decode(&req)
user, err := h.service.Register(r.Context(), req.Name, req.Email)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
json.NewEncoder(w).Encode(user)
}
依存方向: infrastructure → usecase → domain(外側から内側へ)。
38. gRPCサーバ
// proto/user.proto
syntax = "proto3";
package user;
service UserService {
rpc GetUser(GetUserRequest) returns (User);
rpc CreateUser(CreateUserRequest) returns (User);
}
message User {
int64 id = 1;
string name = 2;
string email = 3;
}
message GetUserRequest { int64 id = 1; }
message CreateUserRequest { string name = 1; string email = 2; }
protoc --go_out=. --go-grpc_out=. proto/user.proto
type server struct {
user.UnimplementedUserServiceServer
repo UserRepository
}
func (s *server) GetUser(ctx context.Context, req *user.GetUserRequest) (*user.User, error) {
u, err := s.repo.Find(ctx, UserID(req.Id))
if err != nil {
return nil, status.Error(codes.NotFound, "not found")
}
return &user.User{Id: int64(u.ID), Name: u.Name, Email: string(u.Email)}, nil
}
// 起動
lis, _ := net.Listen("tcp", ":50051")
s := grpc.NewServer()
user.RegisterUserServiceServer(s, &server{...})
s.Serve(lis)
GoogleがgRPCを使い倒している。Kubernetes、etcdのAPIもgRPCベース。
39. セキュリティ
39-1. SQLインジェクション
// Bad
db.Exec("SELECT * FROM users WHERE id = " + userInput)
// Good: prepared statement
db.Exec("SELECT * FROM users WHERE id = ?", userInput)
database/sql は自動でescape。
39-2. パスワードハッシュ
import "golang.org/x/crypto/bcrypt"
hash, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
err := bcrypt.CompareHashAndPassword(hash, []byte(password))
// argon2がよりモダン
import "golang.org/x/crypto/argon2"
key := argon2.IDKey([]byte(password), salt, 1, 64*1024, 4, 32)
39-3. CSRF / XSS
html/template は自動エスケープ:
tmpl, _ := template.New("page").Parse("<h1>{{.Name}}</h1>")
tmpl.Execute(w, struct{ Name string }{Name: "<script>"})
// 出力: <h1><script></h1>
CSRFはmiddlewareで:
import "github.com/gorilla/csrf"
csrfMiddleware := csrf.Protect([]byte("32-byte-long-auth-key"))
http.ListenAndServe(":8080", csrfMiddleware(router))
39-4. JWT認証
import "github.com/golang-jwt/jwt/v5"
claims := jwt.MapClaims{
"user_id": 1,
"exp": time.Now().Add(time.Hour).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signed, _ := token.SignedString([]byte(secret))
// 検証
parsed, err := jwt.Parse(signed, func(t *jwt.Token) (interface{}, error) {
return []byte(secret), nil
})
40. 周辺ツール
40-1. golangci-lint
golangci-lint run
複数のリンターを統合。CIで必須。
40-2. air(hot reload)
go install github.com/air-verse/air@latest
air
ファイル変更で自動再起動。開発時の必需品。
40-3. delve(デバッガ)
go install github.com/go-delve/delve/cmd/dlv@latest
dlv debug ./cmd/server
(dlv) break main.handler
(dlv) continue
(dlv) print req
VSCode / GoLandからGUIでも操作可能。
40-4. govulncheck
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
依存ライブラリの脆弱性を検出。CI必須。
41. Goの現代的な書き方ガイド
// ✅ Good
func ReadConfig(ctx context.Context, path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("ReadConfig: %w", err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("ReadConfig: parse: %w", err)
}
return &cfg, nil
}
// 規律:
// 1. contextは第一引数
// 2. errorは最後の戻り値
// 3. fmt.Errorf %wでチェイン
// 4. 早期リターン
// 5. deferでリソース解放
// 6. インターフェースは利用側で定義
// 7. structは意味を持たせる
// 8. exported名前は大文字始まり
// 9. パッケージ名は短く
// 10. テストを書く
42. Go実務の補足知識
42-1. embed(Go 1.16+)
import _ "embed"
//go:embed data.txt
var data string
//go:embed templates/*
var templates embed.FS
ファイルをバイナリに埋め込める。外部ファイルなしの単一バイナリ配布が可能。
42-2. atomic.Value
var v atomic.Value
v.Store(map[string]int{"a": 1})
m := v.Load().(map[string]int)
ロックなしで値を共有。
42-3. errgroup
import "golang.org/x/sync/errgroup"
g, ctx := errgroup.WithContext(ctx)
for _, url := range urls {
url := url
g.Go(func() error {
return fetch(ctx, url)
})
}
if err := g.Wait(); err != nil {
return err
}
「複数のgoroutineを並行して、最初のerrorで全部キャンセル」。実用上頻出。
42-4. sync.Map
var m sync.Map
m.Store("key", value)
v, ok := m.Load("key")
m.Delete("key")
m.Range(func(k, v interface{}) bool { return true })
並行安全なmap。map + Mutex より速い場面がある。
42-5. iotaの応用
const (
KB = 1 << (10 * (iota + 1)) // 1024
MB
GB
TB
)
type Day int
const (
Sunday Day = iota
Monday
// ...
)
43. Goコミュニティ
カンファレンス
- GopherCon
- GoLab
- Go Conference (Japan)
Web
- go.dev(公式)
- pkg.go.dev(パッケージドキュメント)
- /r/golang
- Gopher Slack
書籍
- 『The Go Programming Language』(K&R風の名著)
- 『Go in Action』
- 『Concurrency in Go』
- 『100 Go Mistakes』Teiva Harsanyi
学習サイト
- Tour of Go
- Go by Example
- Effective Go
- Gophercises
44. Goの哲学を象徴するコード
package main
import (
"context"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "ok")
})
mux.HandleFunc("GET /users/{id}", getUser)
srv := &http.Server{
Addr: ":8080",
Handler: mux,
}
go func() {
slog.Info("server started", "addr", srv.Addr)
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
slog.Error("server failed", "err", err)
os.Exit(1)
}
}()
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
slog.Info("shutting down")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
slog.Error("shutdown error", "err", err)
}
slog.Info("bye")
}
func getUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, `{"id": "%s"}`, id)
}
これだけで:
外部依存ゼロで本番品質のサービスが書ける。これがGoの実力です。
45. Goの基本の整理
Goは 「クラウド時代のC」。シンプル、高速、並行、移植可能、配布が楽 ─ クラウドネイティブのすべてを満たす設計です。
Kubernetes、Docker、Terraform、Prometheus、etcd、CockroachDB、Caddy、Hugo、Drone、Gitea ─ あなたが今日触れたインフラの大半がGoで書かれています。
新規プロジェクトでの推奨:
- Go 1.22以上
- 標準ライブラリ中心
- 外部依存最小限
- context.Contextを伝播
- 構造化ロギング(slog)
- golangci-lint / govulncheckをCIに
業務での価値:
- 学習コスト最小(数日で生産的に)
- チーム開発に強い(gofmt規律)
- クロスコンパイル
- 単一バイナリ配布
- クラウドネイティブ標準
Goの価値は、少ない構文、標準化された整形、明示的なエラー処理、軽量な並行処理を組み合わせ、チームで読みやすいサービスを作りやすい点にあります。
Go の実践的な設計パターン
Error Handling の進化
従来の error interface
err := someFunction()
if err != nil {
return fmt.Errorf("failed: %w", err)
}
Go 1.13+ wrapping
var myErr MyError
if errors.As(err, &myErr) {
// 特定のエラータイプへ
}
// または errors.Is
if errors.Is(err, io.EOF) {
// EOF チェック
}
sentinel errors の代わりに errors.Is() を使う
// 古いスタイル(非推奨)
var ErrNotFound = errors.New("not found")
if err == ErrNotFound { }
// 新しいスタイル
type NotFoundError struct {
Resource string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s not found", e.Resource)
}
// どこでも使える
if errors.Is(err, &NotFoundError{}) { }
Context の適切な使用
// Timeout付きコンテキスト
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// キャンセル伝播
if err := someBlockingOperation(ctx); err != nil {
// ctx がキャンセルされて中断された
}
// マルチプロセッシング例
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
for {
select {
case <-ctx.Done():
return // ctx cancelled
default:
// work
}
}
}()
Interface{} とジェネリクス(Go 1.18+)
// Before: interface{}
func Print(vals ...interface{}) {
for _, v := range vals {
fmt.Println(v)
}
}
// After: generics
func Print[T any](vals ...T) {
for _, v := range vals {
fmt.Println(v)
}
}
// Constraint
func Sum[T constraints.Integer](vals ...T) T {
var result T
for _, v := range vals {
result += v
}
return result
}
Concurrency パターン
Worker Pool
const NumWorkers = 4
func WorkerPool(jobs <-chan Job, results chan<- Result) {
var wg sync.WaitGroup
for i := 0; i < NumWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
results <- process(job)
}
}()
}
go func() {
wg.Wait()
close(results)
}()
}
Fan-out/Fan-in
func fanOut(jobs <-chan Job) []<-chan Result {
channels := make([]<-chan Result, NumWorkers)
for i := 0; i < NumWorkers; i++ {
out := make(chan Result)
go func() {
for job := range jobs {
out <- process(job)
}
close(out)
}()
channels[i] = out
}
return channels
}
func fanIn(cs ...<-chan Result) <-chan Result {
var wg sync.WaitGroup
out := make(chan Result)
for _, c := range cs {
wg.Add(1)
go func(ch <-chan Result) {
for result := range ch {
out <- result
}
wg.Done()
}(c)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
Interface Design
// Good: 小さくて具体的
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// 大きなinterface は分割
type ReadWriter interface {
Reader
Writer
}
// 悪い例:大きすぎる
type Storage interface {
Create(...) error
Read(...) error
Update(...) error
Delete(...) error
List(...) error
// ... many more
}
Dependency Injection
type Database interface {
Query(ctx context.Context, sql string) (Rows, error)
}
type Service struct {
db Database
}
func NewService(db Database) *Service {
return &Service{db: db}
}
// テスト時は mock を inject
type MockDB struct{}
func (m *MockDB) Query(ctx context.Context, sql string) (Rows, error) {
// test implementation
}
service := NewService(&MockDB{})
Performance Considerations
Allocations
// 悪い:毎回 allocate
func AppendString(items []string, new string) []string {
return append(items, new)
}
// 改善:pre-allocate
func AppendStrings(items []string, new ...string) []string {
items = make([]string, 0, len(items)+len(new))
items = append(items, items...)
items = append(items, new...)
return items
}
Benchmark
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
result := add(1, 2)
_ = result
}
}
// 実行:go test -bench=. -benchmem
// Output: BenchmarkAdd-8 1000000000 0.5 ns/op 0 B/op 0 allocs/op
Profiling
import _ "net/http/pprof"
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// ブラウザで http://localhost:6060/debug/pprof にアクセス
// CPU profile: go tool pprof http://localhost:6060/debug/pprof/profile
// Heap: go tool pprof http://localhost:6060/debug/pprof/heap
Testing の充実
// Table-driven tests
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive", 1, 2, 3},
{"negative", -1, -2, -3},
{"zero", 0, 0, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := add(tt.a, tt.b)
if got != tt.expected {
t.Errorf("add(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.expected)
}
})
}
}
// Subtests で hierarchy
func TestComplex(t *testing.T) {
t.Run("setup", func(t *testing.T) { /* ... */ })
t.Run("main", func(t *testing.T) { /* ... */ })
t.Run("teardown", func(t *testing.T) { /* ... */ })
}
Modules と Versioning
// go.mod
module github.com/user/project
go 1.21
require (
github.com/lib/pq v1.10.0
golang.org/x/crypto v0.0.0-20210...
)
// Require minimal version
require github.com/some/lib >= v1.2.0
// Exclude problematic version
exclude github.com/bad/lib v1.0.0
// Replace for local development
replace github.com/user/project => ../local/path
Build Constraints
//go:build linux && amd64
// または
// +build linux,amd64
package main
import "fmt"
func platFunc() {
fmt.Println("linux amd64 specific")
}
まとめ
Goは、単純な構文、標準ライブラリ、明示的なエラー処理、goroutineとchannelを中心に、読みやすく運用しやすいサーバーサイドソフトウェアを作るための言語です。抽象を増やしすぎず、context、テスト、ログ、依存管理を一貫して扱うことが、Goらしい設計につながります。