Go言語のcontextパッケージを活用したキャンセル処理の基本を徹底解説!初心者でもわかるgoroutineの安全な終了方法
生徒
「先生、Go言語で長時間動く処理を止めたいときはどうすればいいんですか?たとえば、APIの呼び出しとか、goroutineがずっと動き続けてしまうときです。」
先生
「とても大事なポイントですね。そんなときに使うのが、context(コンテキスト)パッケージです。これは、処理の“キャンセル”や“タイムアウト”を簡単に管理するための仕組みなんです。」
生徒
「なるほど。つまり、goroutineを安全に止められるってことですか?」
先生
「その通りです。では、Goのcontextを使って、キャンセル処理の基本をわかりやすく説明していきましょう。」
1. contextパッケージとは?
context(コンテキスト)とは、Go言語で「処理の状態を共有するための仕組み」です。特に、キャンセルやタイムアウトをgoroutine全体に伝えるために使われます。
例えば、Webサーバーでリクエストを処理しているとき、ユーザーが途中でページを閉じたら、そのリクエスト処理を止めたいですよね。そんなときにcontextが役立ちます。
つまり、contextを使うと「処理をやめる合図」をgoroutineに伝えることができるのです。
2. contextの基本構造
Go言語のcontextには、以下のような種類があります。
- context.Background():一番基本となる空のコンテキスト。
- context.WithCancel():キャンセル機能を追加する。
- context.WithTimeout():一定時間で自動キャンセル。
- context.WithDeadline():指定時刻でキャンセル。
これらを使い分けることで、処理の制御がとても簡単になります。では、実際にWithCancelを使って、goroutineをキャンセルしてみましょう。
3. context.WithCancelの使い方
WithCancelは、手動でキャンセルできるcontextを作る関数です。まずはサンプルコードを見てください。
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
go func() {
for {
select {
case <-ctx.Done():
fmt.Println("処理を中断しました")
return
default:
fmt.Println("goroutine実行中...")
time.Sleep(500 * time.Millisecond)
}
}
}()
time.Sleep(2 * time.Second)
cancel() // キャンセルを呼び出す
time.Sleep(1 * time.Second)
}
このコードでは、cancel()を呼ぶことで、goroutineにキャンセルの合図を送っています。goroutineはctx.Done()を受け取ると処理を止めます。
goroutine実行中...
goroutine実行中...
goroutine実行中...
goroutine実行中...
処理を中断しました
このように、キャンセルを安全に伝えることで、無限ループのような処理でも安全に停止できます。
4. context.WithTimeoutで自動キャンセル
今度は、一定時間が経ったら自動でキャンセルされるWithTimeoutを使ってみましょう。手動でキャンセルを呼ばなくても、自動で処理を止めることができます。
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
go func() {
for {
select {
case <-ctx.Done():
fmt.Println("タイムアウトにより終了")
return
default:
fmt.Println("処理中...")
time.Sleep(500 * time.Millisecond)
}
}
}()
time.Sleep(3 * time.Second)
}
このコードでは、2秒経過すると自動的にキャンセルされます。プログラムが勝手に止まるような動きを作れるため、外部APIなど「待ち時間が長い処理」に向いています。
処理中...
処理中...
処理中...
タイムアウトにより終了
5. contextを使う理由とメリット
contextを使うことで、複数のgoroutineをまとめて管理できるという大きな利点があります。
- 親goroutineがキャンセルすると、すべての子goroutineに通知される
- タイムアウトや終了タイミングを統一できる
- ネットワーク処理・API通信で安全なキャンセルが可能
もしcontextを使わないと、goroutineが止まらず、メモリを無駄に消費することになります。大規模なシステムでは、このような「停止制御」が非常に重要です。
6. contextのキャンセル伝播(親子関係)
contextは親子関係を持つことができます。つまり、親contextをキャンセルすると、その子contextも自動的にキャンセルされます。
package main
import (
"context"
"fmt"
"time"
)
func main() {
parent, cancel := context.WithCancel(context.Background())
child, _ := context.WithCancel(parent)
go func() {
<-child.Done()
fmt.Println("子contextもキャンセルされました")
}()
time.Sleep(time.Second)
cancel() // 親をキャンセル
time.Sleep(time.Second)
}
このように、親がキャンセルされると、子も自動で終了します。複数の処理をまとめて停止したいときに便利な仕組みです。
7. 実務での活用例
contextは、特にWebアプリケーションやAPIサーバーでよく使われます。たとえば、net/httpパッケージでは、リクエストごとに自動でcontextが生成されます。
ユーザーがページを閉じたらリクエスト処理がキャンセルされる仕組みも、実はcontextで実現されています。これにより、不要な処理を自動で止め、サーバーのリソースを節約できます。