この章で行うこと

  • HTTPサーバー起動処理の実装
  • ルーティングの実装

この章の流れ

まずは、net/httpパッケージに関する話をします。大まかな仕組みを知った後で、今回のハンズオンではどのように実装していくかの方針を理解していただければと思っています。

次から始まるそのセクションに関しては、以下のZenn Booksを大いに参考にしています。とてもわかりやすいのでオススメです。

https://zenn.dev/hsaki/books/golang-httpserver-internal/viewer/serverstart

ここでは、実装の理解に必要な部分のみを扱いますが、時間のある方はぜひ読んでみることをお勧めいたします。

net/httpパッケージについて理解した後は、実際にサーバー・ルーティングの実装に入ります。

net/httpにおけるサーバー・ルーティングの処理

net/httpにおけるサーバー・ルーティングの処理がどのように行われているのかをなんとなく理解し、整理することがこのセクションの目的です。ご自身でもコードリーディングを行い、実際に処理を追っていただければ、より理解しやすいと思います。VSCodeなどであれば簡単に定義ジャンプできますので、ぜひやって見てください。

それでは、サーバ起動処理・ルーティング処理を見ていきましょう。

単純にサーバーを起動するなら、http.ListenServe()関数を使用するだけでそれが可能です。

http.ListenAndServe(":8080", nil)

ハンドラは以下のように登録できましたね(11章より)。

http.HandleFunc("GET /",func (w http.ResponseWriter, r *http.Request){...})

http.ListenServe()

http.ListenAndServe()関数の中では、*http.Server構造体を生成した後で、*http.Server型のListenAndServe()メソッドが呼ばれているようです。

// ListenAndServe always returns a non-nil error.
func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}

http.Server構造体

http.Server型には、サーバーを操作するためのメソッドが定義されています(一部抜粋)。

func (srv *http.Server) ListenAndServe() error
func (srv *http.Server) ListenAndServeTLS(certFile string, keyFile string) error
func (srv *http.Server) Shutdown(ctx context.Context) error

構造体の中身は以下のようになっています。

type Server struct {
	// Addr は、サーバーがリッスンする TCP アドレスを "host:port" の形式で指定します。
	// 空の場合は、デフォルトで ":http"(ポート 80)が使用されます。
	Addr string

	// リクエストを処理するハンドラー。
	// nil の場合はデフォルトで http.DefaultServeMux が使用されます。
	Handler Handler 

	// (本当はもっとフィールドがあります)
}

http.ListenAndServe()の第二引数にはnilを入れていたので、Server構造体のHandlerフィールドにはnilが入ることになりますね。

そこで、Handlerフィールドのコメントに注目すると、

// nil の場合はデフォルトで http.DefaultServeMux が使用されます。

とありますね。

このhttp.DefaultServeMuxはマルチプレクサと呼ばれるものです。

http.ListenAndServe()関数の第二引数にnilを入れると、デフォルトのマルチプレクサを使用してくれるようです。

マルチプレクサ(http.ServeMux)

マルチプレクサとは、いわゆる「ルーティングハンドラ」と読み替えることができます。これもhttp.Handlerインターフェースを満たしています。

リクエストが来ると、登録されたパス・ハンドラの組み合わせに従って、リクエストを振り分けます。以下はそれを表した図です。

golangにおいてマルチプレクサはhttp.ServeMux型として表現されており、http.ServeMux型には、ハンドラを登録するためのメソッドが定義されています(一部抜粋)。

func (mux *ServeMux) Handle(pattern string, handler Handler)
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request))
func (mux *ServeMux) register(pattern string, handler Handler)

実際にハンドラーをルーティングに登録するのはregister()メソッドです。Handle()とHandleFunc()メソッドは、register()メソッドを呼び出すためのものです。

*http.ServeMux型のインスタンス、つまりマルチプレクサを自身で用意して、メソッドを呼び出しを行います。

mux:=http.NewServeMux() // マルチプレクサを用意

mux.Handle("GET /",handler)
mux.HandleFunc("GET /",func(rw http.ResponseWriter,r *http.Request))

http.NewServeMux()関数によってマルチプレクサを用意できます。

11章で示した図を少し変えていますが、以下のようになります(適宜拡大してください🙏)。register()メソッドはServeMux型のメソッドだったのですね。

https://qiita.com/huji0327/items/c85affaf5b9dbf84c11e

まとめ

ここまでの話で、サーバー関連はhttp.Server型が、ルーティング関連はhttp.ServeMux型がといったように処理が分離されていると整理することができます。

型 役割 メソッド(例)
http.Server サーバー関連の処理 ・ ListenAndServe():サーバーを起動しリクエストを待機 ・ Shutdown():サーバーを安全にシャットダウン
http.ServeMux ルーティング関連の処理 ・ Handle():パターンに対してハンドラーを登録 ・ HandleFunc():関数型のハンドラーを登録

http.Handle()関数とhttp.HandleFunc()関数

http.ServeMux型のHandle()メソッドとHandleFunc()メソッドが登場して、若干混乱するかもしれないと思ったので、http.Handle()関数とhttp.HandleFunc()関数を改めてここで取り上げておきます。

どちらの関数も、ルーティングにハンドラを登録できるのでしたね。

メソッド名 説明
http.Handle() http.Handler を満たす構造体をルーティングに登録する。
htp.HandleFunc() func(ResponseWriter, *Request))のシグネチャをとる関数をHandlerFunc(handler)でキャストし、http.Handlerを満たす形で登録する。

これらの関数はどちらも、内部でデフォルトのマルチプレクサに対してハンドラを登録しています。

func Handle(pattern string, handler Handler) {
    DefaultServeMux.register(pattern, handler)
}

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
     DefaultServeMux.register(pattern, HandlerFunc(handler))
}

デフォルトのマルチプレクサは以下のように定義されています。

// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux

var defaultServeMux ServeMux

http.Handle()とhttp.HandleFunc()を使用すると、マルチプレクサを意識せずにハンドラをルーティングに登録することができると言えます。

http.ListenAndServe()関数の第二引数にnilを入れると、デフォルトのマルチプレクサが利用されるのでしたね。

実装方針

HTTPサーバー周りの実装と、ルーティング周りの実装をどのようにしていくかの方針を述べていきます。

「サーバー周りの処理」と「ルーティング周りの処理」の分離

見出しの通りですが、サーバー起動の処理と、ルーティング周りの処理は分離して実装していきます。

反対に、「分離しない」実装はどのようになるかというと、以下のやり方になります。

  • http.Handle()かhttp.HandleFunc()をつかってハンドラを登録
  • http.ListenAndServe(":8080",nil)でサーバーを起動

ハンドラの登録とサーバー起動の処理が関数として分かれてはいるものの、ルーティングを司るhttp.ServeMuxの存在がほとんど隠蔽されています。デフォルトのマルチプレクサが内部で使用されていたのでしたね。

