go-chi/chiの動作確認時に最初に書くコード

個人的には、細々としたコードの動作確認のために小さな1ファイルのコードを書くということを常々やっている。頭があまり良い方ではないので、ドキュメントを読んだだけでは実際の動作を正確に理解することができない。加えて微妙な差異を追加しての比較検討なしに違いを把握することができない機能があったりもする。

go-chi

個人的にgoでのweb APIの実装を試すときにはgo-chi/chiを使っている。

github.com

動作確認のための最初のコードをここにメモしておくことにする。

READMEのコードは少し複雑すぎる

readmeの最初のコードは簡潔すぎるし、発展的な例は少し複雑すぎる気がする。提供されている細々とした機能を試すのにこれらを全部コピペというのはあまり嬉しくない。

最初に書くコード

個人的にはこれくらいからはじめていくのが良いと思っている。web APIjsonを返しておきたい。

package main

import (
    "log"
    "net/http"
    "os"

    "github.com/go-chi/chi"
    "github.com/go-chi/chi/middleware"
    "github.com/go-chi/render"
)

func main() {
    r := chi.NewRouter()
    r.Use(middleware.Logger)

    r.Get("/api", func(w http.ResponseWriter, r *http.Request) {
        data := map[string]string{
            "message": "hello",
        }
        render.JSON(w, r, data)
    })

    addr := os.Getenv("Addr")
    if addr == "" {
        addr = ":4444"
    }

    log.Printf("listen: %s", addr)
    if err := http.ListenAndServe(addr, r); err != nil {
        log.Fatalf("!! %+v", err)
    }
}

ポイントはいくつかあって

  • JSONを返す一番手軽な方法が知りたい
  • request/responseが成功したかを確認したい
  • どこにrequestすれば良いか知りたい (待ち受けるportを知りたい)
  • portは場合によっては変更したい
  • 例とは言えどもerrorハンドリングはまともにしたい

こういうMakefileを作る1

export Addr ?= :44444
00:
  go run $(shell echo $@*/)main.go

確認はhttpieで。

$ http :4444/api
HTTP/1.1 200 OK
Content-Length: 20
Content-Type: application/json; charset=utf-8
Date: Tue, 22 Sep 2020 12:19:34 GMT

{
    "message": "hello"
}
$ tree
.
├── 00hello
│   └── main.go
├── Makefile
├── go.mod
└── go.sum

1 directory, 4 files

go.modは作っておく場合も多い。

何か変更したかったら00から01を作る感じ。

例えば以下のようなことを考えて対応するコードを埋めていく感じで作業をしている。

  • requestのpathからデータを取りたい
  • postされたJSONをハンドリングしたい
  • NotFound時のデフォルトハンドラーを設定したい

次の作業の例

試しにJSONのハンドリングを例に取ると、以下の様な作業を行う。概ね以下のような流れ。

  1. 00の次なので01を作ろう
  2. postされたJSONをハンドリングしよう
  3. (JSONがinvalidなこともあるな)
  4. statusを変更して返す方法を探そう
  5. できた
  6. 動作確認しよう。

00の次なので01を作ろう

$ mkdir 01postjson
$ cp -r 00/* 01*
# edit 01*/main.go

postされたJSONをハンドリングしよう

できたところまで一気に。

--- 00hello/main.go  2020-09-22 21:21:09.000000000 +0900
+++ 01postjson/main.go    2020-09-22 21:30:15.000000000 +0900
@@ -1,6 +1,8 @@
 package main
 
 import (
+   "encoding/json"
+   "fmt"
    "log"
    "net/http"
    "os"
@@ -21,6 +23,24 @@
        render.JSON(w, r, data)
    })
 
+   r.Post("/api/hello", func(w http.ResponseWriter, r *http.Request) {
+       // {"target": "someone"}
+       params := map[string]string{}
+       decoder := json.NewDecoder(r.Body)
+       if err := decoder.Decode(&params); err != nil {
+           render.Status(r, 400)
+           render.JSON(w, r, map[string]interface{}{
+               "error": err.Error(),
+           })
+       }
+
+       defer r.Body.Close()
+       data := map[string]string{
+           "message": fmt.Sprintf("hello %s", params["target"]),
+       }
+       render.JSON(w, r, data)
+   })
+
    addr := os.Getenv("Addr")
    if addr == "" {
        addr = ":4444"
  • net/httpをそのまま見る形なので特に悩むことはなくrequest.Bodyを触れば良い
  • render.Status()という物がある。

これでなるほどーと思う感じになる(net/httpに寄せるならrenderパッケージは不要だが、content typeの指定などはだるいので省略したい)。

動作確認

動作確認もほぼほぼ同じ様な感じ。

makeにこれを付け足す。

# echo '{"target": "foo"}' | http --json POST :44444/api/hello
01:
  go run $(shell echo $@*/)main.go

実際に動かしてみる。

server側

$ make 01
go run 01postjson/main.go
2020/09/22 21:30:18 listen: :44444
2020/09/22 21:30:21 "POST http://localhost:44444/api/hello HTTP/1.1" from 127.0.0.1:57526 - 200 24B in 281.307µs

client側

$ echo '{"target": "foo"}' | http --json POST :44444/api/hello
HTTP/1.1 200 OK
Content-Length: 24
Content-Type: application/json; charset=utf-8
Date: Tue, 22 Sep 2020 12:30:21 GMT

{   
    "message": "hello foo"
}

と、まぁそういう感じ。1つ試したあとのディレクトリはこういう形。

$ tree
.
├── 00hello
│   └── main.go
├── 01postjson
│   └── main.go
├── Makefile
├── go.mod
└── go.sum

2 directories, 5 files

おわりに

こんな感じで細かい1ファイルを作りまくって動作検証をしたりしている日々。

gist

gistのことを考えると、main.goではなくmain00.goのような名前にしたほうが良いかもしれない。


  1. exportの利用は複雑なものに関してはオススメされていないがまぁ便利なので。。