LoginSignup
4
0

gin.Contextをcontext.Contextとして利用するときはgin.EngineのContextWithFallbackをtrueにする

Posted at

以下のようにしておくとcontext.Contextにgin.Contextを渡した際にValue()などを利用できる。

r := gin.Default()
r.ContextWithFallback = true

説明

例えばIntoContextのようにcontext.Contextを受け取ってWithValueで値をセットしたcontext.Contextを返す関数や
context.ContextからValueでセットしてある値を取り出す関数を利用する場合。

type usernameKey struct{}

func IntoContext(ctx context.Context, username string) context.Context {
	return context.WithValue(ctx, usernameKey{}, username)
}

func FromContext(ctx context.Context) (string, error) {
	value := ctx.Value(usernameKey{})
	if value == nil {
		return "", errors.New("not found")
	}
	username, ok := value.(string)
	if !ok {
		return "", errors.New("invalid type")
	}
	return username, nil
}

r.ContextWithFallbackfalse (初期値) の場合gin.Contextをそのまま渡すと FromContext内のctx.Valuenilが返ってくる。

func handle(ctx *gin.Context) {
    username, err := FromContext(ctx)
    if err != nil {
        panic(err) // not foundなerrorでパニックになる
    }
}

ctx.Request.Context()を渡せば取得できる。

func handle(ctx *gin.Context) {
    username, err := FromContext(ctx.Request.Context())
    if err != nil {
        panic(err) // パニックにならない
    }
    println(username) // IntoContextでセットした値が出力される
}

ctx.Request.Context()を毎回渡すのはちょっと・・・。
c := ctx.Request.Context()とするのもちょっと・・・。
やはりctx *gin.Contextctxをそのまま渡したい。

ということでコードを読んでみると ContextWithFallbacktrueの場合は ctx.Request.Context().Value()にフォールバックし、falseの場合はnilを返すように実装されていました。

// https://github.com/gin-gonic/gin/blob/v1.9.1/context.go#L1184-L1188
func (c *Context) hasRequestContext() bool {
	hasFallback := c.engine != nil && c.engine.ContextWithFallback
	hasRequestContext := c.Request != nil && c.Request.Context() != nil
	return hasFallback && hasRequestContext
}

// https://github.com/gin-gonic/gin/blob/v1.9.1/context.go#L1229-L1232
func (c *Context) Value(key any) any {
    // 一部省略...

	if !c.hasRequestContext() {
		return nil
	}
	return c.Request.Context().Value(key)
}

gin.Default()や内部で使われているgin.New()ではContextWithFallbackに値をセットしていないのでデフォルトのfalseになっています。
そのため、gin.Default()gin.New()の後にContextWithFallbacktrueをセットすることで想定した動作をさせることができます。

r := gin.Default()
r.ContextWithFallback = true

test

package main

import (
	"net/http/httptest"
	"testing"

	"github.com/gin-gonic/gin"
)

func TestGinContext(t *testing.T) {
	gin.SetMode(gin.TestMode)
	tests := []struct {
		name                string
		contextWithFallback bool
		wantErr             bool
	}{
		{
			name:                "ContextWithFallbackがfalse",
			contextWithFallback: false,
			wantErr:             true,
		},
		{
			name:                "ContextWithFallbackがtrue",
			contextWithFallback: true,
			wantErr:             false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			w := httptest.NewRecorder()
			c, r := gin.CreateTestContext(w)
			c.Request = httptest.NewRequest("GET", "http://localhost", nil)
			r.ContextWithFallback = tt.contextWithFallback

			c.Request = c.Request.WithContext(IntoContext(c, "user"))

			username, err := FromContext(c)
			if tt.wantErr {
				if err == nil {
					t.Fatal("err is nil")
				}
				return
			}

			if err != nil {
				t.Fatal(err)
			}

			if username != "user" {
				t.Fatal("invalid username got: ", username)
			}
		})
	}
}

実行結果

Running tool: /usr/bin/go test -timeout 30s -run ^TestGinContext$ gin-context

=== RUN   TestGinContext
=== RUN   TestGinContext/ContextWithFallbackがfalse
--- PASS: TestGinContext/ContextWithFallbackがfalse (0.00s)
=== RUN   TestGinContext/ContextWithFallbackがtrue
--- PASS: TestGinContext/ContextWithFallbackがtrue (0.00s)
--- PASS: TestGinContext (0.00s)
PASS
ok      gin-context     0.005s
4
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
4
0