今回は、マルチプレクサを自身で用意し、それに対してハンドラを登録することで、サーバー処理を司るhttp.Server型からルーティング処理を切り離します。

ルーティング(http.ServeMux)を切り離すメリット

ルーティング機能を切り離すことによって、拡張性を持たせることが可能です。

巷には、ルーティングの機能を提供するパッケージが多く存在します。代表的なものに、以下の二つなどがあります。

ルーティング、つまりマルチプレクサの機能を提供してくれるものです。そして、これら二つのマルチプレクサは、http.Handlerインターフェースを満たし、net/httpパッケージと互換性を持って実装されています。

そのため、全く同じようにしてハンドラへの登録ができます。また、http.Handlerインターフェースを満たしているため、http.ListenAndServe()関数に渡すことができます(第二引数はhttp.Handler型)。

// net/http
mux := http.NewServeMux()
mux.Handle("/", handlerA)
mux.HandleFunc("/products", handlerB)

http.ListenAndServe(":8080",mux)

// chi
r := chi.NewRouter()
r.Handle("/", handlerA)
r.HandleFunc("/products", handlerB)

http.ListenAndServe(":8080",r)

// gorilla/mux
r := mux.NewRouter()
r.Handle("/", handlerA)
r.HandleFunc("/products", handlerB)

http.ListenAndServe(":8080",r)

このように、書き方が変わらないということは、ルーティング機能のみを他ライブラリに乗り換えるなどが容易になり、拡張性が見込めると言えそうですね。

http.Sever構造体は以下のようになっていました。

type Server struct {
	Addr string
	Handler Handler // マルチプレクサが入る
	// (本当はもっとフィールドがあります)
}

「サーバー操作にはhttp.Serverのメソッドを利用し、http.Server構造体のHandlerフィールドに代入するマルチプレクサを別で生成する」といったやり方で、ルーティング処理を切り離すようにします。

次から、実装に入っていきます。

HTTPサーバー起動処理の実装

まずはサーバー処理の方から実装していきましょう。

サーバー処理の実装
// app/infrastructure/server/server.go
package server

import (
	"context"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"golang.org/x/sync/errgroup"
)

// *http.Server型をラップする
type server struct {
	srv *http.Server
}

// 独自のserver.Server型を返すコンストラクタ
// main関数でマルチプレクサを渡す
func NewServer(port string, mux http.Handler) *server {
	return &server{
		&http.Server{
			Addr:    port,
			Handler: mux,
		},
	}
}

// サーバーを起動する
func (s *server) Run(ctx context.Context) error {
	ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
	// シグナルの監視をやめてリソースを開放する
	defer stop()
	eg, ctx := errgroup.WithContext(ctx)
	eg.Go(func() error {
		// サーバー起動中にエラーが起きると、ctxに伝達される(Shutdownによるエラーは正常なので無視)
		log.Printf("server is runnning at port %q...", s.srv.Addr)
		if err := s.srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			log.Printf("http server on %s failed : %+v", s.srv.Addr, err)
			return err
		}
		log.Printf("The server on %s is gracefully shutting down", s.srv.Addr)

		return nil
	})
	// サーバーからのエラーとシグナルを待機する
	<-ctx.Done()
	// シャットダウンをするまでのタイムアウト時間を設定
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := s.srv.Shutdown(ctx); err != nil {
		log.Fatalf("failed to shutdown http server on %s : %+v", s.srv.Addr, err)
	}
    log.Println("server was successfully shut down gracefully")
	// ゴルーチンのクロージャ内のエラーを返す
	// ゴルーチンを待機する
	return eg.Wait()
}

テストコード

// app/infrastructure/server/server_test.go
package server

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"testing"
	"time"

	"golang.org/x/sync/errgroup"
)

func TestRun(t *testing.T) {
	// テストとしてキャンセル通知を送るため
	ctx, cancel := context.WithCancel(context.Background())
	eg, ctx := errgroup.WithContext(ctx)
	// サーバー構造体(テスト対象)
	mux := http.NewServeMux()
	sut := NewServer(":8881", mux)
	// デフォルトマルチプレクサにハンドラを登録
	want := "Hello,world!"
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, want)
	})
	eg.Go(func() error {
		return sut.Run(ctx)
	})

	// リクエストを送ってレスポンスが期待通りか確かめる
	// リトライ処理なしだとCIで落ちる
	for i := 0; i < 5; i++ {
		resp, err := http.Get("http://localhost:8881")
		if err != nil {
			if i < 4 {
				time.Sleep(1 * time.Second) // リトライ前に少し待機
				continue
			}
			t.Errorf("failed to request server : %v", err)
			break
		}

		// ステータスコードを確認
		if resp.StatusCode != http.StatusOK {
			t.Errorf("expected status code 200 but got %d", resp.StatusCode)
			continue
		}

		got, err := io.ReadAll(resp.Body)
		if err != nil {
			t.Fatalf("failed to read body: %v", err)
		}

		// 読み取ったレスポンスを閉じる
		resp.Body.Close()

		if string(got) != want {
			t.Errorf("want %q , but got %q", want, got)
		}
	}

	// キャンセル通知を送ってサーバーが正常に終了するか
	cancel()
	if err := eg.Wait(); err != nil {
		t.Fatal(err)
	}
}

細かなポイントが多いですが、大まかには以下の二つです。順に見ていきます。

  • *http.Serverをラップしたserver構造体
  • サーバー起動〜グレースフルシャットダウンの実装

*http.Serverをラップしたserver構造体

内部にsrvフィールドとしてhttp.Serverをもつ、server構造体を定義しています。NewServer()関数によって独自にラップした*server型を返します。

// *http.Server型をラップする
type server struct {
	srv *http.Server
}

// 独自のserver.Server型を返すコンストラクタ
// main関数でマルチプレクサを渡す
func NewServer(port string, mux http.Handler) *server {
	return &server{
		&http.Server{
			Addr:    port,
			Handler: mux,
		},
	}
}

main.goにおけるmain関数でサーバー起動処理としてこの関数を呼び出します。その際、引数にルーティングを登録したマルチプレクサ(http.ServeMux)を渡すことで、サーバー処理とルーティング処理の分離を図るようにしています。

そして、Run()関数内でサーバー起動にhttp.Server型のListenAndServe()メソッドを呼び出しています。

s.srv.ListenAndServe()

ここで、別にhttp.ListenAndServe()関数でもよくないか?と思われる方もいるかもしれません。このようにしている理由は二つで、

  • サーバー処理のみを扱うhttp.Server型を使用することで責務が明確化する
  • グレースフルシャットダウンを行えるShutdown()メソッドを使いたい

ざっくりと、「このようにした方が柔軟に実装できるのではないか」という感じでしょうか。

サーバー起動〜グレースフルシャットダウンの実装

グレースフルシャットダウン

グレースフルシャットダウンとは、

