Skip to content

Instantly share code, notes, and snippets.

@creack
Forked from enricofoltran/main.go
Created January 7, 2018 17:30
Show Gist options
  • Save creack/4c00ee404f2d7bd5983382cc93af5147 to your computer and use it in GitHub Desktop.
Save creack/4c00ee404f2d7bd5983382cc93af5147 to your computer and use it in GitHub Desktop.
A simple golang web server with basic logging, tracing, health check, graceful shutdown and zero dependencies
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"strconv"
"sync/atomic"
"syscall"
"time"
)
type middleware func(http.Handler) http.Handler
type middlewares []middleware
func (mws middlewares) apply(hdlr http.Handler) http.Handler {
if len(mws) == 0 {
return hdlr
}
return mws[1:].apply(mws[0](hdlr))
}
func (c *controller) shutdown(ctx context.Context, server *http.Server) context.Context {
ctx, done := context.WithCancel(ctx)
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
go func() {
defer done()
<-quit
signal.Stop(quit)
close(quit)
atomic.StoreInt64(&c.healthy, 0)
server.ErrorLog.Printf("Server is shutting down...\n")
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
server.SetKeepAlivesEnabled(false)
if err := server.Shutdown(ctx); err != nil {
server.ErrorLog.Fatalf("Could not gracefully shutdown the server: %s\n", err)
}
}()
return ctx
}
type controller struct {
logger *log.Logger
nextRequestID func() string
healthy int64
}
func main() {
listenAddr := ":5000"
if len(os.Args) == 2 {
listenAddr = os.Args[1]
}
logger := log.New(os.Stdout, "http: ", log.LstdFlags)
logger.Printf("Server is starting...")
c := &controller{logger: logger, nextRequestID: func() string { return strconv.FormatInt(time.Now().UnixNano(), 36) }}
router := http.NewServeMux()
router.HandleFunc("/", c.index)
router.HandleFunc("/healthz", c.healthz)
server := &http.Server{
Addr: listenAddr,
Handler: (middlewares{c.tracing, c.logging}).apply(router),
ErrorLog: logger,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 15 * time.Second,
}
ctx := c.shutdown(context.Background(), server)
logger.Printf("Server is ready to handle requests at %q\n", listenAddr)
atomic.StoreInt64(&c.healthy, time.Now().UnixNano())
if err := server.ListenAndServe(); err != http.ErrServerClosed {
logger.Fatalf("Could not listen on %q: %s\n", listenAddr, err)
}
<-ctx.Done()
logger.Printf("Server stopped\n")
}
func (c *controller) index(w http.ResponseWriter, req *http.Request) {
if req.URL.Path != "/" {
http.NotFound(w, req)
return
}
fmt.Fprintf(w, "Hello, World!\n")
}
func (c *controller) healthz(w http.ResponseWriter, req *http.Request) {
if h := atomic.LoadInt64(&c.healthy); h == 0 {
w.WriteHeader(http.StatusServiceUnavailable)
} else {
fmt.Fprintf(w, "uptime: %s\n", time.Since(time.Unix(0, h)))
}
}
func (c *controller) logging(hdlr http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
defer func(start time.Time) {
requestID := w.Header().Get("X-Request-Id")
if requestID == "" {
requestID = "unknown"
}
c.logger.Println(requestID, req.Method, req.URL.Path, req.RemoteAddr, req.UserAgent(), time.Since(start))
}(time.Now())
hdlr.ServeHTTP(w, req)
})
}
func (c *controller) tracing(hdlr http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
requestID := req.Header.Get("X-Request-Id")
if requestID == "" {
requestID = c.nextRequestID()
}
w.Header().Set("X-Request-Id", requestID)
hdlr.ServeHTTP(w, req)
})
}
// main_test.go
var (
_ http.Handler = http.HandlerFunc((&controller{}).index)
_ http.Handler = http.HandlerFunc((&controller{}).healthz)
_ middleware = (&controller{}).logging
_ middleware = (&controller{}).tracing
)
@dstroot
Copy link

dstroot commented Jan 8, 2018

@creack - why did you remove ctx from the tracing controller? The original code had:

return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    requestID := r.Header.Get("X-Request-Id")
    if requestID == "" {
         requestID = nextRequestID()
    }
    ctx := context.WithValue(r.Context(), requestIDKey, requestID)
    w.Header().Set("X-Request-Id", requestID)
    next.ServeHTTP(w, r.WithContext(ctx))
})

@dstroot
Copy link

dstroot commented Jan 8, 2018

Also could you explain this?

// main_test.go
var (
	_ http.Handler = http.HandlerFunc((&controller{}).index)
	_ http.Handler = http.HandlerFunc((&controller{}).healthz)
	_ middleware   = (&controller{}).logging
	_ middleware   = (&controller{}).tracing
)

@wvh
Copy link

wvh commented Jun 2, 2019

Those variable assignments ensure the handlers implement the required interfaces/types. If someone were to write their own handlers, Go would complain at compile time if those handlers' signatures wouldn't be correctly typed.

@creack
Copy link
Author

creack commented Jun 2, 2019

@dstroot Sorry I missed your comments. I removed the context from the tracing because requestID is already in the req, and @wvh is correct, those _ T = xx are there to enforce the interfaces at compile time.

@bluebrown
Copy link

Thanks for sharing.

@pococms
Copy link

pococms commented Aug 25, 2022

Can I use this in an open source static site generator? If so would you mind telling me what license you'd prefer? Thanks!

@creack
Copy link
Author

creack commented Aug 25, 2022

No problem. MIT license

@pococms
Copy link

pococms commented Jan 3, 2023 via email

@xinzhanguo
Copy link

golang-ci lint
41:3: shadow: declaration of "ctx" shadows declaration at line 27 (govet)
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)

@creack
Copy link
Author

creack commented Jul 12, 2024

That is fine and expected

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment