echoで作るGo製Webサーバー

## echo
アドベントカレンダー2日目です。１日目は[こちら](https://qiita.com/shuyaeer/items/8b9f44c3f72615cb0548)
`echo`でwebサーバーを作ります。

https://github.com/labstack/echo

`echo`の特徴
```
ルートを賢く優先する最適化されたHTTPルーター
堅牢でスケーラブルなRESTful APIを構築
APIのグループ化
拡張可能なミドルウェアフレームワーク
ルート、グループ、またはルートレベルでのミドルウェアの定義
JSON、XML、フォームペイロード用のデータバインディング
様々なHTTPレスポンスを送信するための便利な機能
集中化されたHTTPエラー処理
任意のテンプレートエンジンでのテンプレートレンダリング
ロガーのフォーマットを自由に定義
高度にカスタマイズ可能
Let’s Encrypt経由での自動TLS
HTTP/2サポート
```

## echoのインストール
```bash
docker-compose exec golang /bin/bash
```

```
go get github.com/labstack/echo/v4
go get github.com/labstack/echo/v4/middleware
mkdir app/day2
touch app/day2/main.go
```


## プログラム
公式のサンプルをお借りしました
```go
package main

import (
	"net/http"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)


func main() {
	e := echo.New()

	e.Use(middleware.Logger())
	e.Use(middleware.Recover())

	e.GET("/", hello)

	e.Logger.Fatal(e.Start(":1323"))
}

func hello(c echo.Context) error {
	return c.String(http.StatusOK, "Hello, World!")
}
```

## ローカルとコンテナのポートをつなげる
ローカルからリクエストを送りたいのでportを繋げています。
```yml
version: '3'
services:
  golang:
    build: .
    tty: true
    volumes:
      - ./:/app/
    # ↓追記
    ports:
      - "1323:1323"
```

## 起動
```bash
$ go run app/day2/main.go
root@8f93343dfdb9:/app# go run app/day2/main.go

   ____    __
  / __/___/ /  ___
 / _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.11.3
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
                                    O\
⇨ http server started on [::]:1323
```

## リクエストを送ってみる
```
$ curl localhost:1323
Hello, World!
```

## レスポンスをJSON形式にする

レスポンス用の構造体を定義し、`c.JSON(http.StatusOK, data)`を`hello`関数で返却しています。

```go
package main

import (
	"net/http"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

type ResponseData struct {
	Status int 	   `json:"status"`
	Message string `json:"message"`
}


func main() {
	e := echo.New()

	e.Use(middleware.Logger()) // ログ出力用
	e.Use(middleware.Recover()) // 復帰用

	e.GET("/", hello)

	e.Logger.Fatal(e.Start(":1323"))
}

func hello(c echo.Context) error {
	data := ResponseData{
		Status: 200,
		Message: "Hello, World!",
	}
	return c.JSON(http.StatusOK, data)
}

```

## POSTリクエストを処理する
```go
package main

import (
	"net/http"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

type ResponseData struct {
	Status int 	   `json:"status"`
	Message string `json:"message"`
}

// リクエストパラメータ用の構造体
type RequestData struct {
    Message string `json:"message"`
}


func main() {
	e := echo.New()

	e.Use(middleware.Logger()) // ログ出力
	e.Use(middleware.Recover()) // 復帰用

	e.GET("/", hello)
	e.POST("/say", say)

	e.Logger.Fatal(e.Start(":1323"))
}

func hello(c echo.Context) error {
	data := ResponseData{
		Status: 200,
		Message: "Hello, World!",
	}
	return c.JSON(http.StatusOK, data)
}

func say(c echo.Context) error {
	data := new(RequestData)
	if err := c.Bind(&data); err != nil {
		return err
	}
	return c.JSON(http.StatusOK, data)
}
```

リクエストを送ってみる
```bash
$ curl localhost:1323/say -XPOST -d '{"message": "saysaysay"}' -H "Content-Type: application/json"
{"message":"saysaysay"}
```

## e.Use(middleware.Recover())を試す
`e.Use(middleware.Recover())`はコード内でパニックが発生した際、サーバーが死なないようにパニックをキャッチしてくれるようです。
本当にリカバリーできるのか試します。
`/error`にリクエストが来たらpanicさせて検証します。

```golang
package main

import (
	"net/http"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

type ResponseData struct {
	Status int 	   `json:"status"`
	Message string `json:"message"`
}

type RequestData struct {
    Message string `json:"message"`
}


func main() {
	e := echo.New()

	e.Use(middleware.Logger()) // ログ出力
	e.Use(middleware.Recover()) // 復帰用

	e.GET("/", hello)
	e.POST("/say", say)
	e.POST("error", func (c echo.Context) error {
		panic("Panic!") //確定でpanicさせてみる
		return nil
	})

	e.Logger.Fatal(e.Start(":1323"))
}

func hello(c echo.Context) error {
	data := ResponseData{
		Status: 200,
		Message: "Hello, World!",
	}
	return c.JSON(http.StatusOK, data)
}

func say(c echo.Context) error {
	data := new(RequestData)
	if err := c.Bind(&data); err != nil {
		return err
	}
	return c.JSON(http.StatusOK, data)
}
```

### e.Use(middleware.Recover())が無効のとき
```bash
$ curl localhost:1323/error -X POST -d '{}'
curl: (52) Empty reply from server
```

ログを見てみます。スタックトレースのみ表示されており、`echo`の外側に漏れてしまっているようです。
```bash
net/http.(*conn).serve.func1()
	/usr/local/go/src/net/http/server.go:1868 +0xb0
panic({0x26eb00?, 0x34ac20?})
	/usr/local/go/src/runtime/panic.go:920 +0x26c
main.main.func1({0x1a6fb759000f5968?, 0x0?})
	/app/app/day2/main.go:30 +0x2c
github.com/labstack/echo/v4.(*Echo).add.func1({0x351678, 0x40000140a0})
	/go/pkg/mod/github.com/labstack/echo/v4@v4.11.3/echo.go:582 +0x50
github.com/labstack/echo/v4/middleware.LoggerWithConfig.func2.1({0x351678, 0x40000140a0})
	/go/pkg/mod/github.com/labstack/echo/v4@v4.11.3/middleware/logger.go:126 +0xa0
github.com/labstack/echo/v4.(*Echo).ServeHTTP(0x40000ea6c0, {0x34dee8?, 0x4000076000}, 0x400010e100)
	/go/pkg/mod/github.com/labstack/echo/v4@v4.11.3/echo.go:669 +0x374
net/http.serverHandler.ServeHTTP({0x34d170?}, {0x34dee8?, 0x4000076000?}, 0x6?)
	/usr/local/go/src/net/http/server.go:2938 +0xbc
net/http.(*conn).serve(0x40000d82d0, {0x34e6b8, 0x400009dd70})
	/usr/local/go/src/net/http/server.go:2009 +0x518
created by net/http.(*Server).Serve in goroutine 1
	/usr/local/go/src/net/http/server.go:3086 +0x4cc
```

### e.Use(middleware.Recover())が有効のとき
```
$ curl localhost:1323/error -X POST -d '{}'
{"message":"Internal Server Error"}
```

しっかりとロギングできていますね。
```bash
{"time":"2023-12-02T14:21:23.892957596Z","level":"-","prefix":"echo","file":"recover.go","line":"120","message":"[PANIC RECOVER] error goroutine 19 [running]:\nmain.main.Recover.RecoverWithConfig.func2.1.1()\n\t/go/pkg/mod/github.com/labstack/echo/v4@v4.11.3/middleware/recover.go:100 +0x128\npanic({0x26eca0?, 0x34b200?})\n\t/usr/local/go/src/runtime/panic.go:914 +0x218\nmain.main.func1({0x40000fb8b8?, 0x0?})\n\t/app/app/day2/main.go:30 +0x2c\ngithub.com/labstack/echo/v4.(*Echo).add.func1({0x351c58, 0x40000a1180})\n\t/go/pkg/mod/github.com/labstack/echo/v4@v4.11.3/echo.go:582 +0x50\nmain.main.Recover.RecoverWithConfig.func2.1({0x351c58, 0x40000a1180})\n\t/go/pkg/mod/github.com/labstack/echo/v4@v4.11.3/middleware/recover.go:131 +0xec\ngithub.com/labstack/echo/v4/middleware.LoggerWithConfig.func2.1({0x351c58, 0x40000a1180})\n\t/go/pkg/mod/github.com/labstack/echo/v4@v4.11.3/middleware/logger.go:126 +0xa0\ngithub.com/labstack/echo/v4.(*Echo).ServeHTTP(0x40000ea6c0, {0x34e4c8?, 0x4000136000}, 0x400010e100)\n\t/go/pkg/mod/github.com/labstack/echo/v4@v4.11.3/echo.go:669 +0x374\nnet/http.serverHandler.ServeHTTP({0x34d750?}, {0x34e4c8?, 0x4000136000?}, 0x6?)\n\t/usr/local/go/src/net/http/server.go:2938 +0xbc\nnet/http.(*conn).serve(0x40000d82d0, {0x34ec98, 0x400009dda0})\n\t/usr/local/go/src/net/http/server.go:2009 +0x518\ncreated by net/http.(*Server).Serve in goroutine 1\n\t/usr/local/go/src/net/http/server.go:3086 +0x4cc\n\ngoroutine 1 [IO wait]:\ninternal/poll.runtime_pollWait(0xffff38d43e98, 0x72)\n\t/usr/local/go/src/runtime/netpoll.go:343 +0xa0\ninternal/poll.(*pollDesc).wait(0x40000e0500?, 0x259e0?, 0x0)\n\t/usr/local/go/src/internal/poll/fd_poll_runtime.go:84 +0x28\ninternal/poll.(*pollDesc).waitRead(...)\n\t/usr/local/go/src/internal/poll/fd_poll_runtime.go:89\ninternal/poll.(*FD).Accept(0x40000e0500)\n\t/usr/local/go/src/internal/poll/fd_unix.go:611 +0x250\nnet.(*netFD).accept(0x40000e0500)\n\t/usr/local/go/src/net/fd_unix.go:172 +0x28\nnet.(*TCPListener).accept(0x40000e4280)\n\t/usr/local/go/src/net/tcpsock_posix.go:152 +0x28\nnet.(*TCPListener).AcceptTCP(0x40000e4280)\n\t/usr/local/go/src/net/tcpsock.go:302 +0x2c\ngithub.com/labstack/echo/v4.tcpKeepAliveListener.Accept({0x40000f9cb8?})\n\t/go/pkg/mod/github.com/labstack/echo/v4@v4.11.3/echo.go:989 +0x1c\nnet/http.(*Server).Serve(0x4000118000, {0x34e5e8, 0x400009a080})\n\t/usr/local/go/src/net/http/server.go:3056 +0x2b8\ngithub.com/labstack/echo/v4.(*Echo).Start(0x40000ea6c0, {0x2ccbbf, 0x5})\n\t/go/pkg/mod/github.com/labstack/echo/v4@v4.11.3/echo.go:686 +0xcc\nmain.main()\n\t/app/app/day2/main.go:33 +0x3c0\n\n"}
{"time":"2023-12-02T14:21:23.893116512Z","id":"","remote_ip":"192.168.112.1","host":"localhost:1323","method":"POST","uri":"/error","user_agent":"curl/8.1.2","status":500,"error":"","latency":299708,"latency_human":"299.708µs","bytes_in":2,"bytes_out":36}
{"time":"2023-12-02T14:22:23.792135471Z","level":"-","prefix":"echo","file":"recover.go","line":"120","message":"[PANIC RECOVER] error goroutine 5 [running]:\nmain.main.Recover.RecoverWithConfig.func2.1.1()\n\t/go/pkg/mod/github.com/labstack/echo/v4@v4.11.3/middleware/recover.go:100 +0x128\npanic({0x26eca0?, 0x34b200?})\n\t/usr/local/go/src/runtime/panic.go:914 +0x218\nmain.main.func1({0x400003e8a8?, 0x0?})\n\t/app/app/day2/main.go:30 +0x2c\ngithub.com/labstack/echo/v4.(*Echo).add.func1({0x351c58, 0x40000140a0})\n\t/go/pkg/mod/github.com/labstack/echo/v4@v4.11.3/echo.go:582 +0x50\nmain.main.Recover.RecoverWithConfig.func2.1({0x351c58, 0x40000140a0})\n\t/go/pkg/mod/github.com/labstack/echo/v4@v4.11.3/middleware/recover.go:131 +0xec\ngithub.com/labstack/echo/v4/middleware.LoggerWithConfig.func2.1({0x351c58, 0x40000140a0})\n\t/go/pkg/mod/github.com/labstack/echo/v4@v4.11.3/middleware/logger.go:126 +0xa0\ngithub.com/labstack/echo/v4.(*Echo).ServeHTTP(0x40000ea6c0, {0x34e4c8?, 0x4000072000}, 0x400010e200)\n\t/go/pkg/mod/github.com/labstack/echo/v4@v4.11.3/echo.go:669 +0x374\nnet/http.serverHandler.ServeHTTP({0x34d750?}, {0x34e4c8?, 0x4000072000?}, 0x6?)\n\t/usr/local/go/src/net/http/server.go:2938 +0xbc\nnet/http.(*conn).serve(0x400006c000, {0x34ec98, 0x400009dda0})\n\t/usr/local/go/src/net/http/server.go:2009 +0x518\ncreated by net/http.(*Server).Serve in goroutine 1\n\t/usr/local/go/src/net/http/server.go:3086 +0x4cc\n\ngoroutine 1 [IO wait]:\ninternal/poll.runtime_pollWait(0xffff38d43e98, 0x72)\n\t/usr/local/go/src/runtime/netpoll.go:343 +0xa0\ninternal/poll.(*pollDesc).wait(0x40000e0500?, 0x27050?, 0x0)\n\t/usr/local/go/src/internal/poll/fd_poll_runtime.go:84 +0x28\ninternal/poll.(*pollDesc).waitRead(...)\n\t/usr/local/go/src/internal/poll/fd_poll_runtime.go:89\ninternal/poll.(*FD).Accept(0x40000e0500)\n\t/usr/local/go/src/internal/poll/fd_unix.go:611 +0x250\nnet.(*netFD).accept(0x40000e0500)\n\t/usr/local/go/src/net/fd_unix.go:172 +0x28\nnet.(*TCPListener).accept(0x40000e4280)\n\t/usr/local/go/src/net/tcpsock_posix.go:152 +0x28\nnet.(*TCPListener).AcceptTCP(0x40000e4280)\n\t/usr/local/go/src/net/tcpsock.go:302 +0x2c\ngithub.com/labstack/echo/v4.tcpKeepAliveListener.Accept({0x40000f9cb8?})\n\t/go/pkg/mod/github.com/labstack/echo/v4@v4.11.3/echo.go:989 +0x1c\nnet/http.(*Server).Serve(0x4000118000, {0x34e5e8, 0x400009a080})\n\t/usr/local/go/src/net/http/server.go:3056 +0x2b8\ngithub.com/labstack/echo/v4.(*Echo).Start(0x40000ea6c0, {0x2ccbbf, 0x5})\n\t/go/pkg/mod/github.com/labstack/echo/v4@v4.11.3/echo.go:686 +0xcc\nmain.main()\n\t/app/app/day2/main.go:33 +0x3c0\n\n"}
{"time":"2023-12-02T14:22:23.792440429Z","id":"","remote_ip":"192.168.112.1","host":"localhost:1323","method":"POST","uri":"/error","user_agent":"curl/8.1.2","status":500,"error":"","latency":1243459,"latency_human":"1.243459ms","bytes_in":2,"bytes_out":36}
```

## おしまい
2日目はwebサーバーを建ててみました。今後の日程でsqlなどを扱う予定ですが、そこでつなぎ込めるようにしようと考えています。