サーバーが停止する際に、既存の接続やリクエストを適切に処理し終わった後にシャットダウンを行う

ことです。

すでに受け付けていたリクエストは、そのリクエストの処理が適切に完了するまで待機します。これによって、安全なサーバーのシャットダウン(切断)を行うというものです。

サーバー起動〜グレースフルシャットダウンの処理は、server独自型に定義したRun()メソッドに記述しています。

// サーバーを起動する
func (s *server) Run(ctx context.Context) error {
	ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
	// シグナルの監視をやめてリソースを開放する
	defer stop()
	eg, ctx := errgroup.WithContext(ctx)
	eg.Go(func() error {
		// サーバー起動中にエラーが起きると、ctxに伝達される(Shutdownによるエラーは正常なので無視)
		log.Printf("server is runnning at port %q...", s.srv.Addr)
		if err := s.srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			log.Printf("http server on %s failed : %+v", s.srv.Addr, err)
			return err
		}
		log.Printf("The server on %s is gracefully shutting down", s.srv.Addr)

		return nil
	})
	// サーバーからのエラーとシグナルを待機する
	<-ctx.Done()
	// シャットダウンをするまでのタイムアウト時間を設定
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := s.srv.Shutdown(ctx); err != nil {
		log.Fatalf("failed to shutdown http server on %s : %+v", s.srv.Addr, err)
	}
	log.Println("server was successfully shut down gracefully")
	// ゴルーチンのクロージャ内のエラーを返す
	// ゴルーチンを待機する
	return eg.Wait()
}

とても長く、複雑に見えますね。

実装コードを順に見ていきましょう。

シグナルをハンドリングする

以下のコードで、新たなcontext.Contextを生成していますね。

ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
// シグナルの監視をやめてリソースを開放する
defer stop()

リクエストの処理中に「コンテナの終了」や「サーバープログラムの終了」が行われた場合は、処理の完了を待機してからサーバーを切断する、グレースフルシャットダウンが行われることが望ましいです。

そこで、「コンテナの終了」や「サーバープログラムの終了」がアプリケーションプロセスに送るシグナルを検知するようにし、検知した場合にはグレースフルシャットダウンを行うという流れにしなければなりません。

signal.NotifyContext()関数は、引数にとったシグナルの受信を検知してくれます。今回、引数にとったシグナルは以下のようなものです。

シグナル 説明
SIGINT (os.Interrupt) ユーザーがCtrl + Cで送信する中断シグナル。プログラムを中断するために使用されます。
SIGTERM システムから終了を依頼するシグナル。通常、プロセスが終了する準備をするために使用されます。

signal.NotifyContext()関数は内部でコンテキストにキャンセル機能を追加しています。

func NotifyContext(parent context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc) {
    // キャンセル機能追加
    ctx, cancel := context.WithCancel(parent)
    // 省略
    return c, c.stop
}

つまり、シグナルを検知したら、コンテキストを通じてキャンセル通知を送れるようになっているのです。

シグナルの検知は以下のようにして待機することができます。(キャンセル通知を受け取るまで、処理がブロッキングするからです)

<-ctx.Done()

コンテキストを用いたキャンセル機能に関しては以下をご参照ください。

https://zenn.dev/hsaki/books/golang-context/viewer/done#context%E3%81%AEdone%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%82%92%E7%94%A8%E3%81%84%E3%81%9F%E3%82%AD%E3%83%A3%E3%83%B3%E3%82%BB%E3%83%AB%E5%87%A6%E7%90%86

ゴルーチンでサーバーを起動する

サーバーの起動は別ゴルーチンとして行い、リクエストを待機するようにしています。

func (s *server) Run(ctx context.Context) error {
    //省略

    // `*errgroup.Group`型・新たなコンテキストの生成
    eg, ctx := errgroup.WithContext(ctx)
    eg.Go(func() error {
        // サーバー起動中にエラーが起きると、ctxに伝達される(Shutdownによるエラーは正常なので無視)
        if err := s.srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
            log.Printf("http server on %s failed : %+v", s.srv.Addr, err)
            return err
        }
    
        //省略
    
        return nil
    })
    // サーバーからのエラーとシグナルを待機する
    <-ctx.Done()
    
    // 省略(シャットダウン処理)
    
    // ゴルーチンのクロージャ内のエラーを返す
    // ゴルーチンを待機する
    return eg.Wait()
}

errgroup(syncパッケージにおけるサブパッケージ)における、*errgroup.Group型のGo()メソッドによってゴルーチンを起動しています。

func (g *Group) Go(f func() error) {...}

Go()メソッドはerrorを返すクロージャを引数にとるため、クロージャ内にてs.srv.ListenAndServe()メソッドを使用しています。このhttp.Serve型のListenAndServe()メソッドこそがサーバーの起動を行ってくれるものでしたね。

このerrgroup型のGo()メソッドによって起動したゴルーチン内で処理を行うことで、エラーハンドリングが容易になります。というのも、ゴルーチン内でエラーが起きると、それをコンテキストによって検知できるのです。

// `*errgroup.Group`型・新たなコンテキストの生成
eg, ctx := errgroup.WithContext(ctx)

errgroupに用意されたWithContext()関数により、新たなコンテキストが作成されています。関数内部では、context.WithCancelCause()関数が使用されており、キャンセル通知を受け取れるようになっています。

errgroup.WithContext(ctx)の引数にとった親コンテキストは、シグナル検知機能を追加したctxです。それによって新たに作成されるコンテキストはつまり、

  • シグナルを検知
  • ゴルーチン内エラーを検知

できるようになっていると言うことです。以下では、「シグナルを検知」と「ゴルーチン内エラー検知」によるキャンセル通知を待ち受けていると言うことになりますね。

<-ctx.Done()

つまり、「シグナルを検知」or「ゴルーチン内エラー検知」してキャンセル通知がコンテキストに送られると、その下のシャットダウン処理に進んでいくと言う感じです。

Run()関数の最後に記述してある次の部分では、ゴルーチンの終了を待機しています。これにより、ゴルーチンによる処理の完了を待たずしてRun()関数が終了してしまう、といったことを防げます。この使い方はsync.WaitGroup型のものとそう変わりません。

// ゴルーチンのクロージャ内のエラーを返す
// ゴルーチンを待機する
return eg.Wait()

またそれと同時に、ゴルーチン内で起きたエラーを返すようです。

Shutdown()メソッド

Shutdow()メソッドこそがグレースフルシャットダウンを行ってくれるものとなっています。以下のように定義されています。

