1
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?

Ginのgin.Contextについて理解する

Posted at

はじめに

はじめまして、Goodpatchでバックエンドエンジニアをしている千里と申します。
2024年11月にGoodpatchへジョインし、ReDesignerというサービスでバックエンドを担当しております。

ReDesignerではバックエンド開発の言語としてGolangを採用しており、WebフレームワークとしてGinを利用しています。
私自身、これまでにWeb開発経験はあったものの、Golangを利用したことは業務上で初めてであり、その構文や機能について学ぶことは多く、入社して約1ヶ月半はGolangの畑に慣れようと茂みを掻き分け歩いている状態です。

今回は、Ginにおいて、最初に出てくるgin.Contextという概念について理解していきたいと思います。

というのも、GinのQuick Startには、以下のようなコードが記載されています。

func main() {
	r := gin.Default()
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})
	r.Run()
}

これは、GinのEngineインスタンスを生成し、/pingに対するGETリクエストを受け取ったら、funcで記載されているHandlerを実行し、その結果としてpongというレスポンスを返すというものです。
この流れは明確で理解できるものの、そのHandler内で引数として受け取るc *gin.Contextというものが何を指しているのか、どういう役割を持っているのかが当初よくわかっていませんでした。そこで、今回はこのGinにおけるContextについて調べ、どのような場面で利用されるのか確認していきます。

gin.Contextとは

早速コードを読んでいきます。context.goのgin.Context structには以下のようなコメントが記載されていました。

// Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example.

なるほど。フレームワークにおける重要な部分であることは見えました。ただ、それがどのようにコードで表現されているのか実際に見ないとわからないので、ここに記載されている機能を実際に確認していきます。

pass variables between middleware

Ginではログ出力や認証、GZIP圧縮などの基本的な機能をMiddlewareという形で追加利用することができます。
今回はgin.Contextの利用を理解するために、以下のようにBasicAuth Middlewareを通して確認してみます。※下記コードはUsing BasicAuth middlewareに記載されているものです。

func main() {
	r := gin.Default()
	authorized := r.Group("/admin", gin.BasicAuth(gin.Accounts{
		"foo":  "bar",
		"test": "pass",
	}))
	authorized.GET("/secrets", func(c *gin.Context) {
		user := c.MustGet(gin.AuthUserKey).(string)
		c.JSON(http.StatusOK, gin.H{"user": user})
	})
	r.Run(":8080")
}

このコードは、/admin/secretsに対するGETリクエストを受け取ったら、BasicAuth Middlewareを通して認証を行い、認証に成功した場合にはuserというキーで認証ユーザー名を返すというものです。この流れをコード上で追ってみていきます。

1)プログラム実行時にBasicAuthが実行される

2)BasicAuthは下記のように定義されており、BasicAuthForRealmが呼び出される

context.go
func BasicAuth(accounts Accounts) HandlerFunc {
    return BasicAuthForRealm(accounts, "")
}

3)BasicAuthForRealmは下記のように定義されており、ベーシック認証用のアカウントをprocessAccountsで処理しておき、リクエストが来た際に実行されるHandlerを返す(このHandlerはリクエストヘッダーのAuthorizationを取得し、searchCredentialで認証情報を検索。見つかった場合は、c.Set(AuthUserKey, user)で認証ユーザー名をgin.Contextに格納するというもの)

context.go
func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc {
    if realm == "" {
        realm = "Authorization Required"
    }
    realm = "Basic realm=" + strconv.Quote(realm)
    pairs := processAccounts(accounts)
    return func(c *Context) {
        // Search user in the slice of allowed credentials
        user, found := pairs.searchCredential(c.requestHeader("Authorization"))
        if !found {
            // Credentials doesn't match, we return 401 and abort handlers chain.
            c.Header("WWW-Authenticate", realm)
            c.AbortWithStatus(http.StatusUnauthorized)
            return
        }

        // The user credentials was found, set user's id to key AuthUserKey in this context, the user's id can be read later using
        // c.MustGet(gin.AuthUserKey).
        c.Set(AuthUserKey, user)
    }
}

4)実際に/admin/secretsに対するGETリクエストを受け取る

5)/adminに対するアクセス処理が先に行われるので、まずBasicAuthForRealmで返すHandlerが実行される。ここでベーシック認証が正しければ、c.Set(AuthUserKey, user)で認証ユーザー名をgin.Context内部に格納される(c.Setはgin.Contextに値を格納するメソッドで、下記のように定義されている)

