七転八起

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

マルチプレクサにhttprouterを使用する

前回の記事の続きです。

ここまでサードパーティライブラリはgo-pg/pgしか使用しておらず、HTTPを捌くのにnet/httpしか使っていませんでした。『Goプログラミング実践入門 標準ライブラリでゼロからWebアプリを作る』で紹介されていたhttprouterがGoでルーティングを捌くのに良さそうだったので、試してみました。

マルチプレクサのライブラリ

上記の書籍にはhttprouterのみ紹介されていましたが、net/httpの流儀に従いつつ、ルーティングを柔軟に実装できるライブラリとして、httprouter以外にgorilla/muxがあったので、両者について簡単に調べてみた。

gorilla/muxはnet/httpのhttp.HandlerFuncをそのまま使えます。なので、元々net/httpで実装していたハンドラを変更しなくても、ルーティングの実装を変更することができます。特徴の一つとして、パスパターンに正規表現を使用できることがあげられます。パスに不適切な値が入ってきたときに、それをルーティングでフィルタできるのは良いですね。

httprouterはハンドラの登録にGETPOSTといったHTTPメソッドと同じ名前の関数を使用することができます。同じパスでもメソッドによってハンドラを切り替えることができ、また、それをわかりやすく明示できるのはよいですね(gorilla/muxでも同じようなことができるようですが)。gorilla/muxとは異なり、正規表現によるフィルタはできないようです。

パフォーマンスについてはこのようなベンチマークがありました(これを作っているのはhttprouterの作者なので、多少ポジショントーク的な部分はありそうですが笑)。パフォーマンスはhttprouterの方がいいようです。今回はhttprouterを使っていきます。

元のコード

少し長いですが、下記のような状態になっています。httprouterを使用するためにはtasksHandlerの中でHTTPメソッドごとに分けている処理をハンドラにする必要があり、それによりtasksHandlerを削除できるようになります。

// 省略

func tasksHandler(w http.ResponseWriter, r *http.Request) {
    db := getDatabase()
    defer func() {
        if err := db.Close(); err != nil {
            log.Println(err)
        }
    }()

    m := validPath.FindStringSubmatch(r.URL.Path)
    if m == nil {
        http.NotFound(w, r)
        return
    }

  // この部分をなんとかしたい
    switch r.Method {
    case "GET":
        getTasks(w, db)
    case "POST":
        createTask(w, r, db)
    case "PUT":
        updateTask(r, m, db)
    case "DELETE":
        deleteTask(m, db)
    }
}

func getTasks(w http.ResponseWriter, db *pg.DB) {
    var tasks []Task
    if err := db.Model(&tasks).Select(); err != nil {
        panic(err)
    }

    tasksJson, err := json.Marshal(tasks)
    if err != nil {
        panic(err)
    }

    if _, err = w.Write(tasksJson); err != nil {
        panic(err)
    }
}

func createTask(w http.ResponseWriter, r *http.Request, db *pg.DB) {
    var task Task
    task.parseBody(r)
    if _, err := db.Model(&task).Insert(); err != nil {
        panic(err)
    }

    taskJson, _ := json.Marshal(task)
    if _, err := w.Write(taskJson); err != nil {
        panic(err)
    }
}

func updateTask(r *http.Request, m []string, db *pg.DB) {
    id, _ := strconv.Atoi(m[2])
    task := new(Task)
    task.find(db, uint(id))

    task.parseBody(r)
    if _, err := db.Model(task).WherePK().Update(); err != nil {
        log.Println(err)
    }
}

func deleteTask(m []string, db *pg.DB) {
    id, _ := strconv.Atoi(m[2])
    task := new(Task)
    task.find(db, uint(id))
    if _, err := db.Model(task).WherePK().Delete(); err != nil {
        log.Println(err)
    }
}

// 省略

func main() {
  // この部分をhttprouterで書き換える
    http.HandleFunc("/api/tasks/", accessLog(tasksHandler))
    log.Fatal(http.ListenAndServe(":5000", nil))
}

修正の手順

1. 各処理をhttp.HandlerFuncにする

現状、各処理の関数の引数はバラバラで、各処理に必要な値のみを渡すようにしています。これらの関数をhttp.HandlerFuncの型にして、http.HandleFuncで登録できるようにします。httprouterを使用する場合はhttprouter.Handleという別の型のハンドラ関数にする必要があるのですが、パスパラメータが引数に追加されるだけなので、一旦http.HandlerFuncにします。また、この過程でコードの重複が生まれてしまいますが、そこも一旦目を瞑って進めていきます。この後、各処理をハンドラとして扱うことになるので、関数名も...Handlerという名称に変えています。

// 省略