// Shutdown は、アクティブな接続を中断することなくサーバーをグレースフルシャットダウンします。
// Shutdown の動作は、まずすべてのリスナーを閉じ、次にアイドル状態の接続をすべて閉じ、
// その後、すべての接続がアイドル状態に戻るのを無期限に待ってからシャットダウンします。
// 指定されたコンテキストの期限がシャットダウン完了前に切れた場合、Shutdown は
// そのコンテキストのエラーを返します。そうでなければ、[Server] の基盤となるリスナー
// を閉じる際に発生したエラーを返します。
//
// Shutdown が呼び出されると、[Serve]、[ListenAndServe]、[ListenAndServeTLS] は
// 直ちに [ErrServerClosed] を返します。そのため、プログラムが即座に終了せず、
// Shutdown の戻りを待つようにしてください。
//
// 一度 Shutdown がサーバーに対して呼び出されると、そのサーバーは再利用できません。
// その後、Serve などのメソッドを呼び出しても [ErrServerClosed] を返します。
func (srv *Server) Shutdown(ctx context.Context) error {...} 

Shutdown()メソッドを扱う上で、コメントに書かれている以下の2点に注意します。

  • コンテキストのタイムアウトを検知すると、エラーが返る
  • ListenAndServe()はErrServerClosedエラーを返す

まず、前者についてですが、以下のようにコンテキストにタイムアウトを設定しています(時間は適当に設定しています)。

// シャットダウンをするまでのタイムアウト時間を設定
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := s.srv.Shutdown(ctx); err != nil {
    log.Fatalf("failed to shutdown http server on %s : %+v", s.srv.Addr, err)
}
log.Println("server was successfully shut down gracefully")

Shutdown()には以下のようにコンテキストのキャンセル通知を待ち受けている記述が見られます。

select {
case <-ctx.Done():
	return ctx.Err()

また後者について、ListenAndServe()はShutdown()によりErrServerClosedを返すとあります。コードリーディングしてそれを確かめてみます。Sever構造体には、シャットダウン中かどうかのフラグとしてinShutdownと言うフィールドがあるようですね。

type Server struct {
    // 他は省略
   inShutdown atomic.Bool // true when server is in shutdown
}

Shutdown()では、初っ端に、おそらくinShutdownフィールドをtrueに変更しているであろう処理が見受けられます。

func (srv *Server) Shutdown(ctx context.Context) error {
	srv.inShutdown.Store(true)
    // 省略
}

ListenAndServe()では、、inShutdownフィールドがtrueであればErrServerClosedエラーを返すといった記述が見られます。

func (srv *Server) ListenAndServe() error {
	if srv.shuttingDown() {
		return ErrServerClosed
	}
    // 省略
}

なるほど、確かにShutdown()するとListenAndServe()からErrServerClosedが返っていますね。ここで注意して実装したいのが、s.srv.ListenAndServe()から返るエラーの中でもErrServerClosedは「正常」なものであるということです。

それを考慮して、s.srv.ListenAndServe()からのerrがあってもそれがErrServerClosedだった場合はクロージャとしてerrorを返さないようにします。

eg.Go(func() error {
    // サーバー起動中にエラーが起きると、ctxに伝達される(Shutdownによるエラーは正常なので無視)
    log.Printf("server is runnning at port %q...", s.srv.Addr)
    if err := s.srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
        log.Printf("http server on %s failed : %+v", s.srv.Addr, err)
        return err
    }
    log.Printf("The server on %s is gracefully shutting down", s.srv.Addr)

    return nil
})

このようにしないと、正常にシャットダウンを行っているにも関わらず、それがゴルーチン内でエラーとなり、errgroupにエラーが渡ることになります。つまり、eg.Wait()によってRun()メソッドからエラーを返してしまうことになります。

それは期待しない挙動なので、errors.Is()によってエラーの値を判別するようにしています。

このような工夫で、シャットダウン時には、ゴルーチンが正常終了するようになっています。

ルーティングの実装

先のサーバー処理を行うserver型(独自型)は、http.Server型をラップしたものでした。

// 独自型
type server struct {
	srv *http.Server
}
// net/http
type Server struct {
    Addr    string
    Handler Handler
}

ラップしてあるhttp.Server型のHandlerフィールドこそ、マルチプレクサ(ルーティングハンドラ)です。

server独自型のインスタンスを返すNewServer()関数には、ルーティングを登録したマルチプレクサを渡します。

func NewServer(port string, mux http.Handler) *server {
	return &server{
		&http.Server{
			Addr:    port,
			Handler: mux,   // ルーティング登録ずみのマルチプレクサが入る
		},
	}
}

つまり、ここで言う「ルーティング処理の実装」とは、

http.ServeMux型にルーティングを登録し、それを返すようにする処理

を書くことだと言えるでしょう。

最終的にmain()関数では以下のようにサーバーを起動する処理を呼び出します。

srv := server.NewServer(
    cfg.Server.Port,
    router.NewMux(),  // ルーティング登録済みのマルチプレクサを渡す
)
srv.Run(ctx) // サーバー起動

ルーティングを登録したマルチプレクサを返すNewMux()関数を実装していきましょう。

以下が実装コードです。

ルーティング処理の実装
// app/infrastructure/router/mux.go
package router

import (
	"net/http"

	"github.com/kakkky/app/adapter/presentation/handler/health"
	swagger "github.com/swaggo/http-swagger"
)

// ルーティングを登録したマルチプレクサを返す
func NewMux() http.Handler {
	// マルチプレクサを初期化
	mux := http.NewServeMux()
	// ハンドラーの初期化
	initHandlers()
	// ミドルウェアの初期化
	initMiddlewares()
	// ハンドラをルーティングに登録
	registerRoutes(mux)

	return mux
}

func registerRoutes(mux *http.ServeMux) {
	// 開発用ルーティング
	{
		mux.HandleFunc("GET /health", health.HealthCheckHandler)
		mux.Handle("GET /swagger/", swagger.WrapHandler)
	}
	// 認証系ルーティング
	{
		mux.Handle("POST /login", composeMiddlewares(logger)(loginHandler))
		mux.Handle("DELETE /logout", composeMiddlewares(logger, authorization)(logoutHandler))
	}
	// ユーザー系ルーティング
	{
		mux.Handle("POST /users", composeMiddlewares(logger)(postUserHandler))
		mux.Handle("DELETE /users/me", composeMiddlewares(logger, authorization)(deleteUserHandler))
		mux.Handle("GET /users", composeMiddlewares(logger, authorization)(getUsersHandler))
		mux.Handle("PATCH /users/me", composeMiddlewares(logger, authorization)(updateUserHandler))
	}
	// タスク系ルーティング
	{
		mux.Handle("POST /tasks", composeMiddlewares(logger, authorization)(postTaskHandler))
		mux.Handle("DELETE /tasks/{id}", composeMiddlewares(logger, authorization)(deleteTaskHandler))
		mux.Handle("PATCH /tasks/{id}/state", composeMiddlewares(logger, authorization)(updateTaskStateHandler))
		mux.Handle("GET /tasks/{id}", composeMiddlewares(logger, authorization)(getTaskHandler))
		mux.Handle("GET /tasks", composeMiddlewares(logger, authorization)(getTasksHandler))
		mux.Handle("GET /users/me/tasks", composeMiddlewares(logger, authorization)(getUserTasksHandler))
	}
}