context.go
func (c *Context) Set(key string, value any) {
    c.mu.Lock()
    defer c.mu.Unlock()
    if c.Keys == nil {
        c.Keys = make(map[string]any)
    }

    c.Keys[key] = value
}

6)次に/admin/secretsに対するアクセス処理が行われ、c.Set(AuthUserKey, user)でgin.Context内部に格納された認証ユーザー名をc.MustGet(gin.AuthUserKey).(string)にて取り出し、userという変数に格納する(c.MustGetはgin.Contextに格納されている値を取得し、存在しない場合はpanicを発生させるメソッドで、下記のように定義されている)

context.go
// MustGet returns the value for the given key if it exists, otherwise it panics.
func (c *Context) MustGet(key string) any {
    if value, exists := c.Get(key); exists {
        return value
    }
    panic("Key \"" + key + "\" does not exist")
}

7)最後にc.JSON(http.StatusOK, gin.H{"user": user})userをJSON形式で返す

このように、ベーシック認証の処理フローでSetMustGetを活用することで、gin.Contextを介してMiddlewareとHandler間で値の受け渡しが可能であることが確認できました。

manage the flow

Ginのcontext.goのコードを読んでいくと、chainHandlersChainという言葉が出てきました。どうやら、1つのリクエストに対して複数のMiddlewareやHandlerが「チェーン」として順序立てて実行されることを想定しているようです。
gin.Contextではその流れを管理しており、実際にAbortNextといったメソッドが用意されていて、context.goには以下のように記載されています。

context.go
/************************************/
/*********** FLOW CONTROL ***********/
/************************************/

// Next should be used only inside middleware.
// It executes the pending handlers in the chain inside the calling handler.
// See example in GitHub.
func (c *Context) Next() {
	c.index++
	for c.index < int8(len(c.handlers)) {
		c.handlers[c.index](c)
		c.index++
	}
}

// IsAborted returns true if the current context was aborted.
func (c *Context) IsAborted() bool {
	return c.index >= abortIndex
}

// Abort prevents pending handlers from being called. Note that this will not stop the current handler.
// Let's say you have an authorization middleware that validates that the current request is authorized.
// If the authorization fails (ex: the password does not match), call Abort to ensure the remaining handlers
// for this request are not called.
func (c *Context) Abort() {
	c.index = abortIndex
}

// AbortWithStatus calls `Abort()` and writes the headers with the specified status code.
// For example, a failed attempt to authenticate a request could use: context.AbortWithStatus(401).
func (c *Context) AbortWithStatus(code int) {
	c.Status(code)
	c.Writer.WriteHeaderNow()
	c.Abort()
}

// AbortWithStatusJSON calls `Abort()` and then `JSON` internally.
// This method stops the chain, writes the status code and return a JSON body.
// It also sets the Content-Type as "application/json".
func (c *Context) AbortWithStatusJSON(code int, jsonObj any) {
	c.Abort()
	c.JSON(code, jsonObj)
}

// AbortWithError calls `AbortWithStatus()` and `Error()` internally.
// This method stops the chain, writes the status code and pushes the specified error to `c.Errors`.
// See Context.Error() for more details.
func (c *Context) AbortWithError(code int, err error) *Error {
	c.AbortWithStatus(code)
	return c.Error(err)
}

こちらも、下記のようなCustom Middlewareを通して確認してみます。※下記コードはCustom Middlewareに記載されているものです。

func MyLogger() gin.HandlerFunc {
	return func(c *gin.Context) {
		t := time.Now()
		c.Set("example", "12345")

		c.Next()

		latency := time.Since(t)
		log.Print(latency)

		status := c.Writer.Status()
		log.Println(status)
	}
}

func main() {
	r := gin.New()
	r.Use(MyLogger())
	r.GET("/test", func(c *gin.Context) {
		example := c.MustGet("example").(string)
		log.Println(example)
	})
	r.Run(":8080")
}

このコードは、/testに対するGETリクエストを受け取ったら、MyLoggerというMiddlewareを通してリクエストの処理時間をログ出力するというものです。MyLoggerにgin.ContextのNextメソッドが利用されています。同様にこの流れをコード上で追ってみていきます。

1)プログラム実行時にMyLoggerが実行される

2)MyLoggerは先と同様に、Handlerを返す関数として定義されている(このHandlerはリクエストの処理にかかる時間をログ出力するというもので、実行時にtime.Now()で現在時刻を取得し、c.Next()で次のHandlerを実行し、その後にtime.Since(t)で処理にかかった時間を計測している)