func tasksHandler(w http.ResponseWriter, r *http.Request) {
  // DBやパスに関する処理を各関数に移行する
  // 引数を(w http.ResponseWriter, r *http.Request)の型に揃える
    switch r.Method {
    case "GET":
        getTasksHandler(w, r)
    case "POST":
        createTaskHandler(w, r)
    case "PUT":
        updateTaskHandler(w, r)
    case "DELETE":
        deleteTaskHandler(w, r)
    }
}

func getTasksHandler(w http.ResponseWriter, _ *http.Request) {
    db := getDatabase()
    defer func() {
        if err := db.Close(); err != nil {
            log.Println(err)
        }
    }()

    var tasks []Task
    if err := db.Model(&tasks).Select(); err != nil {
        panic(err)
    }

    tasksJson, err := json.Marshal(tasks)
    if err != nil {
        panic(err)
    }

    if _, err = w.Write(tasksJson); err != nil {
        panic(err)
    }
}

func createTaskHandler(w http.ResponseWriter, r *http.Request) {
    db := getDatabase()
    defer func() {
        if err := db.Close(); err != nil {
            log.Println(err)
        }
    }()

    var task Task
    task.parseBody(r)
    if _, err := db.Model(&task).Insert(); err != nil {
        panic(err)
    }

    taskJson, _ := json.Marshal(task)
    if _, err := w.Write(taskJson); err != nil {
        panic(err)
    }
}

func updateTaskHandler(w http.ResponseWriter, r *http.Request) {
    db := getDatabase()
    defer func() {
        if err := db.Close(); err != nil {
            log.Println(err)
        }
    }()

    m := validPath.FindStringSubmatch(r.URL.Path)
    if m == nil {
        http.NotFound(w, r)
        return
    }

    id, _ := strconv.Atoi(m[2])
    task := new(Task)
    task.find(db, uint(id))

    task.parseBody(r)
    if _, err := db.Model(task).WherePK().Update(); err != nil {
        log.Println(err)
    }
}

func deleteTaskHandler(w http.ResponseWriter, r *http.Request) {
    db := getDatabase()
    defer func() {
        if err := db.Close(); err != nil {
            log.Println(err)
        }
    }()

    m := validPath.FindStringSubmatch(r.URL.Path)
    if m == nil {
        http.NotFound(w, r)
        return
    }

    id, _ := strconv.Atoi(m[2])
    task := new(Task)
    task.find(db, uint(id))
    if _, err := db.Model(task).WherePK().Delete(); err != nil {
        log.Println(err)
    }
}

// 省略

2. httprouterのルーターを使用する

次にmainの内部で行っているルーティングの処理をhttprouterで書き換えていきます。この際に、1で書き換えた関数の引数に_ httprouter.Paramsを追記する必要があります。この引数は後々使用しますが、一旦全てこの形にします。またこれにより、tasksHanlderは削除することができました。

// 省略
// tasksHandlerは削除する

// 引数にhttprouter.Paramsを追加
func getTasksHandler(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
  // 省略
}

func createTaskHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    // 省略
}

func updateTaskHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    // 省略
}

func deleteTaskHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    // 省略
}

// 省略

func main() {
    router := httprouter.New()

    // /tasks
    router.GET("/api/tasks", accessLog(getTasksHandler))
    router.POST("/api/tasks", accessLog(createTaskHandler))
    router.PUT("/api/tasks/:id", accessLog(updateTaskHandler))
    router.DELETE("/api/tasks/:id", accessLog(deleteTaskHandler))

    log.Fatal(http.ListenAndServe(":5000", router))
}

3. PUT/DELETEでのidの処理にhttprouter.Paramsを使用する

これまでパスに含まれるTaskのidを正規表現を使用して抽出していましたが、各ハンドラの引数にhttprouter.Paramsが入ったので、それを使用すれば簡単にパスパラメータを処理できるようになります。2でPUT/DELETEのパスに:idが含まれていますが、これを抽出できるようになります。

// 省略

func updateTaskHandler(_ http.ResponseWriter, r *http.Request, p httprouter.Params) {
    // 省略

  // p.Bynameを使用して、パスパラメータを取得
    id, _ := strconv.Atoi(p.ByName("id"))
    task := new(Task)
    task.find(db, uint(id))

  // 省略
}

func deleteTaskHandler(_ http.ResponseWriter, _ *http.Request, p httprouter.Params) {
    // 省略

    id, _ := strconv.Atoi(p.ByName("id"))
    task := new(Task)
    task.find(db, uint(id))

    // 省略
}

// 省略

最終的なコードは下記になります。

https://github.com/kensei18/sample-todo-golang-vue/blob/31a8a4428796df17588804cdc9f9df9f1ea3a20e/backend/main.go

最後に

httprouterを使用することでルーティングが明示的になり、可読性が増したように感じます。ただ、DBに関する処理など、重複したコードが現れたので、その辺りの修正を今後進めていきたいです。