ハンドラーを初期化する処理

// app/infrastructure/router/handlers.go
package router

import (
	"github.com/kakkky/app/adapter/presentation/handler/auth"
	"github.com/kakkky/app/adapter/presentation/handler/task"
	"github.com/kakkky/app/adapter/presentation/handler/user"
	"github.com/kakkky/app/adapter/queryservice"
	"github.com/kakkky/app/adapter/repository"
	authUsecase "github.com/kakkky/app/application/usecase/auth"
	taskUsecase "github.com/kakkky/app/application/usecase/task"
	userUsecase "github.com/kakkky/app/application/usecase/user"
	userDomain "github.com/kakkky/app/domain/user"
	authInfra "github.com/kakkky/app/infrastructure/auth"
	"github.com/kakkky/app/infrastructure/db/sqlc"
	"github.com/kakkky/app/infrastructure/kvs"
)

// 認証系ハンドラー
var (
	loginHandler  *auth.LoginHandler
	logoutHandler *auth.LogoutHandler
)

// ユーザー系ハンドラー
var (
	postUserHandler   *user.PostUserHandler
	deleteUserHandler *user.DeleteUserHandler
	updateUserHandler *user.UpdateUserHandler
	getUsersHandler   *user.GetUsersHandler
)

// タスク系ハンドラー
var (
	postTaskHandler        *task.PostTaskHandler
	deleteTaskHandler      *task.DeleteTaskHandler
	updateTaskStateHandler *task.UpdateTaskStateHandler
	getTaskHandler         *task.GetTaskHandler
	getTasksHandler        *task.GetTasksHandler
	getUserTasksHandler    *task.GetUserTasksHandler
)

// ハンドラーを初期化する
func initHandlers() {
	initAuthHandlers()
	initUserHandlers()
	initTaskHandlers()
}

func initAuthHandlers() {
	loginHandler = auth.NewLoginHandler(
		authUsecase.NewLoginUsecase(
			repository.NewUserRepository(sqlc.NewSqlcQuerier()),
			repository.NewJwtAuthenticatorRepository(kvs.NewRedisCommander()),
			authInfra.NewJwtAuthenticator(),
		),
	)

	logoutHandler = auth.NewLogoutHandler(
		authUsecase.NewLogoutUsecase(
			authInfra.NewJwtAuthenticator(),
			repository.NewJwtAuthenticatorRepository(kvs.NewRedisCommander()),
		),
	)
}

func initUserHandlers() {
	postUserHandler = user.NewPostUserHandler(
		userUsecase.NewRegisterUsecase(
			repository.NewUserRepository(sqlc.NewSqlcQuerier()),
			userDomain.NewUserDomainService(repository.NewUserRepository(sqlc.NewSqlcQuerier())),
		),
	)

	deleteUserHandler = user.NewDeleteUserHandler(
		userUsecase.NewUnregisterUsecase(
			repository.NewUserRepository(sqlc.NewSqlcQuerier()),
		),
	)

	getUsersHandler = user.NewGetUsersHandler(
		userUsecase.NewFetchUsersUsecase(
			repository.NewUserRepository(sqlc.NewSqlcQuerier()),
		),
	)

	updateUserHandler = user.NewUpdateUserHandler(
		userUsecase.NewUpdateProfileUsecase(
			repository.NewUserRepository(sqlc.NewSqlcQuerier()),
		),
	)
}

func initTaskHandlers() {
	postTaskHandler = task.NewPostTaskHandler(
		taskUsecase.NewCreateTaskUsecase(
			repository.NewTaskRepository(sqlc.NewSqlcQuerier()),
		),
	)

	deleteTaskHandler = task.NewDeleteTaskHandler(
		taskUsecase.NewDeleteTaskUsecase(
			repository.NewTaskRepository(sqlc.NewSqlcQuerier()),
		),
	)

	updateTaskStateHandler = task.NewUpdateTaskStateHandler(
		taskUsecase.NewUpdateTaskStateUsecase(
			repository.NewTaskRepository(sqlc.NewSqlcQuerier()),
		),
	)

	getTaskHandler = task.NewGetTaskHandler(
		taskUsecase.NewFetchTaskUsease(
			queryservice.NewTaskQueryService(sqlc.NewSqlcQuerier()),
		),
	)

	getTasksHandler = task.NewGetTasksHandler(
		taskUsecase.NewFetchTasksUsease(
			queryservice.NewTaskQueryService(sqlc.NewSqlcQuerier()),
		),
	)

	getUserTasksHandler = task.NewGetUserTasksHandler(
		taskUsecase.NewFetchUserTasksUsecase(
			queryservice.NewTaskQueryService(sqlc.NewSqlcQuerier()),
		),
	)
}

ミドルウェアを初期化する処理

// app/infrastructure/router/middlewares.go
package router

import (
	"net/http"

	"github.com/kakkky/app/adapter/presentation/middleware"
	"github.com/kakkky/app/adapter/repository"
	authUsecase "github.com/kakkky/app/application/usecase/auth"
	"github.com/kakkky/app/infrastructure/auth"
	"github.com/kakkky/app/infrastructure/kvs"
)

// ミドルウェア
var (
	authorization func(h http.Handler) http.Handler
	logger        func(h http.Handler) http.Handler
)

// ミドルウェアを初期化する
func initMiddlewares() {
	authorization = middleware.Authorication(
		authUsecase.NewAuthorizationUsecase(
			auth.NewJwtAuthenticator(),
			repository.NewJwtAuthenticatorRepository(kvs.NewRedisCommander()),
		),
	)
	logger = middleware.Logger
}

// 以下は既に実装済みです。

// 適用させたい順で、ミドルウェアを引数に入れる
// composeMiddlewares(M1,M2,M3)とした場合、M1(M2(M3()))といったようにラップされたミドルウェアを返す
func composeMiddlewares(middlewares ...func(http.Handler) http.Handler) func(http.Handler) http.Handler {
	return func(h http.Handler) http.Handler {
		for i := range middlewares {
			h = middlewares[len(middlewares)-(i+1)](h)
		}
		return h
	}
}

テストコード

// app/infrastructure/router/mux_test.go
package router

import (
	"net/http"
	"net/http/httptest"
	"testing"
)

func TestNewMux(t *testing.T) {
	tests := []struct {
		name     string
		method   string
		path     string
		wantCode int
		wantErr  bool
	}{
		{
			name:     "正常にヘルスチェックハンドラを呼び出せる",
			method:   http.MethodGet,
			path:     "/health",
			wantCode: http.StatusOK,
			wantErr:  false,
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			w := httptest.NewRecorder()
			rw := httptest.NewRequest(tt.method, tt.path, nil)
			sut := NewMux()
			sut.ServeHTTP(w, rw)
			resp := w.Result()
			if resp.StatusCode != tt.wantCode {
				t.Errorf("want status code %d, but got %d", tt.wantCode, resp.StatusCode)
			}
		})
	}
}