3)実際に/testに対するGETリクエストを受け取る

4)MyLoggerで返すHandlerが実行され、現在時刻を取得して、c.Set("example", "12345")でgin.Context内部にexampleというキーで12345という値を格納した後に、c.Next()で処理が一時中断され、次のHandler(/testへのGETリクエストを処理するHandler)を実行する(Nextメソッドは下記のように定義されており、c.index++でインデックスを一つ進めることで、次のHandlerを実行している)

context.go
func (c *Context) Next() {
	c.index++
	for c.index < int8(len(c.handlers)) {
		c.handlers[c.index](c)
		c.index++
	}
}

5)次のHandlerとして/testに対するアクセス処理が行われ、c.MustGet("example").(string)でgin.Context内部に格納されたexampleの値を取り出し、それをログ出力。その後、MyLoggerc.Next()の後続処理が行われ、処理にかかった時間をログ出力

このように、Middlewareとして登録したMyLoggerの返すHandlerと/testに対するGETリクエストを処理するHandlerはgin.ContextのNextメソッドを介して実行順序を制御していることがわかりました。また、先のベーシック認証の例ではBasicAuthForRealm内で、ベーシック認証が失敗した場合に下記を実行しています。

auth.go
user, found := pairs.searchCredential(c.requestHeader("Authorization"))
if !found {
    // Credentials doesn't match, we return 401 and abort handlers chain.
    c.Header("WWW-Authenticate", realm)
    c.AbortWithStatus(http.StatusUnauthorized)
    return
}

これは、認証に失敗したら、401 Unauthorizedを返すとともに、そのリクエストに対する後続の処理を中断するというものです。このように、gin.Contextによって、リクエストの処理フローを中断することも可能であることが確認できました。

validate the JSON of a request

Ginではリクエストボディに対するバリデーションを行うためにShouldBindShouldBindJSONといったメソッドが用意されています。詳しくはModel binding and validationこちらに記載があります。これらのメソッドでは、リクエストBodyを構造体にバインドし、バリデーションエラーが発生した場合には、そのエラーが返されるのでそれを利用してエラーハンドリングを行うことができます。これらのメソッドを利用した例を見てみます。

type Login struct {
	User     string `json:"user" binding:"required"`
	Password string `json:"password" binding:"required"`
}

func main() {
	router := gin.Default()
	router.POST("/loginJSON", func(c *gin.Context) {
		var json Login
		if err := c.ShouldBindJSON(&json); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		if json.User != "manu" || json.Password != "123" {
			c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
			return
		}

		c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
	})
	router.Run(":8080")
}

このコードは、/loginJSONに対するPOSTリクエストを受け取ったら、リクエストボディをLogin構造体にバインドし、バリデーションエラーが発生した場合には、そのエラーをJSON形式で返すというものです。同様にこの流れをコード上で追ってみていきます。

1)/loginJSONに対するPOSTリクエストを受け取る

2)ShouldBindJSONでリクエストボディをLogin構造体にバインドし、バリデーションエラーが発生した場合には、そのエラーをJSON形式で返す(ShouldBindJSONは下記のように定義されており、その内部では、bindingパッケージのBindメソッドを利用している)

context.go
func (c *Context) ShouldBindWith(obj any, b binding.Binding) error {
	return b.Bind(c.Request, obj)
}

// 間省略

func (c *Context) ShouldBindJSON(obj any) error {
	return c.ShouldBindWith(obj, binding.JSON)
}

3)バリデーションエラーが発生しなかった場合には、json.User != "manu" || json.Password != "123"でユーザー名とパスワードがmanu123でない場合には、401 Unauthorizedを返し、それ以外の場合には200 OKを返す

このように、gin.ContextにあるShouldBindJSONを利用することで、リクエストボディのバリデーションを行うことができることが確認できました。Bindに関しては、Gin自体で実施されているのではなく、bindingパッケージを利用しているので、そちらを別途利用しても問題なさそうですが、そこをラップして使いやすい形式で提供しているところがWebフレームワークらしさを感じます。そして、そこの利便性を提供するためにgin.Contextが役に立っているということが理解できました。

render a JSON response

JSONレスポンスの出力については、今までの例でもc.JSONとして登場してきており、レスポンスを返す際には頻出すると思われます。その内部は以下のように定義されています。

