✍️

Go初心者がcontextを解説

に公開

contextパッケージとは

前回投稿した記事 https://zenn.dev/yamakazu/articles/1e948e0ed113f1
でも書きましたが改めて

タイムアウトやキャンセルの伝達

をcontextパッケージを通してサーバーに伝えるために使われます。

実際にコードを書いて説明

今回は animal 関数が 10秒以内に処理を終えられなかったらリクエストをキャンセル という例で説明します。

package main

import (
    "fmt"
    "context"
    "time"
)

func animal() {
    ctx := context.Background()
    ctx, cancel := context.WithTimeout(ctx, 10 * time.Second)
defer cancel()

for i:=0; i < 5; i++ {
    select {
    case <- time.After(3 * time.Second):
        fmt.Println("cute")
    case <- ctx.Done():
        fmt.Println("キャンセル")
        return
        }
    }
}

func main() {
    animal()
}

説明

まずはこの部分から!

ctx, cancel := context.WithTimeout(ctx, 10 * time.Second)

//今回は分かりやすくするために上記のように書いたが実際には実際にはこう書くのが普通
ctx, cancel := context.WithTimeout(context.Background(), 10 * time.Second)

これは 10秒経ったら ctx が自動キャンセル(タイムアウト)する という意味です。

Go公式の説明と直訳

以下go公式文
"The context package provides functions to derive new Context values from existing ones. These values form a tree: when a Context is canceled, all Contexts derived from it are also canceled. Background is the root of any Context tree; it is never canceled."
直訳すると
"context パッケージは、既存の Context から新しい Context 値を派生させるための関数を提供します。これらの値は木構造を形成します。ある Context がキャンセルされると、そこから派生したすべての Context もキャンセルされます。Background はすべての Context 木の“根(ルート)”であり、それ自身がキャンセルされることは決してありません。"

context.Background()

context の設計は木構造になっていて、
Background が全ての Context の「親(Root)」 になります。
WithTimeout
WithCancel
WithValue

など 全ての派生 Context を作るための“クリーンな親”が Background() です。

分かりやすく例えるなら、、

context.Background() は
「まっさらな紙」
そこに:
タイムアウトを貼り付け → WithTimeout
キャンセル機能をつける → WithCancel
値を持たせる → WithValue
という 装飾を後から付けていくイメージ。
とChatGpt君が例えてくれました😄

つまりBackground()を使う理由は

WithTimeout や WithCancel で派生 context を作るための 完全にクリーンな親が必要だから。
です!!!

先ほどのコードは
"10秒"後に処理をキャンセル(止める)するctxを生成しているということです!

さてここまできたら最初の頃よりはなんとなくわかると思いますが

for i := 0; i < 5; i++ {
		select {
		case <-time.After(3 * time.Second):
			fmt.Println("cute")
		case <-ctx.Done():
			fmt.Println("キャンセル")
            return
		}
	}

このコードの意味は
10秒以内に処理ができなかったら強制的に処理がキャンセルされるという意味です!

time.After()

指定した時間(今回は3秒)後に“1回だけ値を送るチャネル”を返す関数」 です

ここではtime.After(3 * time.Second)となっているので
3秒ごとに結果を返すという意味です!
ctxは10秒後にキャンセルとなるようにしているのでfor文が5回動くまでに10秒かかった場合は
ctx.Done()に入り処理はキャンセルとなります。

では今回作成したコードの結果を見てみましょう

package main

import (
    "fmt"
    "context"
    "time"
)

func animal() {
    ctx := context.Background()
    ctx, cancel := context.WithTimeout(ctx, 10 * time.Second)
defer cancel()

for i:=0; i < 5; i++ {
    select {
    case <- time.After(3 * time.Second):
        fmt.Println("cute")
    case <- ctx.Done():
        fmt.Println("キャンセル")
        return
        }
    }
}

func main() {
    animal()
}

// 実行結果
cute        // 3秒
cute        // 6秒
cute        // 9秒
キャンセル  // 10秒 timeout発生

4回目の開始時に ctx がタイムアウトし Done が閉じたため
select は ctx.Done() を選び “キャンセル” が表示され、return でループを抜けます。

まとめ

contextはめっちゃ重要
Goは公式を見ると深く理解しやすい
キャンセルやタイムアウトを「木構造で伝える」
Background() は全ての Context の根っこ
WithTimeout で期限付きの Context を作れる
time.After は “指定時間後に1回値を送るチャネル”

次はdatabase/sqlパッケージをやっていこうと思います!!

Discussion