NewMux()内部では、以下のように生成した*http.ServeMux型のmux変数にハンドラーを登録しています。

// マルチプレクサを初期化
mux := http.NewServeMux()

ポイントは、 大きく分けて以下の二つです。

  • ハンドラー・ミドルウェアをinitHandlers()・initMiddlewares()関数で初期化している
  • ハンドラーをregisterRoutes()関数によってルーティングに登録する

ハンドラー・ミドルウェアを初期化

initHandlers()関数

NewMux()はinitHandlers()関数を呼び出していますが、これは内部で3つの関数を呼び出しているだけです。

func initHandlers() {
	initAuthHandlers()
	initUserHandlers()
	initTaskHandlers()
}

それぞれは、パッケージ変数として設定したハンドラーを初期化する処理が記述されています。

ハンドラー初期化時に依存性を注入する

ルーティングにハンドラー(http.Handler型の構造体)を登録するには、ハンドラーの初期化をする必要があります。(どのハンドラにも、NewXxxHandler()関数を定義していました。それです。)

ハンドラーは、ユースケース処理を呼び出すものでした。そのユースケースは、リポジトリやクエリサービス、ドメインサービスの処理を呼び出し、操作していました。ハンドラーが依存する「ユースケース」コンポーネントは具体型ですが、その「ユースケース」の依存先はインターフェースとしていたことを思い出してください。

以下は、PostUserHandlerが抱えている依存先を示した図です。

インターフェースは単なる「振る舞い」でしたね。ハンドラーの初期化に際して、

インターフェースという「抽象型」に対して、実装の詳細である「具体型」を注入してやる必要があります。

これを、Dependency Injection(依存性の注入)、略してDIといいます。

例えば、ユースケースはリポジトリを利用していましたが、依存していたのはリポジトリのインターフェースでしたね。そこに、リポジトリの具体型である、repository.userRepositoryをDIします。

また、そのリポジトリもORMを抽象化したrepository.Querierインターフェースに依存していました。そこに、sqlcを利用する具体型であるsqlc.SqlcQuerierをDIしています。

コードで表すと、以下のようになります。

postUserHandler = user.NewPostUserHandler(  //ハンドラーの初期化
    userUsecase.NewRegisterUsecase( //ユースケースの初期化
        repository.NewUserRepository(sqlc.NewSqlcQuerier()),  //リポジトリ・sqlc具体型の初期化
        userDomain.NewUserDomainService(repository.NewUserRepository(sqlc.NewSqlcQuerier())),  //ユーザードメインサービスの初期化
    ),
)

このように、NewXxx()といったコンストラクタ(インスタンスを初期化する関数)によって依存性を注入する方法を、コンストラクタインジェクションといいます。

ハンドラーと同じく、ミドルウェアも同じようにDIしているので確認しておいてください。

ハンドラーをルーティングに登録

registerRoutes()では、ハンドラをマルチプレクサに登録しています。

// ユーザー系ルーティング
	{
		mux.Handle("POST /users", composeMiddlewares(logger)(postUserHandler))
		mux.Handle("DELETE /users/me", composeMiddlewares(logger, authorization)(deleteUserHandler))
		mux.Handle("GET /users", composeMiddlewares(logger, authorization)(getUsersHandler))
		mux.Handle("PATCH /users/me", composeMiddlewares(logger, authorization)(updateUserHandler))
	}

http.ServeMux型のHandle()メソッドやHandleFunc()メソッドに関しては先述しました。また、前章で実装したcomposeMiddlewares()関数を使用することでミドルウェアを適用していています。

ユーザー登録のエンドポイント(POST /users)に関しては、認可制御を設けないため、ミドルウェアをロガーのみに絞っています。

ここでは、「ルーティングパターン」について取り上げます。

強化されたルーティングパターン

Handle()メソッドなどについては説明しましたが、ルーティングパターンについてはまだ説明していませんでした。ルーティングパターンと言っているのは、以下のpatternのことです。

func (mux *http.ServeMux) Handle(pattern string, handler http.Handler)

Go1.22から、ルーティングパターンは拡張されたようです。

https://tip.golang.org/doc/go1.22#enhanced_routing_patterns

バージョンアップによる拡張で以下のようなことが可能になりました。

  • HTTPメソッドの指定
  • パスパラメータの表現

順に取り上げてみます。

<HTTPメソッドの指定> あるパスに対して、リクエストを受けつけるHTTPメソッドを指定できるようになりました。以下のように、HTTPメソッド /パスの形式で記述できます。

"POST /users"

<パスパラメータの表現> パスパラメータとして扱いたい部分を {}で囲むことでそれを表現できます。例えば、以下のように記述すると、

"GET /tasks/{id}"

以下のようなルーティングパターンがマッチするようになります。

"GET /tasks/1"   //idが1のタスクを取得
"GET /tasks/2"   //idが2のタスクを取得
"GET /tasks/3"   //idが3のタスクを取得

また、パスパラメータを取得するために、PathValue()メソッドが用意されています。

func (r *http.Request) PathValue(name string) string

このメソッドは、いくつかのハンドラーで使用していました。

func (gth *GetTaskHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
    // GET /tasks/1 でリクエストを受け付けた場合、idには"1"が入る
    id := r.PathValue("id")

    // 省略
}

/hoge/{id}の{id}の部分はワイルドカードとも呼ばれています。{id}が何であってもルーティングパターンとマッチするという点で、確かにワイルドカードと言えそうですね。他にも、/hoge/{id...}とすることで、/hogeといったパスパラメータがないパターンもマッチするように出来る書き方もあります。

main()で呼び出す

ここまでに実装した処理を呼び出しましょう。以下の二行をrun()関数内に追加してください。

// app/main.go
func run(ctx context.Context, cfg *config.Config) {
    // 省略

	// サーバーを起動し、指定したポートでリクエストを処理
	srv := server.NewServer(cfg.Server.Port, router.NewMux())
	srv.Run(ctx)
}

router.NewMux()でルーティングを登録したマルチプレクサを、server.NewServer()に渡しています。また、ポート番号には環境変数を渡しています。

実際にエンドポイントを叩いてみる

これで、Todo APIの機能は全て実装し終えました(残すはエンドポイントの統合テストのみです)

まずは、コンテナを起動し、サーバーが起動している様子を確かめて見ましょう。

make up

サーバーがポート番号8080で起動していますね。

todo-api  | 2025/02/04 03:00:22 successfully connected to db (attempt 3/5)
todo-api  | 2025/02/04 03:00:22 successfully connected to redis
todo-api  | 2025/02/04 03:00:22 server is runnning at port ":8080"...

Postmanというツールを使用して、エンドポイントを叩いてみます。

https://www.postman.com/

