Go

目次

主要項目のみを表示しています。詳細な小見出しは本文内で確認できます。

概要

まず、この章の中心構造を図で確認します。細部に入る前に、どの概念がどこへつながるかをつかむための地図です。

flowchart LR A["Goコード"] --> B["シンプルな型と構文"] B --> C["goroutine"] B --> D["interface"] C --> E["channel"] D --> F["小さな抽象"] E --> G["並行処理"] F --> H["保守しやすい設計"] G --> I["サーバー・CLI・分散システム"] H --> I
コード例の読み方

コード例は、そのまま写すためだけのものではありません。直前の本文で「何を確かめる例か」を押さえ、直後の説明で「どの性質が見えるか」を確認してください。実務では、ここに入力の境界、失敗時の挙動、依存する実行環境を足して読むと判断しやすくなります。

要点

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 GriesemerRob PikeKen 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

GOOSGOARCH を指定するだけで、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の制御フローは シンプルifforswitch の3つで、whiledo-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の switchbreak 不要(自動的に抜ける)。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が容量を超えた場合、新しい配列にコピーしてから追加。これにより ss2 が異なる配列を指すこともあれば、同じ配列を共有することも(容量に余裕があれば共有のまま)。

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

anyGo 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で返す

recoverHTTPサーバなどで「リクエスト処理がパニックしてもサーバを落とさない」用途で使われます。


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の特徴

  • 超軽量: 起動時 約8KBのスタック(OSスレッドは数MB)
  • ランタイムがM:Nでスケジュール(OSスレッド数より多くのgoroutineを動かせる)
  • 数百万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.1810年越しに導入。シンプルさを保ちつつ型安全な汎用コードが書けるようになりました。


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.WriterGoの最重要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(基礎)

  • Day 1-2: 環境構築、Hello World、goツール
  • Day 3-4: 型・変数・制御フロー
  • Day 5-6: slice / map / struct
  • Day 7: 簡単なCLIツール

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.EOFsql.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)
    }
}

gomocktestify/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()
}

dockerkubectlhugogh などが採用する事実上の標準。サブコマンド・補完・ヘルプ自動生成。


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>&lt;script&gt;</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 })

並行安全なmapmap + 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)
}

これだけで:

  • 標準ライブラリのみでHTTPサーバ
  • ヘルスチェックエンドポイント
  • パスパラメータ抽出(Go 1.22+)
  • 構造化ロギング
  • graceful shutdown

外部依存ゼロで本番品質のサービスが書ける。これが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らしい設計につながります。

参考文献

公式・標準