[Go]contextのイメージから使い方まで理解する

に公開

はじめに

func CreateHoge (ctx context.Context, userID uuid.UUID) error {}

開発をする中で上記のcontextというものがよく分からなかったので、調べるついでに記事にしようということで執筆しました。

想定する読者

  • contextの存在は知っているが、使い方に自信がない

  • 「とりあえずctx渡しておけばいいんでしょ?」レベルから脱却したい

contextの概要とイメージ

まずはcontextの概要についてです。

こちらの記事でも載っていましたが、公式では下記のように説明しています。

Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.

(訳): contextパッケージで定義されているContext型は、処理の締め切り・キャンセル信号・API境界やプロセス間を横断する必要のあるリクエストスコープな値を伝達させることができます。

出典:pkg.go.dev - context pkg

リクエストスコープ:ブラウザからのリクエスト(要求)がサーバーで処理され、レスポンス(応答)が返されるまでの間、特定のデータを保存できる範囲のこと

簡単にイメージするとリクエストスコープ内で情報を引き継ぐ箱のようなものです。

この箱に認証情報や、ログで出力したいIDなどを詰め込んで使っていきます。

contextにデータを詰め、取得してみる

それではcontextにデータを詰め込み、詰め込まれているデータの取得を具体的なコードで説明していきます。

まずは詰めるcontextを作るため、Backgroundで空のcontextを生成します。

ctx := context.Background()

次に作成したcontextに値を詰めるため、WithValueを使用します。

ctx = context.WithValue(ctx, "userID", "user-12345")

下記のように指定

  • 第一引数:値を詰めるcontext

  • 第二引数:キー

  • 第三引数:詰める値

contextに詰めた値を取得するためにValueを使用します。

引数にはcontextに保存する際に指定したキーを渡すと、対象のキーの値を取得することができます。

id, ok := ctx.Value("userID").(string) 	
if !ok {
    log.Println("userID not found in context")
    return
}
fmt.Println(id)  // "出力結果:user-12345"

実践的なコード例

実際に使う一例として、認証情報の伝播が挙げられます。

下記のコードでは以下のようなことを実現しています。

  • 認証ミドルウェアであるAuthMiddlewareでcontextに認証情報(対象のユーザーが管理者か一般ユーザーかなどの情報)を詰める

  • 各handler内部でcontextから引き出すような形にして、権限にあった処理の分岐を書ける形にしてあげる

type userContextKey struct{}

// 認証ミドルウェア
func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        user, err := validateToken(token)
        if err != nil {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        
        ctx := context.WithValue(r.Context(), userContextKey{}, user)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// ハンドラーで使用
func GetUserProfile(w http.ResponseWriter, r *http.Request) {
    user := r.Context().Value(userContextKey{}).(*User)
    // userの権限チェックなど
}

contextにはリクエストスコープで共通するデータのみ詰める

公式からは下記のように明記されていて、contextにはリクエストスコープで共通するデータを渡すようにして、関数の引数で渡すような値をcontextに詰めないようにとのことです。

Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.

(訳)コンテキスト・バリューは、プロセスやAPIを通過するリクエスト・スコープのデータにのみ使用し、関数にオプションのパラメータを渡す際には使用しない。

出典:https://pkg.go.dev/context#pkg-overview

具体例

// ページネーションのパラメータをcontextに入れることは非推奨
ctx = context.WithValue(ctx, "page", 1)
ctx = context.WithValue(ctx, "limit", 20)

// 関数の引数として渡すことを推奨
func GetUsers(ctx context.Context, page int, limit int) ([]User, error)

ページネーションのパラメータはGetUsersメソッドでのみ参照される情報なのでcontextに含めないよう推奨しているということですね。

終わりに

この記事ではcontextのイメージから基本的な使い方について解説しました。

私自身contextのイメージがつきづらく理解までに時間がかかったので、イメージつけの部分から記述しました。これを機に応用的なgoroutineでのcontextの使い方についても理解を深めていきたいと感じました。

お読みいただきありがとうございました。

Discussion