From 131d6c8bc4287bea034eff664585d4f2549e4311 Mon Sep 17 00:00:00 2001 From: murasame29 Date: Mon, 16 Feb 2026 00:30:28 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20AGENTS.md=20=E4=BB=95=E6=A7=98?= =?UTF-8?q?=E3=81=AB=E6=BA=96=E6=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Echo を net/http.ServeMux に変更 - Config をグローバル変数から DI 経由に変更 - internal/config に設定を移動 - Health Check を /livez, /readyz に変更 - Middleware パターン (Recovery, Logging) を追加 - pkg/lifecycle.Application インターフェースを追加 - Go 1.24 に更新 --- .docker/app/.air.toml | 48 ++++--------- .docker/app/Dockerfile | 22 +++--- .docker/app/local.Dockerfile | 14 +++- .env.template | 9 ++- README.md | Bin 1246 -> 2590 bytes cmd/app/main.go | 47 +++++------- cmd/config/config.go | 27 ------- cmd/config/type.go | 15 ---- compose.yaml | 15 ++-- go.mod | 15 +--- go.sum | 43 +---------- internal/config/config.go | 41 +++++++++++ internal/container/{dig.go => container.go} | 22 ++++-- internal/framework/contexts/context.go | 40 ----------- internal/middleware/logging.go | 36 ++++++++++ internal/middleware/middleware.go | 18 +++++ internal/middleware/recovery.go | 23 ++++++ internal/router/router.go | 33 ++++++--- internal/server/option.go | 50 ------------- internal/server/server.go | 76 +++++++++----------- pkg/lifecycle/lifecycle.go | 9 +++ 21 files changed, 271 insertions(+), 332 deletions(-) delete mode 100644 cmd/config/config.go delete mode 100644 cmd/config/type.go create mode 100644 internal/config/config.go rename internal/container/{dig.go => container.go} (54%) delete mode 100644 internal/framework/contexts/context.go create mode 100644 internal/middleware/logging.go create mode 100644 internal/middleware/middleware.go create mode 100644 internal/middleware/recovery.go delete mode 100644 internal/server/option.go create mode 100644 pkg/lifecycle/lifecycle.go diff --git a/.docker/app/.air.toml b/.docker/app/.air.toml index ad85169..18b027b 100644 --- a/.docker/app/.air.toml +++ b/.docker/app/.air.toml @@ -1,46 +1,24 @@ -root = "." -testdata_dir = "testdata" +root = "/app" tmp_dir = "tmp" [build] - bin = "./.bin/main" - cmd = "go build -o ./.bin/main cmd/app/main.go" - delay = 1000 - exclude_dir = ["assets", "tmp", "vendor", "testdata"] - exclude_file = [] - exclude_regex = ["_test.go"] - exclude_unchanged = false - follow_symlink = false - full_bin = "" - include_dir = [] + cmd = "go build -o ./tmp/main ./cmd/app" + bin = "./tmp/main" + full_bin = "./tmp/main -e .env" include_ext = ["go", "tpl", "tmpl", "html"] - include_file = [] - kill_delay = "0s" + exclude_dir = ["assets", "tmp", "vendor", ".git"] + delay = 1000 + stop_on_error = true log = "build-errors.log" - poll = false - poll_interval = 0 - post_cmd = [] - pre_cmd = [] - rerun = false - rerun_delay = 500 - send_interrupt = false - stop_on_error = false - args_bin = ["-e", ".env"] + +[log] + time = false [color] - app = "" - build = "yellow" main = "magenta" - runner = "green" watcher = "cyan" - -[log] - main_only = false - time = false + build = "yellow" + runner = "green" [misc] - clean_on_exit = false - -[screen] - clear_on_rebuild = false - keep_scroll = true \ No newline at end of file + clean_on_exit = true diff --git a/.docker/app/Dockerfile b/.docker/app/Dockerfile index 04b2684..4c60c16 100644 --- a/.docker/app/Dockerfile +++ b/.docker/app/Dockerfile @@ -1,19 +1,17 @@ -FROM golang:1.23.2 AS builder -WORKDIR /build +FROM golang:1.24-alpine AS builder -COPY . . - -RUN CGO_ENABLED=0 go build -o /build/main ./cmd/app/main.go - -FROM gcr.io/distroless/static-debian11:nonroot WORKDIR /app -COPY --from=builder /build/main / +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server ./cmd/app -ENV PORT= +FROM gcr.io/distroless/static-debian12 -EXPOSE ${PORT} +COPY --from=builder /app/server /server -USER 1000 +EXPOSE 8080 -CMD ["/main"] \ No newline at end of file +ENTRYPOINT ["/server"] diff --git a/.docker/app/local.Dockerfile b/.docker/app/local.Dockerfile index 14ec4c5..100e58f 100644 --- a/.docker/app/local.Dockerfile +++ b/.docker/app/local.Dockerfile @@ -1,6 +1,14 @@ -FROM golang:1.22.2 AS app +FROM golang:1.24-alpine + WORKDIR /app -RUN go install github.com/cosmtrek/air@latest +RUN go install github.com/air-verse/air@latest + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +EXPOSE 8080 -CMD ["air","-c",".docker/app/.air.toml"] \ No newline at end of file +CMD ["air", "-c", ".docker/app/.air.toml"] diff --git a/.env.template b/.env.template index c8204b5..6f63226 100644 --- a/.env.template +++ b/.env.template @@ -1,4 +1,7 @@ -# server envrionment variables -HOST=0.0.0.0 +# Server +HOST=localhost PORT=8080 -SHUTDOWN_TIMEOUT=5s +SHUTDOWN_TIMEOUT=10s +READ_TIMEOUT=30s +WRITE_TIMEOUT=30s +IDLE_TIMEOUT=60s diff --git a/README.md b/README.md index 07604b073437024711eaba75c1d8c754ab5f63e6..d13c7cf336d9301cfb272a5c74349d1b85cfb8b9 100644 GIT binary patch literal 2590 zcmbVOO=whC6h35QQi!fe|;1#&uRd zS5Lx_Na0CfkH5Z5ksW5A;hVO`pq(%=wJ}J8m5;p`@FyVK3uZaUjY94mBt3aK__v80 z5pU zj`<3(e#KcX;k^OW2z;G@KC|Qk{3R;aXnh!Lrp&5EX4LF?-_@%)kyVoqE}q+dQ<~X~p5wf97`@Z~7gydGzo;#94N&iQn6b9!D(*mvR>20iim0W%$%#t%Eo|*+sLwYoa(OL3gEa? zdb5|5GIEg&GIM=|=j2dQGHzneHK*nFn1wO$WL^2D3d-%!LW{{4O8df2W~y0yrOw3ZGK#EmTXXb}x8*0_`wR`lU;TFmK1L){1YgJB)}l12I$h zzN7!@FF^YlU5pjHqz}-`mdo&afYbu?>bM^Ua3AA-S}|U_?p+mSf{xgs-w4qb;o~Ve-vQ8h#0V|S3=}z**^9NQa>05F7G;x5Byia-pDS*uFc(7 D*{u9! literal 1246 zcmaJ>O>0v@6ur6;1;K?s;6W)Um~z%dH_~b~v7rgtmVmWw(!8YYYj|l>gredHMB8GC z=%OIv2X0)n8^3mf7z=K65p*fI5nFK4ok;cEnT~m-MTYn0W$roW-kEzQe>Pggle|nw zOv;jzP%7pflS$6wQj#$lG~fGWpPVF4e+OiEa)^jw@ws+b0(22INqjn@QpY+zV-3Ht z0*?&UaGX3sGN+7oC<$zoUxW`b@2Y1phY@sO776&9nr+f$vSyiGYpmf)Fo0fzn?ngj z+(8~!k(4;I*gV}tPD_gKF)6Ul0IFW2auQj@4f{B=^q_#t$=lS3V;BRtiPNd-#l;~8sT!&|(9Uv0mrsQ=ZjzA3YBO`OxV=l33u zskTJ@ANWu>FtimnBhu=zTjP>7ofI^OCr9a07kzZMWg$QZyAYaOGY0R;!B$lmq!UYA zQFRy5e(JCYJ?#Qdw#~R-*R6V*4#~2Ewr9$>c*W}8Le7Kiyi;q=mVcw?`@;1~(YQ_7 zF79?VLB4uNZ%}kt_VVPlzPz>2|deooIE$xYg-dEgRj=irzcN zu(Rc@(yeuNX&uY-fXluM4Re#f;wR%v7{&c$Uq;>RBaQhj)!7q^{-5lWCmIion|0SO h@r_ 0 { - if err := godotenv.Load(path...); err != nil { - return fmt.Errorf("failed to load env: %w", err) - } - } - - config := &config{} - - if err := env.Parse(&config.Server); err != nil { - return err - } - - // ... configに構造体を追加したら env.Parseする - Config = config - - return nil -} diff --git a/cmd/config/type.go b/cmd/config/type.go deleted file mode 100644 index 5e9882e..0000000 --- a/cmd/config/type.go +++ /dev/null @@ -1,15 +0,0 @@ -package config - -import "time" - -// config はアプリケーションの設定を表す構造体です。基本的には環境変数から読み込みます。 -type config struct { - Server struct { - Host string `env:"HOST" envDefault:"localhost"` - Port int `env:"PORT" envDefault:"8080"` - ShutdownTimeout time.Duration `env:"SHUTDOWN_TIMEOUT" envDefault:"10s"` - } -} - -// Config は読み込まれた設定を保持します。 -var Config *config diff --git a/compose.yaml b/compose.yaml index bff07b4..b56c735 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,10 +1,13 @@ services: - backend: + app: build: - context: .docker/app - dockerfile: local.Dockerfile - platform: linux/amd64 + context: . + dockerfile: .docker/app/local.Dockerfile ports: - - 8080:8080 + - "8080:8080" volumes: - - ./:/app \ No newline at end of file + - .:/app + env_file: + - .env.template + environment: + - HOST=0.0.0.0 diff --git a/go.mod b/go.mod index f7a56ad..e08fa68 100644 --- a/go.mod +++ b/go.mod @@ -1,23 +1,10 @@ module github.com/murasame29/go-httpserver-template -go 1.22.2 +go 1.24.0 require ( github.com/caarlos0/env/v11 v11.2.2 github.com/joho/godotenv v1.5.1 - github.com/labstack/echo/v4 v4.12.0 go.uber.org/dig v1.18.0 golang.org/x/sync v0.9.0 ) - -require ( - github.com/labstack/gommon v0.4.2 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasttemplate v1.2.2 // indirect - golang.org/x/crypto v0.29.0 // indirect - golang.org/x/net v0.31.0 // indirect - golang.org/x/sys v0.27.0 // indirect - golang.org/x/text v0.20.0 // indirect -) diff --git a/go.sum b/go.sum index ac25aaa..35fcb9b 100644 --- a/go.sum +++ b/go.sum @@ -1,55 +1,16 @@ -github.com/caarlos0/env/v11 v11.0.0 h1:ZIlkOjuL3xoZS0kmUJlF74j2Qj8GMOq3CDLX/Viak8Q= -github.com/caarlos0/env/v11 v11.0.0/go.mod h1:2RC3HQu8BQqtEK3V4iHPxj0jOdWdbPpWJ6pOueeU1xM= github.com/caarlos0/env/v11 v11.2.2 h1:95fApNrUyueipoZN/EhA8mMxiNxrBwDa+oAZrMWl3Kg= github.com/caarlos0/env/v11 v11.2.2/go.mod h1:JBfcdeQiBoI3Zh1QRAWfe+tpiNTmDtcCj/hHHHMx0vc= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8= -github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8= -github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= -github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= -github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= -github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= -github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc= -go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw= go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= -golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= -golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= -golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..8946b51 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,41 @@ +package config + +import ( + "fmt" + "time" + + "github.com/caarlos0/env/v11" + "github.com/joho/godotenv" +) + +// Config はアプリケーションの設定を表す構造体です。 +type Config struct { + Server ServerConfig +} + +// ServerConfig はサーバーの設定を表す構造体です。 +type ServerConfig struct { + Host string `env:"HOST" envDefault:"localhost"` + Port int `env:"PORT" envDefault:"8080"` + ShutdownTimeout time.Duration `env:"SHUTDOWN_TIMEOUT" envDefault:"10s"` + ReadTimeout time.Duration `env:"READ_TIMEOUT" envDefault:"30s"` + WriteTimeout time.Duration `env:"WRITE_TIMEOUT" envDefault:"30s"` + IdleTimeout time.Duration `env:"IDLE_TIMEOUT" envDefault:"60s"` +} + +// Load は環境変数から設定を読み込みます。 +func Load(paths ...string) (*Config, error) { + if len(paths) > 0 { + if err := godotenv.Load(paths...); err != nil { + return nil, fmt.Errorf("failed to load env: %w", err) + } + } + + cfg := &Config{} + + if err := env.Parse(&cfg.Server); err != nil { + return nil, fmt.Errorf("failed to parse server config: %w", err) + } + + return cfg, nil +} diff --git a/internal/container/dig.go b/internal/container/container.go similarity index 54% rename from internal/container/dig.go rename to internal/container/container.go index 26f0df5..10bfeaa 100644 --- a/internal/container/dig.go +++ b/internal/container/container.go @@ -1,7 +1,11 @@ package container import ( + "net/http" + + "github.com/murasame29/go-httpserver-template/internal/config" "github.com/murasame29/go-httpserver-template/internal/router" + "github.com/murasame29/go-httpserver-template/internal/server" "go.uber.org/dig" ) @@ -12,14 +16,20 @@ type provideArg struct { opts []dig.ProvideOption } -// NewContainer は、digを用いて依存注入を行う -func NewContainer() error { - var noOpts []dig.ProvideOption +// New は dig を用いて依存注入コンテナを初期化します。 +func New(cfg *config.Config) error { container = dig.New() args := []provideArg{ - {constructor: router.NewEcho, opts: noOpts}, - // {constructor: db.NewRepository, opts: as[dai.DataAccessInterfce]()}, + // Config + {constructor: func() *config.Config { return cfg }}, + {constructor: func() *config.ServerConfig { return &cfg.Server }}, + + // Router + {constructor: router.NewServeMux, opts: as[http.Handler]()}, + + // Server + {constructor: server.New}, } for _, arg := range args { @@ -35,7 +45,7 @@ func as[T any]() []dig.ProvideOption { return []dig.ProvideOption{dig.As(new(T))} } -// Invoke は、 *dig.ContainerのInvokeをwrapしてる関数 +// Invoke は *dig.Container の Invoke をラップした関数です。 func Invoke[T any](opts ...dig.InvokeOption) (T, error) { var r T if err := container.Invoke(func(t T) error { diff --git a/internal/framework/contexts/context.go b/internal/framework/contexts/context.go deleted file mode 100644 index c1f3be8..0000000 --- a/internal/framework/contexts/context.go +++ /dev/null @@ -1,40 +0,0 @@ -package contexts - -import ( - "context" - - "github.com/labstack/echo/v4" -) - -type ContextKey string - -func (c ContextKey) String() string { - return string(c) -} - -const ( - RequestID ContextKey = "x-request-id" -) - -var contextkeys = []ContextKey{ - RequestID, -} - -func GetRequestID(ctx context.Context) string { - v, ok := ctx.Value(RequestID.String()).(string) - if !ok { - return "" - } - return v -} - -// ConvertContext はecho.Contextのkeyをcopyしてcontext.Contextに変換 -func ConvertContext(c echo.Context) context.Context { - ctx := context.Background() - for _, key := range contextkeys { - v := c.Get(key.String()) - ctx = context.WithValue(ctx, key, v) - } - - return ctx -} diff --git a/internal/middleware/logging.go b/internal/middleware/logging.go new file mode 100644 index 0000000..289af43 --- /dev/null +++ b/internal/middleware/logging.go @@ -0,0 +1,36 @@ +package middleware + +import ( + "log/slog" + "net/http" + "time" +) + +// responseWriter はステータスコードを記録するためのラッパーです。 +type responseWriter struct { + http.ResponseWriter + statusCode int +} + +func (rw *responseWriter) WriteHeader(code int) { + rw.statusCode = code + rw.ResponseWriter.WriteHeader(code) +} + +// Logging はリクエストをログに記録するミドルウェアです。 +func Logging(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK} + next.ServeHTTP(rw, r) + + slog.InfoContext(r.Context(), "request completed", + "method", r.Method, + "path", r.URL.Path, + "status", rw.statusCode, + "duration", time.Since(start), + "remote_addr", r.RemoteAddr, + ) + }) +} diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go new file mode 100644 index 0000000..279d404 --- /dev/null +++ b/internal/middleware/middleware.go @@ -0,0 +1,18 @@ +package middleware + +import "net/http" + +// Middleware は http.Handler をラップする関数型です。 +type Middleware func(http.Handler) http.Handler + +// Chain は複数のミドルウェアを連結します。 +// 順序: 最初に渡したものが最も外側になります。 +// 例: Chain(recovery, logging, tracing) → recovery(logging(tracing(handler))) +func Chain(middlewares ...Middleware) Middleware { + return func(next http.Handler) http.Handler { + for i := len(middlewares) - 1; i >= 0; i-- { + next = middlewares[i](next) + } + return next + } +} diff --git a/internal/middleware/recovery.go b/internal/middleware/recovery.go new file mode 100644 index 0000000..543295c --- /dev/null +++ b/internal/middleware/recovery.go @@ -0,0 +1,23 @@ +package middleware + +import ( + "log/slog" + "net/http" + "runtime/debug" +) + +// Recovery はパニックをリカバリーするミドルウェアです。 +func Recovery(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if err := recover(); err != nil { + slog.ErrorContext(r.Context(), "panic recovered", + "error", err, + "stack", string(debug.Stack()), + ) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } + }() + next.ServeHTTP(w, r) + }) +} diff --git a/internal/router/router.go b/internal/router/router.go index 961ac1d..268b501 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -3,16 +3,33 @@ package router import ( "net/http" - "github.com/labstack/echo/v4" + "github.com/murasame29/go-httpserver-template/internal/middleware" ) -// NewEcho は、echo/v4 を利用した http.Handlerを返す関数です。 -func NewEcho() http.Handler { - engine := echo.New() +// NewServeMux は Go 1.22+ の net/http.ServeMux を利用した http.Handler を返します。 +func NewServeMux() http.Handler { + mux := http.NewServeMux() - engine.GET("/healthz", func(c echo.Context) error { - return c.String(200, "OK") - }) + // Health Check endpoints + mux.HandleFunc("GET /livez", handleLivez) + mux.HandleFunc("GET /readyz", handleReadyz) - return engine + // Middleware を適用 + handler := middleware.Chain( + middleware.Recovery, + middleware.Logging, + )(mux) + + return handler +} + +func handleLivez(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) +} + +func handleReadyz(w http.ResponseWriter, r *http.Request) { + // TODO: DB接続などの準備状態をチェック + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) } diff --git a/internal/server/option.go b/internal/server/option.go deleted file mode 100644 index 1c9f74a..0000000 --- a/internal/server/option.go +++ /dev/null @@ -1,50 +0,0 @@ -package server - -import ( - "time" -) - -// Option は...(ry -type Option func(*Server) - -// WithPort はポート番号を設定するオプションです。 -func WithPort(port int) Option { - return func(s *Server) { - s.port = port - } -} - -// WithHost はホスト名を設定するオプションです。 -func WithHost(host string) Option { - return func(s *Server) { - s.host = host - } -} - -// WithReadTimeout はリクエストの読み込みタイムアウトを設定するオプションです。 -func WithReadTimeout(timeout time.Duration) Option { - return func(s *Server) { - s.srv.ReadTimeout = timeout - } -} - -// WithWriteTimeout はレスポンスの書き込みタイムアウトを設定するオプションです。 -func WithWriteTimeout(timeout time.Duration) Option { - return func(s *Server) { - s.srv.WriteTimeout = timeout - } -} - -// WithIdleTimeout はアイドルタイムアウトを設定するオプションです。 -func WithIdleTimeout(timeout time.Duration) Option { - return func(s *Server) { - s.srv.IdleTimeout = timeout - } -} - -// WithShutdownTimeout はシャットダウンタイムアウトを設定するオプションです。 -func WithShutdownTimeout(timeout time.Duration) Option { - return func(s *Server) { - s.shutdownTimeout = timeout - } -} diff --git a/internal/server/server.go b/internal/server/server.go index 958043c..e4be376 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -7,85 +7,73 @@ import ( "net/http" "os/signal" "syscall" - "time" + "github.com/murasame29/go-httpserver-template/internal/config" "golang.org/x/sync/errgroup" ) -// DefaultShutdownTimeout はデフォルトのシャットダウンタイムアウトです。 -const DefaultShutdownTimeout time.Duration = 10 - // Server はHTTPサーバーを表します。 +// pkg/lifecycle.Application インターフェースを実装しています。 type Server struct { - port int - host string - shutdownTimeout time.Duration - + cfg *config.ServerConfig srv *http.Server } // New はサーバーを生成します。 -func New(handler http.Handler, opts ...Option) *Server { - server := &Server{ - port: 8080, - host: "localhost", - shutdownTimeout: DefaultShutdownTimeout, - srv: new(http.Server), - } - - for _, opt := range opts { - opt(server) +func New(cfg *config.ServerConfig, handler http.Handler) *Server { + srv := &http.Server{ + Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port), + Handler: handler, + ReadTimeout: cfg.ReadTimeout, + WriteTimeout: cfg.WriteTimeout, + IdleTimeout: cfg.IdleTimeout, } - server.srv = &http.Server{ - Addr: fmt.Sprintf("%s:%d", server.host, server.port), - Handler: handler, + return &Server{ + cfg: cfg, + srv: srv, } - - return server } // Run はサーバーを起動します。 func (s *Server) Run(ctx context.Context) error { - slog.Info("server starting", "addr", s.srv.Addr) - return s.srv.ListenAndServe() + slog.InfoContext(ctx, "server starting", "addr", s.srv.Addr) + if err := s.srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + return fmt.Errorf("listen and serve: %w", err) + } + return nil } // Shutdown はサーバーを停止します。 func (s *Server) Shutdown(ctx context.Context) error { - slog.Info("server shutdown ...") + slog.InfoContext(ctx, "server shutting down...") return s.srv.Shutdown(ctx) } -// RunWithGracefulShutdown はgraceful shutdownを行うサーバーを起動します。 -func (s *Server) RunWithGracefulShutdown(ctx context.Context) { - ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGALRM) +// RunWithGracefulShutdown は graceful shutdown を行うサーバーを起動します。 +func (s *Server) RunWithGracefulShutdown(ctx context.Context) error { + ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) defer stop() - errWg, errCtx := errgroup.WithContext(ctx) - errWg.Go(func() error { - if err := s.Run(ctx); err != nil && err != http.ErrServerClosed { - return fmt.Errorf("listen And Serve error : %+v", err) - } + eg, egCtx := errgroup.WithContext(ctx) - return nil + eg.Go(func() error { + return s.Run(egCtx) }) - errWg.Go(func() error { - <-errCtx.Done() + eg.Go(func() error { + <-egCtx.Done() - ctx, cancel := context.WithTimeout(context.Background(), s.shutdownTimeout) + shutdownCtx, cancel := context.WithTimeout(context.Background(), s.cfg.ShutdownTimeout) defer cancel() - return s.Shutdown(ctx) + return s.Shutdown(shutdownCtx) }) - err := errWg.Wait() - - if err != context.Canceled && - err != nil { - slog.Error("context canceled", "error", err) + if err := eg.Wait(); err != nil && err != context.Canceled { + return err } slog.Info("server shutdown completed") + return nil } diff --git a/pkg/lifecycle/lifecycle.go b/pkg/lifecycle/lifecycle.go new file mode 100644 index 0000000..4c7b2c6 --- /dev/null +++ b/pkg/lifecycle/lifecycle.go @@ -0,0 +1,9 @@ +package lifecycle + +import "context" + +// Application はアプリケーションのライフサイクルを表すインターフェースです。 +type Application interface { + Run(ctx context.Context) error + Shutdown(ctx context.Context) error +}