※エンドポイントを叩くツールは色々ありますので、何を使ってもらっても構いません。

VSCodeを使用している場合は、拡張機能をインストールしてみてください。

左端のツールバーにPostmanのアイコンが表示されると思いますので、そちらをクリックしましょう。

New HTTP Request というボタンがあるのでクリックしてください。タブが開きます。

ここでは簡単に、ヘルスチェックを行います。http://localhost:8080/healthをGETメソッドでリクエストを送信してみてください。 11章で定義したHealthCheckHandklerの処理を呼び出せていれば、レスポンスボディ・ステータスコードが以下のように表示されます。 (小さくてすみません...位置関係だけ把握していただければ🙏)

期待通りにレスポンスが返ってきていることを確認してください。以下のようにレスポンスが返っていれば成功です。また、ステータスコードは200となっているはずです。

{
    "status": 200,
    "data": {
        "health_check": "ok"
    }
}

他のエンドポイントも期待通り動くはずなので、試してみましょう。

http://localhost:8080/usersにPOSTメソッドでリクエストをした場合、以下のようにログが書き込まれることも確認できます。

todo-api  | =====================================================================================================
todo-api  | [REQUEST]
todo-api  | Timestamp       : 2025-02-04T06:35:55Z
todo-api  | Method          : POST
todo-api  | Path            : /users
todo-api  | Authorization   :
todo-api  | Body            :
todo-api  | {
todo-api  |     "name": "fuga",
todo-api  |     "email": "[email protected]",
todo-api  |     "password": "fugafura"
todo-api  | }
todo-api  | -----------------------------------------------------------------------------------------------------
todo-api  | [RESPONSE]
todo-api  | Timestamp       : 2025-02-04T06:35:55Z
todo-api  | Status          : 201
todo-api  | Body            :
todo-api  | {
todo-api  |     "status": 201,
todo-api  |     "data": {
todo-api  |         "id": "01JK7TRBTQM35NVZ68F9KV759R",
todo-api  |         "email": "[email protected]",
todo-api  |         "name": "fuga"
todo-api  |     }
todo-api  | }
todo-api  | =====================================================================================================

グレースフルシャットダウンについて

Ctrl + Cで中断シグナルを送ってみましょう。すると以下のようにログが出るはずです。

todo-api  | 2025/02/04 03:00:22 successfully connected to db (attempt 3/5)
todo-api  | 2025/02/04 03:00:22 successfully connected to redis
todo-api  | 2025/02/04 03:00:22 server is runnning at port ":8080"...
Gracefully stopping... (press Ctrl+C again to force)  // Ctrl + Cをここで押した

Gracefully stopping...とあり、グレースフルシャットダウンが行われていることが確認できますね。しかし、これは本章で実装したコードによる挙動ではないことに注意してください。

これは、docker composeの挙動です。

おそらく、Ctrl + Cで中断シグナルを送った先がdocker composeのプロセスになるからだと思われます。

自身で実装したグレースフルシャットダウンが機能するのかを確かめたい場合は、dockerからではなく、ローカルでサーバーを起動してみる必要があります。以下のコマンドを叩いてください。

go run app/main.go

※ main()が呼び出しているrun()関数におけるDB・Redisの起動処理はコメントアウトしておいてください。

すると、以下のようにサーバーが起動します。

2025/02/04 13:00:51 server is runnning at port ""...

ポート番号を環境変数として設定できていない(コンテナ内で行っていた)ので、デフォルトである80番ポートが使用されています。

ここで、Ctrl + Cを押して見ましょう。

2025/02/04 13:00:51 server is runnning at port ""...
^C2025/02/04 13:03:01 The server on  is gracefully shutting down
2025/02/04 13:03:01 server was successfully shut down gracefully

自身で書いたシャットダウン処理が実行されていることがわかりますね。

swaggoのエンドポイントを通す

11章にてswaggoを使ったドキュメンテーションを生成しました。これを閲覧するには、エンドポイントを通して、それにアクセスする必要があります。

11章でインストールしたswaggo/http-swaggerパッケージには、swaggoで生成したドキュメンテーションを表示するためのハンドラーが定義されています。それをルーティングに登録して、エンドポイントを叩いて見ましょう。

https://github.com/swaggo/http-swagger

今回使用しているswaggo/http-swaggerは、net/httpに対応したものです。つまり、net/httpで実装されたハンドラーを提供してくれます。

swaggoは、非常に多くのwebフレームワークをサポートしています。

// app/infrastructure/router/mux.go
import (
	"net/http"

	"github.com/kakkky/app/adapter/presentation/handler/health"
	swagger "github.com/swaggo/http-swagger"  // 追加
)

// 開発用ルーティング
{
    mux.HandleFunc("GET /health", health.HealthCheckHandler)
    mux.Handle("GET /swagger/", swagger.WrapHandler) // 追加
}

記述ができれば、コンテナを起動して、http://localhost:8080/swagger/にアクセスして見ましょう。以下のようなページが表示されます。

これで、APIドキュメンテーションを閲覧できるようになりました。

現段階のディレクトリ

ひとまず、APIの機能自体はこれで実装完了です!! ドメイン〜ハンドラーまで、ボトムアップにコンポーネントを組み立て、遂にこの章でエンドポイントを通すことができました。

あとは、このエンドポイントのテストを書くのみとなりました。

以下に現段階でのディレクトリを示しておきます。

