七転八起

技術的に気になったことや詰まったことをメモ替わりに書いていきます。

ハンドラとロギングの分離

前回の記事で初めてGoでWebアプリケーションを作成しましたが、コードがかなりごちゃごちゃしているので、いい感じにリファクタリングできないか、試行錯誤していこうと思います。前回のコードの実装が終わって、しばらくGoから離れていたのですが、『Goプログラミング実践入門 標準ライブラリでゼロからWebアプリを作る』を読み始めて、net/httpを使用したWebアプリケーションのより良い実装方法がわかってきたので、少しずつ試していきたいと思います。

元のコード

下記のように、ハンドラ関数の中にアクセスログのコードを記述していました。

// 省略

// ハンドラ関数
func tasksHandler(w http.ResponseWriter, r *http.Request) {
    // ex) 2021/06/03 19:11:56 GET /api/tasks/
  log.Printf("%v %v", r.Method, r.URL.Path)

  /*
  リクエストを処理する汚いコード
  今後整理したい
  */
}

// 省略

func main() {
    http.HandleFunc("/api/tasks/", tasksHandler)
    log.Fatal(http.ListenAndServe(":5000", nil))
}

このアプリケーションはリソースが1つしかない単純なものなので、上記のようにハンドラ関数の中にアクセスログのロジックを記述したとしても大して問題ありませんが、リソースがさらに増えて、かつ全てのエンドポイントに共通のログを出力させたいとなったときには、各ハンドラ関数にlog.Printf("%v %v", r.Method, r.URL.Path)を記述する必要があり、重複するコードが増えて非常に不便です。上手いやり方ないかな、と思っていましたが、先述の書籍の『3.3.4 ハンドラとハンドラ関数のチェイン』にこれを解決する方法がガッツリ丁寧に載っていました。

修正後のコード

考え方については後回しにして、ひとまずコードを書き換えます。

// 省略

// ハンドラ関数
func tasksHandler(w http.ResponseWriter, r *http.Request) {
  // accessLogに移行

  /*
  リクエストを処理する汚いコード
  今後整理したい
  */
}

// 省略

func accessLog(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        log.Printf("%v %v", r.Method, r.URL.Path)
        h(w, r)
    }
}

func main() {
  // ハンドラ関数をaccessLogでチェインにする
    http.HandleFunc("/api/tasks/", accessLog(tasksHandler))
    log.Fatal(http.ListenAndServe(":5000", nil))
}

ログ出力に関するロジックをtasksHandlerから分離して、accessLogという関数に移しています。accessLogはハンドラ関数を引数にとって、ハンドラ関数を返します。内部では無名関数のハンドラ関数を戻り値としていて、その内部ではログ出力をした後に引数としていたハンドラ関数を実行する、という形になっています。実際に書いてみると、どうって事のないシンプルなコードになりました。

メインの処理の前後に別の処理を行うための関数なので、Pythonデコレータと役割的には同じような印象を受けました。ただ、Pythonのデコレータの場合は、デコレータの概念や記法を知らないと書くことができないのはもちろん、人のコードを読んでも何をやっているのかわかりません。そして、@を使った記法はググラビリティが非常に悪いので、「デコレータ」という名称を知らないと、その意味を調べることもできません(体験談)。一方で、上記のように関数をチェインさせる方法は、基本的なプログラミングの考え方を理解している人であれば、多少Goの知識が不足していたとしても十分理解できる、と感じました。もちろんPythonでもデコレータを使わずに記述することは可能ですが、Pythonは複数行にわたって無名関数を使用することはできないので、関数内関数を作り、その関数名をつけて、その内部に処理を書いて、それを外側の関数で返す、というステップを踏まないといけないです(デコレータ使う場合でも同じことをしないといけない)。それと比べると、同じことをやっていても、Goの方がスッキリしているかな、という印象です。

最後に

net/httpにはデフォルトでのログ出力のロジックがないので、自分で実装しないといけないというのは、Ruby on Rails / FastAPIでの開発しかやってきていない身としては新鮮でした。改めてRailsやFastAPIの利便性を認識できたと同時に、その辺の実装を自分でやれば、後からの変更も自分でできるので、柔軟性はこっちの方が高いかな、とも思いました(どっちをとっても面倒はあるはずですが笑)。