99
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

GolangのWebフレームワークginのmiddlewareについての覚書

前置き

最近Golangを用いたWebAPIを開発しています。
Webフレームワークにはginを利用しているのですが、そのginにおけるmiddlewareについてなかなかまとまった資料などが見当たらなかったため、実際に動かしながら確認した動きについてまとめておこうと思います。

バージョンなど

  • Golang:1.10.1
  • gin:1.2

基本動作

サンプルソース

ginにおけるmiddlewareの挙動を理解するために以下のようなソースを用意します。
localhost:8080にアクセスした時にmessageを持つ構造を返しています。
またsampleMiddleware()をmiddlewareとして指定しています。

package main

import (
    "log"

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

func main() {
    r := gin.Default()
    r.Use(sampleMiddleware())
    r.GET("/", func(c *gin.Context) {
        log.Println("main logic")
        c.JSON(200, gin.H{"message": "Hello!"})
    })
    r.Run()
}

func sampleMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        log.Println("before logic")
        c.Next()
        log.Println("after logic")
    }
}

挙動

上記ファイルを用意して、

go run main.go
curl localhost:8080

とコマンドを打つと、以下のようなレスポンスが返ってきます。

{"message":"Hello!"}

そしてこの時のginのログは以下のように出力されています。

before logic
main logic
after logic
[GIN] 2018/05/20 - 21:14:48 | 200 |      89.316µs |             ::1 | GET      /

説明

このログ出力から分かることは、middlewareとして指定したメソッドにおいて、

  • c.Next()の前に記述した処理はルーティング内の処理の前に実行される
  • c.Next()の後に記述した処理はルーティング内の処理の後に実行される

ということです。
これで共通の処理を前後にそれぞれ挟み込むことができるわけです。

middlewareの適用の仕方について

先の例では以下のようにmiddlewareを使用しました。

r.Use(sampleMiddleware())

全てのルーティングに共通して処理させたい場合は問題ないのですが、特定のルーティングの時だけmiddlewareを呼びたいこともあるでしょう。
そんな時は以下のようにもmiddlewareを使うこともできます。

サンプルソース

func main() {
    r := gin.Default()
    r.GET("/", func(c *gin.Context) {
        log.Println("main logic")
        c.JSON(200, gin.H{"message": "Hello!"})
    })
    r.GET("/a", sampleMiddleware(), func(c *gin.Context) {
        log.Println("main logic")
        c.JSON(200, gin.H{"message": "Welcome!"})
    })
    r.Run()
}
// 省略

 挙動

上記ソースを動かして、/と/aにアクセスするとginのログは以下のように出力されます。/aにアクセスした時だけmiddlewareの処理が実行されていることが分かると思います。

main logic
[GIN] 2018/05/20 - 21:26:10 | 200 |       300.4µs |             ::1 | GET      /
before logic
main logic
after logic
[GIN] 2018/05/20 - 21:26:20 | 200 |     251.709µs |             ::1 | GET      /a

 Groupを利用したmiddlewareの適用

特定のルーティングにおいてmiddlewareを実行したいという時、上で説明したやり方で可能ですが、その特定のルーティングが複数ある場合はGroupルーティングを使用した方法が適切です。ログイン済みユーザーのみがアクセスできるルートを用意するのに便利です。

func main() {
    r := gin.Default()
    sampleGroup := r.Group("/")
    sampleGroup.Use(sampleMiddleware())
    {
        sampleGroup.GET("/b", func(c *gin.Context) {
            c.JSON(200, gin.H{"message": "in group1"})
        })
        sampleGroup.GET("/c", func(c *gin.Context) {
            c.JSON(200, gin.H{"message": "in group2"})
        })
    }
    r.GET("/", func(c *gin.Context) {
        log.Println("main logic")
        c.JSON(200, gin.H{"message": "Hello!"})
    })
    r.Run()
}

このように記述することで、/bまたは/cにアクセスされた時にのみmiddlewareを実行することが可能になります。
ちなみにmiddlewareを適用するには以下のように書くことも可能です。

sampleGroup := r.Group("/", sampleMiddleware())
{
    sampleGroup.GET("/b", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "in group1"})
    })
    sampleGroup.GET("/c", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "in group2"})
    })
}

処理をmiddleware内で終了させたい

例えば認証処理などをmiddlewareにて行い、もし不適切なリクエストであった場合、エラーレスポンスを返したいとして以下のような記述をしたとします。

package main

import (
    "log"
    "strconv"

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

func main() {
    r := gin.Default()
    r.Use(sampleMiddleware())
    r.GET("/:id", func(c *gin.Context) {
        log.Println("main logic")
        c.JSON(200, gin.H{"message": "Hello!"})
    })
    r.Run()
}

func sampleMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        id, _ := strconv.Atoi(c.Param("id"))
        if id == 0 {
            c.JSON(400, gin.H{"message": "invalid id"})
        }
    }
}

しかしこのような書き方だと、localhost:8080/0にアクセスした際のレスポンスは以下のようになります。

{"message":"invalid id"}{"message":"Hello!"}

要するにエラーコードとエラーメッセージを返しながらも以降の処理が継続されていることになります。
ログイン済みのユーザーにだけコンテンツを見せたい時などはこれでは困ります。
そういった場合はAbortを使うことで対処できます。

func sampleMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        id, _ := strconv.Atoi(c.Param("id"))
        if id == 0 {
            c.JSON(400, gin.H{"message": "invalid id"})
            c.Abort()
        }
    }
}

この場合、localhost:8080/0にアクセスすると、エラーメッセージのみが返ってきます。

{"message":"invalid id"}

Abortを使うことによって、middlewareにおいて以降の処理を通すことなくレスポンスを返すことができるようになります。

まとめ

お疲れ様でした。Golangはやはり日本語資料が少なく、基本的な機能の実装においても調査の時間が取られてしまうことが多いですね。頑張りましょう。
記事内に不適切・不十分な説明がございましたら、ご指摘いただけますと助かります。
閲覧いただきありがとうございました。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
99
Help us understand the problem. What are the problem?