0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GoWatchを作りながら学ぶGoの並行処理 #4 — 実装してわかったハマりどころまとめ

0
Last updated at Posted at 2026-04-14

はじめに

前回はcontextを学びました。
前回記事

  • contextはキャンセルやタイムアウトの信号を複数のgoroutineに伝播させる仕組み
  • WithCancel は任意のタイミングで、WithTimeout は時間経過で自動キャンセルする
  • GoWatchでは3層のcontext階層でアプリ全体・サイクル・個別リクエストのライフサイクルを管理している

最終回の今回は、GoWatchを実装する中で実際にハマったポイントを3つ紹介します。どれも「動いているように見えるが実は問題がある」種類のバグで、Goに慣れていないと気づきにくいものです。


ハマりどころ① deferをループ内で使ってはいけない

問題のコード

// ❌ 問題のある実装
func (c *Checker) worker(ctx context.Context) {
    for {
        select {
        case url := <-c.jobs:
            resp, err := http.Get(url)
            if err != nil {
                continue
            }
            defer resp.Body.Close() // ← ここが問題
        }
    }
}

一見問題なさそうですが、これは深刻なリソースリークを引き起こします。

なぜ問題なのか

deferその関数が返るときに実行される仕組みです。ループの中で defer を使うと、ループが回るたびに defer が積み重なり、関数が終了するまで一切実行されません。

worker() は無限ループで動き続けるため、resp.Body.Close() は永遠に呼ばれません。その結果、HTTPレスポンスのBodyが開きっぱなしになり、コネクションを占有し続けます。

正しい書き方

// ✅ 正しい実装
func (c *Checker) worker(ctx context.Context) {
    for {
        select {
        case url := <-c.jobs:
            resp, err := http.Get(url)
            if err != nil {
                continue
            }
            resp.Body.Close() // deferを使わず明示的に呼ぶ
        }
    }
}

ループ内では defer を使わず、使い終わったタイミングで明示的に呼びます。もしくは処理を別関数に切り出して defer を使う方法もあります。

// ✅ 別関数に切り出す方法
func (c *Checker) check(ctx context.Context, url string) CheckResult {
    resp, err := http.Get(url)
    if err != nil {
        return CheckResult{URL: url, IsDown: true}
    }
    defer resp.Body.Close() // 関数が終わると確実に呼ばれる
    // ...
}

ハマりどころ② ポインタ型と値型の罠(*time.Ticker)

問題のコード

// ❌ 問題のある実装
type Checker struct {
    ticker time.Ticker // 値型で持っている
}

func (c *Checker) tickerLoop(ctx context.Context) {
    c.ticker = *time.NewTicker(30 * time.Second)
    defer c.ticker.Stop()
    // ...
}

なぜ問題なのか

time.NewTicker() はポインタ(*time.Ticker)を返します。これを値型(time.Ticker)にコピーすると、内部のchannelや状態が意図しない形でコピーされます。

Goでは内部にchannelやmutexを持つ型は値コピーしてはいけないというルールがあります。コピーした瞬間に元のTickerと内部状態が乖離し、予期しない動作を引き起こします。

正しい書き方

// ✅ 正しい実装
type Checker struct {
    ticker *time.Ticker // ポインタ型で持つ
}

func (c *Checker) tickerLoop(ctx context.Context) {
    c.ticker = time.NewTicker(30 * time.Second)
    defer c.ticker.Stop()
    // ...
}

ポインタで持つことで、内部状態を共有したまま同じTickerを参照し続けられます。

一般的な判断基準として、New〇〇() がポインタを返す型はポインタで持つと覚えておくと安全です。


ハマりどころ③ goroutineリークを防ぐ考え方

goroutineリークとは

goroutineリークとは、不要になったgoroutineが終了せずにメモリを占有し続ける状態です。

よくある原因は「受信されることのないchannelを待ち続けるgoroutine」です。

// ❌ リークするコード
func leak() {
    ch := make(chan int)
    go func() {
        val := <-ch // 誰も送信しないので永遠に待ち続ける
        fmt.Println(val)
    }()
    // chに何も送らずに関数が終わる
}

このgoroutineはプログラムが終了するまでメモリに残り続けます。

GoWatchでどう防いでいるか

GoWatchでは2つの方針でgoroutineリークを防いでいます。

① すべてのgoroutineにctxを渡す

func (c *Checker) worker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done(): // アプリ終了時に必ずここに入る
            return
        case url := <-c.jobs:
            // 処理
        }
    }
}

ctx.Done() を監視することで、アプリケーション終了時にすべてのgoroutineが確実に終了します。

② channelのクローズを明示的に管理する

// tickerLoopが終了するときにjobsをクローズする
func (c *Checker) tickerLoop(ctx context.Context) {
    defer close(c.jobs)
    // ...
}

送信側がchannelをクローズすると、受信側の for range ch は自動的に終了します。これによりworkerが無限に待ち続ける状態を防げます。


このシリーズを振り返って

4回にわたってGoWatchの実装を通じてGoの並行処理を学びました。

テーマ 学んだこと
#1 goroutine / channel 並行処理の基本単位とデータの渡し方
#2 Worker Pool goroutineの数を制御して安全に並列処理する
#3 context キャンセルとタイムアウトをgoroutineに伝播させる
#4 ハマりどころ defer・ポインタ・goroutineリークの落とし穴

並行処理は「動いているように見えるが実は問題がある」バグが多い領域です。今回紹介した3つのハマりどころはどれも実際に踏んだものなので、同じところで詰まっている方の参考になれば嬉しいです。

次に学ぶとよいこと

このシリーズで扱えなかったテーマとして以下が挙げられます。

  • sync.RWMutex — 読み取りと書き込みを分けてロック効率を上げる
  • goroutineリークのテストgo.uber.org/goleak を使った自動検出
  • sync.WaitGroup — 複数のgoroutineの完了を待つ別のアプローチ

GoWatchのソースコードはこちら → GitHub

最後まで読んでいただきありがとうございました。


参考


関連記事リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?