- Published on
Go 完全ガイド — Goroutine・Channel・Context・実戦マイクロサービスまで (Season 2 Ep 3, 2025)
- Authors

- Name
- Youngju Kim
- @fjvbn20031
はじめに — なぜGoは今も使われるのか
Rustが「学習は難しいが強力」だとすれば、Goは「学びやすく、すぐ本番投入できる」側だ。
2024〜2025のGoの位置:
- Kubernetes: クラウドネイティブの中心。Go製
- Docker: コンテナ生態系の原点
- Terraform: IaCの標準
- Prometheus / Grafana: Observabilityの標準
- CockroachDB / InfluxDB / etcd: 分散DB
- gRPC: GoogleのRPC標準、Goファースト
- Cloudflare, Uber, Monzo, Square: マイクロサービス主力言語
- Go 1.23 (2024/08):
range over funcイテレータ、改善されたtimer - Go 1.24 (2025/02): Generic Type Alias、tool directive
Goは「Kubernetes世界の共通語」。DevOps・SRE・Cloud Nativeエンジニアには必須。
第1部 — Goの哲学: Less is Moreの真意
1.1 Goが意図的に外したもの
- Generics (1.18まで無し) — 意図的に遅く導入
- 三項演算子 —
if-elseに統一 - 例外(try/catch) — Error as value
- 継承 — Composition only
- Macro — コード生成ツールで代替
- 関数オーバーロード — 名前を変える
Rob Pike曰く: 「GoはJavaが多すぎると感じる人のための言語だ。」
1.2 設計原則5つ
- Simplicity: 仕様書がポケットに収まる
- Composition over Inheritance: interface + embedding
- Explicit over Implicit: magicなし
- Concurrency as First-class: Goroutine・Channelが言語レベル
- Pragmatism: 純粋さより現場性
1.3 得意 vs 苦手
| 得意 | 苦手 |
|---|---|
| マイクロサービス | 低レベルメモリ制御 |
| CLIツール | GPUコンピューティング |
| ネットワークプログラム | リアルタイムシステム |
| DevOpsツール | ゲームエンジン |
| gRPCサービス | モバイルUI |
| コンテナ/オーケストレーション | 組み込みファームウェア |
第2部 — Goroutine: Goの並行モデル
2.1 Goroutineとは
軽量スレッド。OSスレッド1本で数千のGoroutineをスケジューリング。
go func() {
fmt.Println("hello from goroutine")
}()
- OSスレッド: 約2MBスタック
- Goroutine: 起動時 2KBスタック、必要に応じ動的拡張
GOMAXPROCS(既定はCPUコア数) 分のOSスレッドを使用
2.2 M:N スケジューラ
- M: OSスレッド (Machine)
- N: Goroutine
- P: Processor (Goroutineキュー管理)
GoランタイムがM個のGoroutineをN本のスレッド上にマルチプレクス。構造的にはTokioと類似。
2.3 Goroutineリーク — 最も多い本番バグ
// 誤り: receiverが居ないとsenderは永久に待つ
func leak() {
ch := make(chan int)
go func() {
ch <- 42 // 受信者なし → 永久ブロック
}()
// 関数は終了、しかしgoroutineは生き続ける
}
対策パターン:
context.Contextでキャンセル伝播selectに<-ctx.Done()を含める- Buffered channel + timeout
2.4 Goroutine制御パターン3つ
// 1. WaitGroup — N個の完了待ち
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
// work
}(i)
}
wg.Wait()
// 2. 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 { /* ... */ }
// 3. Semaphore — 同時実行数の制限
import "golang.org/x/sync/semaphore"
sem := semaphore.NewWeighted(10)
for _, task := range tasks {
task := task
sem.Acquire(ctx, 1)
go func() {
defer sem.Release(1)
do(task)
}()
}
第3部 — Channel: メモリを共有して通信するな、通信で共有せよ
3.1 基本
ch := make(chan int) // Unbuffered
ch := make(chan int, 10) // Buffered
ch <- 42 // 送信
val := <-ch // 受信
close(ch) // クローズ
val, ok := <-ch // ok=falseなら閉じて空
for v := range ch { // 閉じるまで反復
fmt.Println(v)
}
3.2 Unbuffered vs Buffered
- Unbuffered: 送信者と受信者が同期(Rendezvous)
- Buffered: バッファが埋まるまで送信者はブロックしない
3.3 select — マルチプレクス
select {
case msg := <-ch1:
fmt.Println("from ch1", msg)
case msg := <-ch2:
fmt.Println("from ch2", msg)
case <-time.After(1 * time.Second):
fmt.Println("timeout")
case <-ctx.Done():
fmt.Println("canceled")
}
selectこそGo並行の真の武器。Channelを組み合わせ複雑なパターンを構築する。
3.4 主要Channelパターン5つ
- Fan-out: 1つのChannelを複数Goroutineが消費
- Fan-in: 複数Channelを1つに合流
- Pipeline: 段階的Channelチェーン
- Worker Pool: N個のWorker + Job Channel
- Done Channel: キャンセル信号(Contextの前身)
// Worker Pool
jobs := make(chan Job, 100)
results := make(chan Result, 100)
for w := 1; w <= 10; w++ {
go worker(jobs, results)
}
for _, j := range allJobs {
jobs <- j
}
close(jobs)
for a := 1; a <= len(allJobs); a++ {
<-results
}
3.5 Channelの罠3つ
- クローズ済Channelへ書き込み: panic。
sync.Onceで1回きり保証 - nil Channelへの送受信: 永久ブロック。selectで無効化トリックに使う
- Over-synchronization: ChannelがMutexより常に良いわけではない
第4部 — Context: Goエコシステムの血流
4.1 Contextとは
リクエスト範囲のキャンセル・デッドライン・値を伝播する標準インターフェース。
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
4.2 4つの生成関数
ctx := context.Background() // ルート
ctx := context.TODO() // 後で決める
ctx, cancel := context.WithCancel(parent)
ctx, cancel := context.WithTimeout(parent, 5*time.Second)
ctx, cancel := context.WithDeadline(parent, time.Now().Add(1*time.Minute))
ctx := context.WithValue(parent, key, val)
必ずdefer cancel() — リソースリーク防止。
4.3 Context伝播の原則5つ
- Contextは常に第一引数:
func DoWork(ctx context.Context, arg T) error - Structに保持しない: リクエスト毎に新規生成
- nilを渡さない: 曖昧なら
context.TODO() - Valueはリクエストメタデータ専用: request ID, user ID, trace情報。業務パラメータは不可
- 全てのブロッキング呼び出しがctxを受ける: DB, HTTP, gRPC すべて
4.4 Context Valueアンチパターン
// 悪い例: 業務パラメータをValueで渡す
ctx = context.WithValue(ctx, "userID", uid)
// 良い例: 型安全なkey
type ctxKey string
const userIDKey ctxKey = "userID"
ctx = context.WithValue(ctx, userIDKey, uid)
func GetUserID(ctx context.Context) (string, bool) {
uid, ok := ctx.Value(userIDKey).(string)
return uid, ok
}
第5部 — エラーハンドリング: Error as Value
5.1 哲学
result, err := doSomething()
if err != nil {
return nil, fmt.Errorf("doSomething failed: %w", err)
}
例外なし。エラーは値。 明示的に処理するか伝播する。
5.2 Error Wrapping (Go 1.13+)
if err != nil {
return fmt.Errorf("failed to load user %d: %w", id, err)
}
var notFoundErr *NotFoundError
if errors.As(err, ¬FoundErr) { /* ... */ }
if errors.Is(err, sql.ErrNoRows) { /* ... */ }
%w: wrap (アンラップ可)%v: 文字列フォーマットのみ (アンラップ不可)
5.3 Sentinel Error vs Typed Error
// Sentinel — シンプルな比較
var ErrNotFound = errors.New("not found")
// Typed — 追加情報
type NotFoundError struct {
Resource string
ID string
}
func (e *NotFoundError) Error() string { /* ... */ }
5.4 アンチパターン5つ
if err != nilを消して無視- ログ出力と伝播を同時に: 二重ログ
panic()の乱用: 回復不能な時のみ- wrapなしで伝播: スタック追跡不可
- エラー文字列が大文字・句読点付き: Go規約違反
第6部 — sync パッケージ: 並行プリミティブ
6.1 コア7つ
| 型 | 用途 |
|---|---|
sync.Mutex | 相互排他 |
sync.RWMutex | 多読み/一書き |
sync.WaitGroup | N個のGoroutine待ち |
sync.Once | 一度だけ実行 |
sync.Cond | 条件変数 |
sync.Map | 並行マップ(特定パターンのみ有利) |
sync.Pool | オブジェクト再利用(GC圧軽減) |
6.2 atomic パッケージ (Go 1.19+)
import "sync/atomic"
var counter atomic.Int64
counter.Add(1)
v := counter.Load()
counter.Store(100)
var ready atomic.Bool
ready.Store(true)
Mutexより速いが単純演算に限る。
6.3 Race Condition検出
go test -race ./...
go run -race main.go
本番投入前に必ず-race通過確認。 CI必須。
第7部 — Generics (Go 1.18+)
7.1 基本文法
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
}
doubled := Map([]int{1, 2, 3}, func(n int) int { return n * 2 })
7.2 Type Constraint
import "golang.org/x/exp/constraints"
func Max[T constraints.Ordered](a, b T) T {
if a > b { return a }
return b
}
type Number interface {
int | int64 | float64
}
func Sum[T Number](s []T) T {
var total T
for _, v := range s { total += v }
return total
}
7.3 使うべきでない時
- コンテナでない
- interfaceで十分 (例:
io.Reader) - コンパイル時間が重要
Goチーム公式アドバイス: 「迷ったら使うな。」
第8部 — 2024〜2025 Go Webフレームワーク比較
| フレームワーク | 特徴 | 適合 |
|---|---|---|
| net/http | 標準ライブラリ | シンプル, 1.22+で強力 |
| chi | ミニマル, 標準寄り | マイクロサービス既定 |
| gin | 成熟・人気 | スタートアップ, フルスタック |
| echo | Gin類似, 整然 | 代替 |
| fiber | Express風, fasthttp | 極限性能 |
| huma | OpenAPIファースト | API契約重視 |
Go 1.22の強化net/httpルータ:
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", getUserHandler)
mux.HandleFunc("POST /users", createUserHandler)
http.ListenAndServe(":8080", mux)
1.22からメソッド・パス変数が標準。フレームワーク不要な場面が多い。
8.1 Chi例
import "github.com/go-chi/chi/v5"
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.Timeout(5 * time.Second))
r.Route("/api/v1", func(r chi.Router) {
r.Get("/users", listUsers)
r.Post("/users", createUser)
r.Get("/users/{id}", getUser)
})
http.ListenAndServe(":8080", r)
第9部 — gRPCとマイクロサービス実戦
9.1 なぜgRPCか
- Protocol Buffers: 型安全 + 小ペイロード
- HTTP/2: マルチプレクス, ヘッダ圧縮
- Streaming: server, client, 双方向
- Code Generation: 多言語整合
9.2 .proto ファイル
syntax = "proto3";
package user.v1;
option go_package = "example.com/gen/user/v1;userv1";
service UserService {
rpc GetUser(GetUserRequest) returns (User);
rpc ListUsers(ListUsersRequest) returns (stream User);
}
message User {
string id = 1;
string name = 2;
int32 age = 3;
}
message GetUserRequest { string id = 1; }
message ListUsersRequest { int32 page_size = 1; }
9.3 buf + ConnectRPC (2025の標準)
- buf: .protoのlint/build/破壊的変更検出
- ConnectRPC: gRPC + REST + gRPC-Webを一括
import "connectrpc.com/connect"
type server struct{}
func (s *server) GetUser(
ctx context.Context,
req *connect.Request[userv1.GetUserRequest],
) (*connect.Response[userv1.User], error) {
return connect.NewResponse(&userv1.User{Id: req.Msg.Id}), nil
}
9.4 マイクロサービスチェックリスト
- Observability 3軸: metrics (Prometheus), logs (structured), traces (OpenTelemetry)
- Context伝播: 全RPCにctxを渡す
- Retry with backoff
- Circuit Breaker:
sony/gobreaker - Timeout: 呼び出し毎に明示
- Health Check: grpc-health-probe
- Graceful Shutdown: SIGTERM時に既存要求を完了
- Rate Limit:
golang.org/x/time/rate - Request ID
- Deadline Propagation
第10部 — Go エコシステム 2024〜2025 の20選
| カテゴリ | パッケージ |
|---|---|
| Web | chi, gin, echo, fiber |
| gRPC | grpc-go, buf, connect-go |
| DB | database/sql + pgx, sqlc, ent, gorm |
| ログ | log/slog (標準, 1.21+), zerolog |
| テスト | testify, gomock, testcontainers-go |
| CLI | cobra, viper, spf13/pflag |
| Observability | prometheus/client_golang, opentelemetry-go |
| 並行性 | golang.org/x/sync/errgroup, semaphore |
| HTTPクライアント | net/http + retryablehttp |
| JSON | encoding/json + json-iterator/go |
10.1 Go 1.23〜1.24の主要変更
- Go 1.23 (2024/08):
rangeover func の安定化, 改善されたtime.Timer - Go 1.24 (2025/02): Generic Type Alias,
tooldirective
第11部 — Rust vs Go 選択ガイド
11.1 決定木
Q1. メモリ・レイテンシが極限か?
Yes -> Rust
No -> Q2
Q2. チーム規模が大きく、高速オンボード重視か?
Yes -> Go
No -> Q3
Q3. システムプログラミング (kernel, DB, runtime) か?
Yes -> Rust
No -> Q4
Q4. Kubernetes/DevOps エコシステムか?
Yes -> Go
No -> Q5
Q5. GC一時停止が許容不可か?
Yes -> Rust
No -> Go (多くの場合)
11.2 冷静な比較
| 項目 | Rust | Go |
|---|---|---|
| 学習曲線 | 3〜6ヶ月 | 2〜4週 |
| コンパイル速度 | 遅い | 非常に速い |
| 実行時性能 | トップ級 | 高い |
| メモリ使用 | 非常に少ない | GCオーバーヘッド |
| 並行安全性 | コンパイル時 | 実行時 (-race) |
| 生態系成熟度 | 急成長中 | 非常に成熟 |
| ドキュメント | 優 | 優 |
| 型システム | 強力・複雑 | 単純 |
日常判断:
- バックエンドAPI -> Go (10分で作れる)
- DB・ランタイム・プロキシ -> Rust
- どちらでも -> チームの言語
第12部 — Go 6ヶ月ロードマップ
Month 1: 基礎
- Tour of Go 完走
- Effective Go 読了
- CLIツール1本作成
Month 2: 並行性
- Concurrency in Go (Katherine Cox-Buday)
- Worker Poolを自作
-raceでrace練習
Month 3: 標準ライブラリを深く
- net/http ソース読解
- context パターン体得
- errgroup + semaphore 実戦
Month 4: 実戦サーバ
- chi + pgx + PostgreSQLでAPI
- Observability 3軸 (slog + prometheus + otel)
- testcontainersで統合テスト
Month 5: gRPC・マイクロサービス
- Protocol Buffers
- grpc-go もしくは connect-go
- 3サービス間通信 + Observability
Month 6: OSS貢献
- Kubernetes/Istio/Prometheusのissue 1つ解決
第13部 — Goチェックポイント12
- GoroutineとOSスレッドの違いを説明できる
- Channelの3状態 (nil, open, closed) の挙動を知る
- Context伝播5原則を言える
- errgroup の存在意義を知る
- Race Condition検出法を知る
sync.Mutexvssync.RWMutexの選択基準%wの意味を知る- Goroutineリーク予防法
- net/http 1.22 ルータの新機能
- gRPC + buf + connect スタックを説明できる
- slog 構造化ロギングを使える
- Generic濫用の害を理解
第14部 — Goアンチパターン10
- Fire-and-forget Goroutine: キャンセル・完了待ちなし -> リーク
- Contextをstructフィールドに保持
- エラー返すべき所で
panic() - interfaceにメソッド10個以上: 「小さいinterface」違反
- 空interface (
any,interface{}) 濫用: 型安全性放棄 - goroutine内で共有変数を同期なしで読み書き: race
fmt.Errorf("%v")でラップ: wrapにならない。%wを使う- defer内でエラー無視: 必要なら無名関数で処理
- mapの同時アクセス: ランタイムpanic。
sync.Mapかsync.RWMutex - nil interface比較:
var e error = (*MyError)(nil); e != nilは true
おわりに — Goは「退屈だから強い」
Goは派手でない。文法は10分で学べる。最新トレンドもほぼない。
しかしその「退屈さ」こそチームの生産性だ。新人が2週間で本番コードを書き、10年前のコードがまだ読める。Kubernetesが500万行でも生き残る理由。
Rustが「間違えればコンパイルされない」なら、Goは**「多くの人が書いても皆が読める」**の哲学。
2025年、シニアエンジニアの道具箱にはRustとGoの両方を。状況に応じて出す。
次回予告 — TypeScript 完全ガイド
Season 2 Ep 4 は TypeScript。次回:
- TypeScript 5.x の新型演算子
- Generics と Conditional Type
- Template Literal Type の実戦
- Zod + tRPC + TanStackで型安全フルスタック
- 2024〜2025 TS エコシステム (Turborepo, Bun, Deno)
- AIと共にTypeScriptを書く
では次回。