.
├── .github
│   └── workflows
│       └── test.yml
├── .gitignore
├── Dockerfile.app
├── Makefile
├── app
│   ├── .air.toml
│   ├── adapter
│   │   ├── presentation
│   │   │   ├── handler
│   │   │   │   ├── auth
│   │   │   │   │   ├── login_handler.go
│   │   │   │   │   ├── login_models.go
│   │   │   │   │   └── logout_handler.go
│   │   │   │   ├── health
│   │   │   │   │   ├── handler.go
│   │   │   │   │   └── health_models.go
│   │   │   │   ├── task
│   │   │   │   │   ├── delete_task_handler.go
│   │   │   │   │   ├── get_task_handler.go
│   │   │   │   │   ├── get_task_models.go
│   │   │   │   │   ├── get_tasks_handler.go
│   │   │   │   │   ├── get_user_tasks_handler.go
│   │   │   │   │   ├── get_user_tasks_models.go
│   │   │   │   │   ├── post_task_handler.go
│   │   │   │   │   ├── post_task_models.go
│   │   │   │   │   ├── update_task_state_handler.go
│   │   │   │   │   └── update_task_state_models.go
│   │   │   │   └── user
│   │   │   │       ├── delete_user_handler.go
│   │   │   │       ├── get_users_handler.go
│   │   │   │       ├── get_users_models.go
│   │   │   │       ├── post_user_handler.go
│   │   │   │       ├── post_user_models.go
│   │   │   │       ├── update_user_handler.go
│   │   │   │       └── update_user_models.go
│   │   │   ├── middleware
│   │   │   │   ├── authorization.go
│   │   │   │   └── logger.go
│   │   │   └── presenter
│   │   │       ├── failure.go
│   │   │       └── success.go
│   │   ├── queryservice
│   │   │   ├── interface_querier.go
│   │   │   └── task_queryservice.go
│   │   └── repository
│   │       ├── interface_kvs_commander.go
│   │       ├── interface_querier.go
│   │       ├── task_repository.go
│   │       ├── token_authenticator_repository.go
│   │       └── user_repository.go
│   ├── application
│   │   └── usecase
│   │       ├── auth
│   │       │   ├── authorization_usecase.go
│   │       │   ├── authorization_usecase_dto.go
│   │       │   ├── authorization_usecase_test.go
│   │       │   ├── interface_jwt_authenticator.go
│   │       │   ├── interface_jwt_authenticator_repository.go
│   │       │   ├── login_usecase.go
│   │       │   ├── login_usecase_dto.go
│   │       │   ├── login_usecase_test.go
│   │       │   ├── logout_usecase.go
│   │       │   ├── logout_usecase_dto.go
│   │       │   ├── logout_usecase_test.go
│   │       │   ├── mock_jwt_authenticator.go
│   │       │   └── mock_jwt_authenticator_repository.go
│   │       ├── task
│   │       │   ├── create_task_usecase.go
│   │       │   ├── create_task_usecase_dto.go
│   │       │   ├── create_task_usecase_test.go
│   │       │   ├── delete_task_usecase.go
│   │       │   ├── delete_task_usecase_dto.go
│   │       │   ├── delete_task_usecase_test.go
│   │       │   ├── fetch_task_usecase.go
│   │       │   ├── fetch_task_usecase_dto.go
│   │       │   ├── fetch_task_usecase_test.go
│   │       │   ├── fetch_tasks_usecase.go
│   │       │   ├── fetch_tasks_usecase_test.go
│   │       │   ├── fetch_user_tasks_usecase.go
│   │       │   ├── fetch_user_tasks_usecase_dto.go
│   │       │   ├── fetch_user_tasks_usecase_test.go
│   │       │   ├── interface_task_query_service.go
│   │       │   ├── mock_task_query_service.go
│   │       │   ├── update_task_state_usecase.go
│   │       │   ├── update_task_state_usecase_dto.go
│   │       │   └── update_task_state_usecase_test.go
│   │       └── user
│   │           ├── fetch_users_usecase.go
│   │           ├── fetch_users_usecase_dto.go
│   │           ├── fetch_users_usecase_test.go
│   │           ├── register_usecase.go
│   │           ├── register_usecase_dto.go
│   │           ├── register_usecase_test.go
│   │           ├── unregister_usecase.go
│   │           ├── unregister_usecase_dto.go
│   │           ├── unregister_usecase_test.go
│   │           ├── update_profile_usecase.go
│   │           ├── update_profile_usecase_dto.go
│   │           └── update_profile_usecase_test.go
│   ├── config
│   │   ├── config.go
│   │   └── config_test.go
│   ├── docs
│   │   ├── docs.go
│   │   ├── swagger.json
│   │   └── swagger.yaml
│   ├── domain
│   │   ├── errors
│   │   │   ├── errors.go
│   │   │   └── errors_test.go
│   │   ├── task
│   │   │   ├── interface_task_repository.go
│   │   │   ├── mock_task_repository.go
│   │   │   ├── task.go
│   │   │   ├── task_content.go
│   │   │   ├── task_state.go
│   │   │   └── task_test.go
│   │   └── user
│   │       ├── interface_user_domain_service.go
│   │       ├── interface_user_repository.go
│   │       ├── mock_user_domain_service.go
│   │       ├── mock_user_repository.go
│   │       ├── user.go
│   │       ├── user_domain_service.go
│   │       ├── user_domain_service_test.go
│   │       ├── user_email.go
│   │       ├── user_hashed_password.go
│   │       ├── user_test.go
│   │       └── users.go
│   ├── go.mod
│   ├── go.sum
│   ├── infrastructure
│   │   ├── auth
│   │   │   ├── certificate
│   │   │   │   ├── private.pem
│   │   │   │   └── public.pem
│   │   │   ├── jwt_authenticator.go
│   │   │   ├── jwt_authenticator_test.go
│   │   │   └── key_parse.go
│   │   ├── db
│   │   │   ├── connect.go
│   │   │   ├── container
│   │   │   │   ├── db.go
│   │   │   │   └── dockertest.go
│   │   │   ├── migrations
│   │   │   │   ├── 000001_create_users_table.down.sql
│   │   │   │   ├── 000001_create_users_table.up.sql
│   │   │   │   ├── 000002_create_tasks_table.down.sql
│   │   │   │   └── 000002_create_tasks_table.up.sql
│   │   │   ├── mysql
│   │   │   │   └── conf.d
│   │   │   │       └── my.conf
│   │   │   ├── sqlc
│   │   │   │   ├── db.go
│   │   │   │   ├── models.go
│   │   │   │   ├── queries
│   │   │   │   │   ├── tasks.sql
│   │   │   │   │   └── users.sql
│   │   │   │   ├── sqlc.yml
│   │   │   │   ├── sqlc_querier.go
│   │   │   │   ├── tasks.sql.go
│   │   │   │   └── users.sql.go
│   │   │   └── testhelper
│   │   │       └── fixture.go
│   │   ├── kvs
│   │   │   ├── redis_client.go
│   │   │   ├── redis_commander.go
│   │   │   └── redis_test_client.go
│   │   ├── queryservice_test
│   │   │   ├── queryservice_test.go
│   │   │   ├── task_queryservice_test.go
│   │   │   └── testdata
│   │   │       └── fixtures
│   │   │           ├── tasks.yml
│   │   │           └── users.yml
│   │   ├── repository_test
│   │   │   ├── jwt_authenticator_repository_test.go
│   │   │   ├── repository_test.go
│   │   │   ├── task_repository_test.go
│   │   │   ├── testdata
│   │   │   │   └── fixtures
│   │   │   │       ├── tasks.yml
│   │   │   │       └── users.yml
│   │   │   └── user_repository_test.go
│   │   ├── router
│   │   │   ├── handlers.go
│   │   │   ├── middlewares.go
│   │   │   ├── middlewares_test.go
│   │   │   ├── mux.go
│   │   │   └── mux_test.go
│   │   └── server
│   │       ├── server.go
│   │       └── server_test.go
│   ├── main.go
│   └── tmp
│       ├── build-errors.log
│       └── main
├── compose.yml
├── go.work
├── go.work.sum
└── pkg
    ├── go.mod
    ├── go.sum
    ├── hash
    │   ├── hash.go
    │   └── hash_test.go
    ├── ulid
    │   ├── ulid.go
    │   └── ulid_test.go
    └── validation
        └── validator.go