🗂

【Poteto】GolangでWebフレームワークはじめ②【ミドルウェアグループ】

2024/11/03に公開

はじめに

  • 前回の記事

https://zenn.dev/poteto0/articles/ce2b70600bc418

  • レポジトリ

https://github.com/poteto0/poteto

作り始めたWebフレームワーク(Poteto)の機能が充実してきて、簡単なWebAPIを実装できるようになりました(ドキュメントは追いついていないですが、、、)。
今回は、開発の自由度を高めるために、Middlewareグループを導入します。
いつも通り、Echoを参考にしていますが、設計思想は違います。

  • Echo

https://github.com/labstack/echo/tree/master

*この記事に乗ってるコードのいくつかは、特に動作確認していないので、雰囲気を味わうイメージでお願いします。。。

middlewareGroupとは

middlewareGroupは、各ルーティングをまとめて、ミドルウェアを適用させる機能です。例えば、Echoの例を見てみましょう。

func main() {
  e := echo.New()
  admin := e.Group("/admin")
  admin.Use(echojwt.WithConfig(config))
  admin.GET("/check_admin", CheckAdmin)
  e.Run()
}

これを設定することで、/admin/*に対して、jwtWithConfigミドルウェアを適用しています。
これを自作フレームワークに実装することが目標です。

Echoの実装を見る

EchoではGroupEchoへのポインタを持ちます。

type Group struct {
	...
	echo       *Echo
	middleware []MiddlewareFunc
}

また、EchoGroup()によってmiddlewareGroupを登録しています。

func (e *Echo) Group(prefix string, m ...MiddlewareFunc) (g *Group) {
	g = &Group{prefix: prefix, echo: e}
	g.Use(m...)
	return
}

GroupEchoへのポインタを持つおかげで、開発者は、適用されるミドルウェアを意識しながらルーティングを構成することができます。これはすごく良いですよね。

func main() {
  e := echo.New()
  admin := e.Group("/admin")
  admin.Use(echojwt.WithConfig(config))
  admin.GET("/check_admin", CheckAdmin) // Groupにルーティング追加するイメージ
  e.Run()
}

ただ逆にフレームワーク側の実装はやや複雑になってしまっています。Echoには、そもそも全てのルーティングに対してミドルウェアを適用することができます。

func main() {
  e := echo.New()
  e.Use(<some middlewares>)
  e.Run()

このミドルウェアはどこに入るかというと、Echoのメンバになります。

type Echo struct {
  ...
  middleware    []MiddlewareFunc
  ...
}

そのため、全体に適応しているミドルウェアはEchoにあり、グループごとに適応するミドルウェアはGroupに格納されます。もちろんこの複雑な設計がキレイに実装されているので、開発者フレンドリーな作りと言えますね。ただ、私はここをあきらめてシンプルな設計にしました。

Potetoでの実装

PotetoではPotetoMiddlewareGroupを持ちます。つまりPotetoは全てのルートに適用するミドルウェアを持っていないわけですね。

type poteto struct {
  ...
  middlewareGroup MiddlewareGroup
}
type middlewareGroup struct {
  children    map[string]MiddlewareGroup
  middlewares []MiddlewareFunc
  key         string
}

では、ミドルウェアを全てのルーティングに適用させるときはどうしているかというと、空文字のミドルウェアグループを作成することで対応しています。これによって、開発者はGroupを意識することなく開発を行うことができます。

func (p *poteto) Register(middlewares ...MiddlewareFunc) {
  p.middlewareGroup.Insert("", middlewares...)
}

ミドルウェア自体は前回記事のルーティングと同様にTrie木を用いているので、結局木のトップノードにミドルウェアグループが出来ている訳ですね。
取り出すときには、以下のようにトップパスから見つかったミドルウェアを全て返しています。

func (mg *middlewareGroup) SearchMiddlewares(pattern string) []MiddlewareFunc {
  middlewares := []MiddlewareFunc{}
  currentNode := mg
  middlewares = append(middlewares, mg.middlewares...)
  patterns := strings.Split(pattern, "/")

  for _, p := range patterns {
    if p == "" {
      continue
    }

    if nextNode, ok := currentNode.children[p]; ok {
      currentNode = nextNode.(*middlewareGroup)
      middlewares = append(middlewares, currentNode.middlewares...)
    } else {
      ...
	}
	return middlewares
}

実装をシンプルにしたことによる弊害

開発者はミドルウェアグループのことを考えながらルーティングを作成する必要が出来てしまいました。

func main() {
  p := poteto.New()
  admin := p.Combine("/admin")
  admin.Use(echojwt.WithConfig(config))
  p.GET("/admin/check_admin", CheckAdmin) // Groupにルーティング追加するイメージ
  p.Run()
}

ただ、ここはオリジナリティとして(笑)、フレームワーク自体のシンプルさを優先しました。

終わりに

今回はミドルウェアグループについての話でした。ぜひ興味出たら動かしてみていただけるとありがたいです。

検討

middlewareGroupという名前ですが、探索中に見つかった全てのミドルウェアを返しているため、名前を変えることも検討しています。またはtop以外は、自身のしか返さないとかでしょうか。

Discussion