5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Go 言語を学ぶAdvent Calendar 2023

Day 2

echoで作るGo製Webサーバー

Last updated at Posted at 2023-12-02

echo

アドベントカレンダー2日目です。1日目はこちら
echoでwebサーバーを作ります。

echoの特徴

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

echoのインストール

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

プログラム

公式のサンプルをお借りしました

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を繋げています。

version: '3'
services:
  golang:
    build: .
    tty: true
    volumes:
      - ./:/app/
    # ↓追記
    ports:
      - "1323:1323"

起動

$ 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関数で返却しています。

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リクエストを処理する

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)
}

リクエストを送ってみる

$ 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させて検証します。

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())が無効のとき

$ curl localhost:1323/error -X POST -d '{}'
curl: (52) Empty reply from server

ログを見てみます。スタックトレースのみ表示されており、echoの外側に漏れてしまっているようです。

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"}

しっかりとロギングできていますね。

{"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などを扱う予定ですが、そこでつなぎ込めるようにしようと考えています。

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?