context.go
// Render writes the response headers and calls render.Render to render data.
func (c *Context) Render(code int, r render.Render) {
	c.Status(code)

	if !bodyAllowedForStatus(code) {
		r.WriteContentType(c.Writer)
		c.Writer.WriteHeaderNow()
		return
	}

	if err := r.Render(c.Writer); err != nil {
		// Pushing error to c.Errors
		_ = c.Error(err)
		c.Abort()
	}
}

// 間省略

// JSON serializes the given struct as JSON into the response body.
// It also sets the Content-Type as "application/json".
func (c *Context) JSON(code int, obj any) {
	c.Render(code, render.JSON{Data: obj})
}

こちらも、先のShouldBindJSONと同様に、外部パッケージとしてrenderパッケージのRenderWriteContentTypeを利用していることがわかります。renderパッケージのREADMEを読むと、下記のように記載されていました。

Render can be used with pretty much any web framework providing you can access the http.ResponseWriter from your handler. The rendering functions simply wraps Go's existing functionality for marshaling and rendering data.

つまり、Ginでも同じくRenderをラップして利用可能な状態にしてあり、それがgin.Contextを介して行われていることがわかりました。

改めてgin.Contextとは

gin.Contextの役割をコードを追っていくことで少し理解することができました。

  • MiddlewareやHandler間で共通して扱える情報格納領域として機能し、SetGetMustGetなどのメソッドを介してデータの受け渡しが可能
  • リクエスト処理の実行フローを制御するためのインデックス管理やNextAbortAbortWithStatusなどのメソッドを持ち、HandlersChain上の実行制御が可能
  • ShouldBindJSONなどのメソッドにより、リクエストボディをGoの構造体にバインドし、バリデーションやエラーハンドリングを行うことが可能
  • JSONHTMLXMLなどのレスポンス生成用メソッドを通して、レスポンスの返却を簡潔に行うことが可能

総じてまとめると、gin.ContextはGinのリクエスト-レスポンス処理サイクル全体を通して、データの受け渡し、フロー制御、リクエスト/レスポンスハンドリングを一元的に担う役割を持っており、開発者は基本的にこのgin.Contextにあるメソッドを利用することで、ある程度のサポートがGinから受けられるのかと思います。

gin.Contextで提供されていること

上記で調べたのは、gin.Contextの一部です。context.goでは下記のような分類がコメントによって行われていました。全てを利用する必要はないですが、どのような機能が利用可能かを知っておくだけでも効果はあると思います。

  • FLOW CONTROL
    • 上述した通り、NextAbortなどのメソッドによる、HandlerChainの制御
  • ERROR MANAGEMENT
    • 発生したエラーを現在のContextに紐づけるためのErrorメソッドを提供
  • METADATA MANAGEMENT
    • SetGetMustGetメソッドによる、Context内部のデータ管理
  • INPUT DATA
    • ParamsQueryなどのリクエストパラメータの取得、ShouldBindShouldBindJSONによるリクエストボディのバインド、ClientIPrequestHeaderなどのリクエスト情報の取得
  • RESPONSE RENDERING
    • CookieSetCookieによるクッキーの操作、JSONHTMLXMLなどのレスポンス生成
  • CONTENT NEGOTIATION
    • コンテンツネゴシエーションのサポート(あまり利用例を見ない)
  • GOLANG.ORG/X/NET/CONTEXT
    • Golangのcontext.Contextインターフェースへの実装

まとめ

  • gin.Contextについて自分の中での理解を深めました
  • gin.Contextについて記述があるcontext.goは、1200行と比較的短く、コメントも多く記載されているので理解しやすかったです
  • コードを追いかけて確認することで、gin.Contextに対する理解がある程度深まりました。これまで、Webフレームワークの学習といえば「TODOアプリやBBSを作って実践的に身につける」もしくは「ReferenceのExamplesを眺めて実行してみる」といった手法が主流でした。しかし、今回のように特定の機能(gin.Context)の役割をコードベースで丁寧に追っていく手法は、また違った学び方として有効性を感じました。従来、Gin Examplesを見る際は、自分の使いたい機能に近いサンプルを探す見方が中心でしたが、今後は「この機能がシステムの中でどう活用されているのか」という視点で読み解くこともできるようになったと思います

おわりに

今回はGinのContextについて調べてみましたが、他のGolangのWebフレームワーク(echo / Fiber / Humaなど)にも同様のContextが存在します。それらとの比較やそれぞれの特徴を調べてみると各フレームワークへの思想が見えたりとさらに深い理解が得られるのかなと思います。

今後もGinやGolangの学習を続け、バックエンド開発のスキルを磨いていきたいと思います!

1
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
1
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?