Go言語において、プログラムを継続的に実行し続ける仕組みである無限ループは、サーバーサイドの開発やバックグラウンド処理において非常に重要な役割を果たします。
効率的なアプリケーションを構築するためには、単にループを回すだけでなく、リソースの消費を最小限に抑えつつ、適切なタイミングで処理を中断する制御技術が求められます。
本記事では、Go言語における無限ループの基本構造から、forとselectを組み合わせた高度な制御方法、そして現代的な開発で避けては通れないリソース漏れ(リーク)を防ぐためのベストプラクティスを詳しく解説します。
2026年現在のGo言語の仕様に基づき、最新のランタイム挙動を考慮した実装パターンを学び、安定したシステムを構築するための知識を深めていきましょう。
Go言語における無限ループの基本形
Go言語には他の言語に存在する while や do-while といったキーワードが存在せず、ループ処理はすべて for キーワードに集約されています。
最も単純な無限ループは、条件式や初期化文をすべて省略した for {} という形式で記述されます。
package main
import "fmt"
func main() {
// 最もシンプルな無限ループの形
for {
fmt.Println("実行中...")
// 実際にはここで何らかの条件判定を行わないと終了しません
break
}
}
実行中...
この基本形式は、デーモンプロセスやメッセージキューのリスナーなど、アプリケーションが終了するまで永続的に稼働する必要があるコンポーネントで広く利用されています。
しかし、何も制御を行わない無限ループは、CPUリソースを過剰に消費する可能性があるため、適切なスリープ処理やチャネルによる待機を組み合わせることが一般的です。
制御を伴う無限ループの実装パターン
実務的なアプリケーションでは、無条件にループを回すのではなく、外部からのシグナルやデータの受信を待機する「イベント駆動型」の無限ループが多用されます。
for/selectパターンによるイベント待機
Go言語の強力な機能であるチャネルと select 文を組み合わせることで、複数のイベントを効率的に待ち受ける無限ループを構築できます。
このパターンは、並行処理における標準的なイディオムとして確立されており、Goroutineの停止やデータの受信をスマートに管理できます。
package main
import (
"fmt"
"time"
)
func worker(dataChan chan int, stopChan chan struct{}) {
for {
select {
case data := <-dataChan:
fmt.Printf("データ受信: %d\n", data)
case <-stopChan:
fmt.Println("ループを停止します")
return
}
}
}
func main() {
dataChan := make(chan int)
stopChan := make(chan struct{})
go worker(dataChan, stopChan)
dataChan <- 1
dataChan <- 2
time.Sleep(100 * time.Millisecond)
stopChan <- struct{}{}
}
データ受信: 1
データ受信: 2
ループを停止します
このように、select を使用することで、データの処理と終了シグナルの検知を多重化(マルチプレクシング)し、効率的なイベントループを実現できます。
Context APIを活用したキャンセル制御
現代的なGoの開発では、ループの終了制御に context.Context を使用することが推奨されます。
context を使うことで、タイムアウトや親プロセスからのキャンセル要求を伝播させ、安全に無限ループを抜けることが可能になります。
package main
import (
"context"
"fmt"
"time"
)
func process(ctx context.Context) {
for {
select {
case <-ctx.Done():
// コンテキストがキャンセルされた場合のクリーンアップ処理
fmt.Println("コンテキストによる停止:", ctx.Err())
return
default:
// メインとなる業務ロジック
fmt.Println("タスク実行中...")
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
process(ctx)
}
タスク実行中...
タスク実行中...
コンテキストによる停止: context deadline exceeded
このコード例では、time.Sleep を含むロジックが default 節で実行されつつ、ctx.Done() によってタイムアウト時にループを安全に脱出しています。
リソース漏れ(リーク)を防ぐための重要事項
無限ループを実装する際、最も注意すべき点は、メモリやGoroutineのリソース漏れです。
終了条件が不適切な無限ループは、プログラムの稼働時間が長くなるにつれてリソースを食い潰し、最終的にシステムをクラッシュさせる原因となります。
Goroutineリークの回避
無限ループを含むGoroutineを開始したものの、そのループを抜ける手段がない場合、「Goroutineリーク」が発生します。
以下の表は、無限ループにおけるリソース管理のチェックポイントをまとめたものです。
| 項目 | リスク | 対策 |
|---|---|---|
| 終了チャネルの欠如 | Goroutineがメモリに残り続ける | Contextや専用のcloseチャネルを必ず用意する |
| time.Tickの無造作な使用 | Tickerが解放されず蓄積する | time.NewTickerを使用して明示的にStopを呼ぶ |
| ループ内でのdefer | ループ終了まで関数が実行されずメモリ増大 | ループ内ではdeferを使わず、必要なら関数化する |
特に time.Tick は、ガベージコレクションの対象になりにくいため、ループ内で長期的に使用する場合は注意が必要です。
time.NewTicker を使い、不要になったら Stop() を呼び出す習慣をつけましょう。
CPUビジーウェイトの防止
条件が満たされるまで何もしない for {} ループを記述すると、そのGoroutineはCPUコアの1つを100%占有してしまいます。
これは「ビジーウェイト」と呼ばれ、システムのパフォーマンスを著しく低下させます。
Goのランタイムスケジューラは協力型(Cooperative)に近い挙動を示しますが、何もチャネル操作や関数呼び出しを含まないタイトなループは、他のGoroutineの実行を妨げる可能性があります。
// 悪い例:CPUを激しく消費する
for {
if condition {
break
}
// 何も待機せず、CPUを回し続ける
}
このような場合は、runtime.Gosched() を呼び出して実行権を譲るか、微小な time.Sleep を入れる、あるいはチャネルによる通知を待機する設計に変更してください。
ループ内でのエラーハンドリングと再試行
無限ループ内でネットワーク通信やデータベース操作を行う場合、エラーが発生した際の挙動を慎重に設計する必要があります。
エラーが発生したからといって即座にループを終了(return)させてしまうと、一時的な障害でサービス全体が停止してしまいます。
一方で、エラーを無視して高速でループを継続すると、ログが溢れたり、接続先のサービスに過剰な負荷(DDoS状態)をかけたりすることになります。
推奨されるのは、「指数バックオフ(Exponential Backoff)」を導入した再試行メカニズムです。
package main
import (
"fmt"
"time"
)
func main() {
backoff := 1 * time.Second
maxBackoff := 60 * time.Second
for {
err := doWork()
if err != nil {
fmt.Printf("エラー発生: %v. %v後に再試行します。\n", err, backoff)
time.Sleep(backoff)
// 指数的に待機時間を増やす
backoff *= 2
if backoff > maxBackoff {
backoff = maxBackoff
}
continue
}
// 成功した場合は待機時間をリセット
backoff = 1 * time.Second
time.Sleep(10 * time.Second)
}
}
func doWork() error {
// 擬似的なエラー発生
return fmt.Errorf("接続失敗")
}
このように、エラーの状態に応じてループの回転速度を調整することで、システムの堅牢性を高めることができます。
Go 1.22以降のループ変数の挙動と注意点
2026年現在では常識となっていますが、Go 1.22以降、for ループの変数のスコープは反復ごとに新しく作成されるようになりました。
かつてのGoでは、無限ループ内で go func() { ... }() を実行し、ループ変数を直接参照すると、すべてのGoroutineが同じメモリアドレス(最後の値)を参照してしまうバグが頻発していました。
現在の仕様では、プログラマが明示的に変数を再定義しなくても、各反復で安全に変数のコピーが渡されます。
ただし、この変更は for i := 0; i < n; i++ や for range に適用されるものであり、単純な for {} 内で手動で値を更新している場合は、引き続きクロージャへの変数キャプチャには注意が必要です。
まとめ
Go言語における無限ループは、シンプルでありながら非常に強力な制御構造です。
for 文による基本ループに select と context を組み合わせることで、複雑な非同期イベントも整然と処理できるようになります。
開発時には、「どのようにループを始めるか」よりも「どのように安全にループを終わらせるか」に注力してください。
リソース漏れを防ぎ、CPU負荷を適切に管理し、エラー時の再試行戦略を組み込むことで、プロダクション環境に耐えうる高品質なプログラムを記述できます。
本記事で解説したパターンを活用し、スケーラブルで堅牢なGoアプリケーションの実装に取り組んでみてください。
