GolangでWebフレームワークはじめ
はじめに
WebAPIを開発するときにWebフレームワークを使うというのが当たり前になってきたと思います(当たり前になる前を知らない)。私は最近Go言語のEchoというフレームワークが気に入っています。とてもシンプルな作りに心を掴まれました。そこである日思ったんですよね、Webフレームワークってどういう中身になってるんだろうと。気になってEchoを真似してWebフレームワークの開発を始めました。
- Echo
- こちらにも影響を受けました
実現したい機能
まずは最低限のWebAPIを目指していきます・・・
- ルーティング
- Jsonのレスポンス
- CORS
ルーティング
今回初めて知ったんですが、ルーティングってトライ木で実装されてるんですね。競プロでしか聞かないので、驚きました。トライ木は簡単に言うと、文字ごとにノードに格納して、いくつかの文字列を木構造にしたデータ構造です。それによって、特に同じような文字列が多いときに最もいい結果を産みます(多分)。このようにしてルーティングのパフォーマンスを上げてたんですね~。
- トライ木については
実装
WebAPIではある程度エンドポイントが共通してきますよね、
users/create
users/update
users/:id
...
なので、大きな効果があるわけです。そして通常のトライ木とは違い、パスの階層でノードを作っていきます。
example → users → create
↳ update
↳ hello
ということで各ルートのメンバはこんな形になります。handler
はそれぞれのエンドポイントに紐づいた関数です。children
が次のroute
を参照し、木構造を形成します。
type route struct {
key string
method string
children map[string]Route
handler HandlerFunc
}
検索
リクエストがあったエンドポイントを検索します。最も大きい階層から、順々に階層をたどっていきます
func (r *route) Search(path string) *route {
currentRoute := r
params := strings.Split(path, "/")
for _, param := range params {
if param == "" {
continue
}
// 子供を辿っていく
if nextRoute, ok := currentRoute.children[param]; ok {
currentRoute = nextRoute.(*route)
} else {
return nil
}
}
return currentRoute
}
挿入
ほぼ検索とおなじですが、子を辿っていき、無くなったらそこに新たなRouteを挿入します
func (r *route) Insert(method, path string, handler HandlerFunc) {
currentRoute := r
params := strings.Split(path, "/")
for _, param := range params {
if param == "" {
continue
}
if nextRoute := currentRoute.children[param]; nextRoute == nil {
// 無くなったら新しく作る
currentRoute.children[param] = &route{
key: param,
method: method,
children: make(map[string]Route),
}
}
// 子を辿っていく
currentRoute = currentRoute.children[param].(*route)
}
currentRoute.handler = handler
}
これでルーティングの機能を実装することができました。次に、リクエストを送ったときに、Jsonのレスポンスを返したいですよね?
Json型レスポンス
まずはコンテキストが必要になってきます。理解はまだまだ不十分ですが、今の私の認識ではリクエストとレスポンスを持ったもの、といった感じです。つまり、リクエストを追記する必要があるわけですね。Goには非常に使いやすいnet/http
パッケージがあるので、それを活用しましょう。
type context struct {
response Response
request *http.Request
path string
}
ここでJson型のレスポンスを書きたければ、こうすればいいのです。便利ですね。
func (ctx *context) JSON(code int, value any) error {
ctx.writeContentType(constant.APPLICATION_JSON)
ctx.response.SetStatus(code)
return ctx.JsonSerialize(value)
}
func (ctx *context) writeContentType(value string) {
header := ctx.response.Header()
if header.Get(constant.HEADER_CONTENT_TYPE) == "" {
header.Set(constant.HEADER_CONTENT_TYPE, value)
}
}
func (ctx *context) JsonSerialize(value any) error {
encoder := json.NewEncoder(ctx.GetResponse())
return encoder.Encode(value)
}
簡単な実装ですが、コンテキストを受け取って、リレーして返す様子っていうのが今一度実感できたのがよかったです。さあこれでWebAPIが出来ました、と見せかけて、クライアントアプリから呼ぼうとするとCORSポリシーに引っ掛かってしまいます。みな最初に思う、これ何やねんです。こうしないとどこもかしこもレスポンスを返してしまうので、必要というかWebフレームワークとしてはこれに対応しないとほぼ何もできません(余談ですが、CORSポリシーってあくまでリクエストは通ってるらしいですね。そのため防御になっているわけではないとか)。
CORS対応
CORSに対応するには、Access-Control-Allow-Origin
ヘッダにクライアントを登録する必要があります。初めに言いますが、ここの実装はほぼEcho
の写経です。でもいい勉強にはなりました。
type CORSConfig struct {
// Originを登録します ex) http://localhost:*
AllowOrigins []string `yaml:"allow_origins
// HTTPメソッドを登録します
AllowMethods []string `yaml:"allow_methods"`
}
func CORSWithConfig(config CORSConfig) poteto.MiddlewareFunc {
...
// 正規表現に対応します。よくあるAccess-Control-Origin: * とかですね
allowOriginPatterns := []string{}
for _, origin := range config.AllowOrigins {
pattern := wrapRegExp(origin)
allowOriginPatterns = append(allowOriginPatterns, pattern)
}
return func(next poteto.HandlerFunc) poteto.HandlerFunc {
return func(ctx poteto.Context) error {
...
origin := req.Header.Get(constant.HEADER_ORIGIN)
res.Header().Add(constant.HEADER_VARY, constant.HEADER_ORIGIN)
preflight := req.Method == http.MethodOptions
...
// 許可されたOriginと照らし合わせます
allowSubDomain := getAllowSubDomain(origin, config.AllowOrigins)
allowOrigin := getAllowOrigin(allowSubDomain, allowOriginPatterns)
// Origin not allowed
if allowOrigin == "" {
...
return ctx.NoContent()
}
// allowed method
if matchMethod(req.Method, config.AllowMethods) {
// レスポンスを返す前に、CORSヘッダーを追加して終了です
res.Header().Set(constant.HEADER_ACCESS_CONTROL_ORIGIN, allowOrigin)
return next(ctx)
}
return ctx.NoContent()
}
}
}
これでほんとに最低限の機能が整いました。あとはサーブしておしまい。
func (p *poteto) Run(addr string) {
if err := http.ListenAndServe(addr, p); err != nil {
panic(err)
}
}
にしても便利ですねnet/http
は。
今後やりたいこと
- JWTミドルウェア
- パラメータを扱う
今後も気長に開発していきます。
最後に
普段使っている道具の中身を見ることができるので、Webフレームワーク開発おすすめです。Goって人が書いたコードも結構すんなり読めるので、私はGolangでやってみるのが良いんじゃないかなーと激推ししてます。
Discussion