04/04/2024 - GO
Go 1.22 introduced an enhanced version of the net/http package's router. This includes method matching and wildcards. In this example we are going to create a custom HTTP package for it. The package will also allow us to use global and route specific middlewares as bonus point.
package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
"time"
"rest/pkg/http"
"rest/pkg/middleware"
"rest/src/api"
)
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
// HTTP api
user := api.User{}
// HTTP router
rtr := http.NewRouter()
rtr.Use(middleware.AccessLogger)
rtr.Add("POST /api/v1/users", user.Create, middleware.DeprecatedRoute)
rtr.Add("DELETE /api/v1/users/{id}", user.Delete)
// HTTP server
srv := http.NewServer(rtr, ":1234")
go func() {
log.Println("api running ...")
if err := srv.Start(); err != nil {
log.Fatalln(err)
}
}()
if err := srv.Shutdown(ctx, time.Second*5); err != nil {
log.Println(err)
}
log.Println("api shutdown ...")
}
package http
import (
"net/http"
"slices"
)
type Router struct {
handler *http.ServeMux
middlewares []func(http.Handler) http.Handler
}
func NewRouter() *Router {
return &Router{
handler: http.NewServeMux(),
}
}
func (r *Router) Use(middlewares ...func(http.Handler) http.Handler) {
r.middlewares = middlewares
}
func (r *Router) Add(path string, handler http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) {
mids := slices.Concat(r.middlewares, middlewares)
for i := len(mids) - 1; i >= 0; i-- {
handler = mids[i](handler).(http.HandlerFunc)
}
r.handler.HandleFunc(path, handler)
}
package http
import (
"context"
"net/http"
"time"
)
type Server struct {
server *http.Server
}
func NewServer(router *Router, address string) Server {
return Server{
server: &http.Server{
Handler: router.handler,
Addr: address,
},
}
}
func (s Server) Start() error {
if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
return err
}
return nil
}
func (s Server) Shutdown(ctx context.Context, timeout time.Duration) error {
<-ctx.Done()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
if err := s.server.Shutdown(ctx); err != nil {
return err
}
return nil
}
package middleware
import (
"log"
"net/http"
)
func AccessLogger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println(r.Method + " " + r.RequestURI)
next.ServeHTTP(w, r)
})
}
package middleware
import (
"log"
"net/http"
)
func DeprecatedRoute(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println("this route has been deprecated")
w.WriteHeader(http.StatusGone)
})
}
package api
import (
"log"
"net/http"
)
type User struct{}
func (u User) Create(w http.ResponseWriter, r *http.Request) {
log.Println("user create")
}
func (u User) Delete(w http.ResponseWriter, r *http.Request) {
log.Println("user delete", r.PathValue("id"))
}
$ curl -iX DELETE "http://localhost:1234/api/v1/users/1"
2024/06/04 19:56:52 DELETE /api/v1/users/1
2024/06/04 19:56:52 user delete 1
$ curl -iX POST "http://localhost:1234/api/v1/users" -d '{"key":"val"}'
2024/06/04 19:56:55 POST /api/v1/users
2024/06/04 19:56:55 this route has been deprecated