Create a gist now

Instantly share code, notes, and snippets.

Embed
What would you like to do?
A simple golang web server with basic logging, tracing, health check, graceful shutdown and zero dependencies
package main
import (
"context"
"flag"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"sync/atomic"
"time"
)
type key int
const (
requestIDKey key = 0
)
var (
listenAddr string
healthy int32
)
func main() {
flag.StringVar(&listenAddr, "listen-addr", ":5000", "server listen address")
flag.Parse()
logger := log.New(os.Stdout, "http: ", log.LstdFlags)
logger.Println("Server is starting...")
router := http.NewServeMux()
router.Handle("/", index())
router.Handle("/healthz", healthz())
nextRequestID := func() string {
return fmt.Sprintf("%d", time.Now().UnixNano())
}
server := &http.Server{
Addr: listenAddr,
Handler: tracing(nextRequestID)(logging(logger)(router)),
ErrorLog: logger,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 15 * time.Second,
}
done := make(chan bool)
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
go func() {
<-quit
logger.Println("Server is shutting down...")
atomic.StoreInt32(&healthy, 0)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
server.SetKeepAlivesEnabled(false)
if err := server.Shutdown(ctx); err != nil {
logger.Fatalf("Could not gracefully shutdown the server: %v\n", err)
}
close(done)
}()
logger.Println("Server is ready to handle requests at", listenAddr)
atomic.StoreInt32(&healthy, 1)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Fatalf("Could not listen on %s: %v\n", listenAddr, err)
}
<-done
logger.Println("Server stopped")
}
func index() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "Hello, World!")
})
}
func healthz() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if atomic.LoadInt32(&healthy) == 1 {
w.WriteHeader(http.StatusNoContent)
return
}
w.WriteHeader(http.StatusServiceUnavailable)
})
}
func logging(logger *log.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
requestID, ok := r.Context().Value(requestIDKey).(string)
if !ok {
requestID = "unknown"
}
logger.Println(requestID, r.Method, r.URL.Path, r.RemoteAddr, r.UserAgent())
}()
next.ServeHTTP(w, r)
})
}
}
func tracing(nextRequestID func() string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
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))
})
}
}
@elmiko

This comment has been minimized.

Show comment
Hide comment
@elmiko

elmiko Jan 7, 2018

very cool, thank you for sharing! =)

elmiko commented Jan 7, 2018

very cool, thank you for sharing! =)

@RomanMinkin

This comment has been minimized.

Show comment
Hide comment
@RomanMinkin

RomanMinkin Jan 7, 2018

Nice! Thanks for sharing!

Question: isn’t it safer to use something like ‘uuid’ for ‘nextRequestID’ or add ‘Mutex.Lock’?

Nice! Thanks for sharing!

Question: isn’t it safer to use something like ‘uuid’ for ‘nextRequestID’ or add ‘Mutex.Lock’?

@creack

This comment has been minimized.

Show comment
Hide comment
@creack

creack Jan 7, 2018

Good work! If you are interested, here is a simplified version with the same features (and more): https://gist.github.com/creack/4c00ee404f2d7bd5983382cc93af5147

creack commented Jan 7, 2018

Good work! If you are interested, here is a simplified version with the same features (and more): https://gist.github.com/creack/4c00ee404f2d7bd5983382cc93af5147

@djui

This comment has been minimized.

Show comment
Hide comment
@djui

djui Jan 7, 2018

It’s usually a good idea to log incoming requests separately and before hand outgoing responses, for security audit and debugging reasons.

djui commented Jan 7, 2018

It’s usually a good idea to log incoming requests separately and before hand outgoing responses, for security audit and debugging reasons.

@munirehmad

This comment has been minimized.

Show comment
Hide comment
@munirehmad

munirehmad Jan 7, 2018

If you want to add simple level based logging (also to file) you can plug in
https://github.com/munirehmad/golanglog

If you want to add simple level based logging (also to file) you can plug in
https://github.com/munirehmad/golanglog

@djui

This comment has been minimized.

Show comment
Hide comment
@djui

djui Jan 7, 2018

Small race condition: the server shutdown might not be clean as the program exits as soon shutdown is called:

When Shutdown is called, Serve, ListenAndServe, and ListenAndServeTLS immediately return ErrServerClosed. Make sure the program doesn't exit and waits instead for Shutdown to return.

https://golang.org/pkg/net/http/#Server.Shutdown

djui commented Jan 7, 2018

Small race condition: the server shutdown might not be clean as the program exits as soon shutdown is called:

When Shutdown is called, Serve, ListenAndServe, and ListenAndServeTLS immediately return ErrServerClosed. Make sure the program doesn't exit and waits instead for Shutdown to return.

https://golang.org/pkg/net/http/#Server.Shutdown

@enricofoltran

This comment has been minimized.

Show comment
Hide comment
@enricofoltran

enricofoltran Jan 7, 2018

@elmiko thanks!

Hi @RomanMinkin, thanks! I've built this trying to avoid external dependencies and Go doesn't have a uuid package in the standard lib; that said, for a production grade app I probably implement a more robust nextRequestID function with sometingh like uuid or https://github.com/sony/sonyflake

@creack thanks for sharing your variant, I like the way you used uptime to store server state!

@munirehmad In this small example I'm trying to avoid third party dependencies, but thanks for your suggestion.

@djui I think my program wait for server.Shutdown() to return, correct me if I'm wrong.

Owner

enricofoltran commented Jan 7, 2018

@elmiko thanks!

Hi @RomanMinkin, thanks! I've built this trying to avoid external dependencies and Go doesn't have a uuid package in the standard lib; that said, for a production grade app I probably implement a more robust nextRequestID function with sometingh like uuid or https://github.com/sony/sonyflake

@creack thanks for sharing your variant, I like the way you used uptime to store server state!

@munirehmad In this small example I'm trying to avoid third party dependencies, but thanks for your suggestion.

@djui I think my program wait for server.Shutdown() to return, correct me if I'm wrong.

@vic3lord

This comment has been minimized.

Show comment
Hide comment
@vic3lord

vic3lord Jan 7, 2018

        done := make(chan bool)
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, os.Interrupt)

	go func() {
		<-quit
		logger.Println("Server is shutting down...")
		atomic.StoreInt32(&healthy, 0)

		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()

		server.SetKeepAlivesEnabled(false)
		if err := server.Shutdown(ctx); err != nil {
			logger.Fatalf("Could not gracefully shutdown the server: %v\n", err)
		}
                close(done)
	}()

	logger.Println("Server is ready to handle requests at", listenAddr)
	atomic.StoreInt32(&healthy, 1)
	if err := server.ListenAndServe(); err != http.ErrServerClosed {
		logger.Fatalf("Could not listen on %s: %v\n", listenAddr, err)
	}
        <-done

This will block until shutdown returns

vic3lord commented Jan 7, 2018

        done := make(chan bool)
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, os.Interrupt)

	go func() {
		<-quit
		logger.Println("Server is shutting down...")
		atomic.StoreInt32(&healthy, 0)

		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()

		server.SetKeepAlivesEnabled(false)
		if err := server.Shutdown(ctx); err != nil {
			logger.Fatalf("Could not gracefully shutdown the server: %v\n", err)
		}
                close(done)
	}()

	logger.Println("Server is ready to handle requests at", listenAddr)
	atomic.StoreInt32(&healthy, 1)
	if err := server.ListenAndServe(); err != http.ErrServerClosed {
		logger.Fatalf("Could not listen on %s: %v\n", listenAddr, err)
	}
        <-done

This will block until shutdown returns

@GeorgeMac

This comment has been minimized.

Show comment
Hide comment
@GeorgeMac

GeorgeMac Jan 7, 2018

@enricofoltran What @vic3lord and @djui are pointing to is that the Shutdown call should finished before the program exits.
There is nothing set up to synchronize the goroutine with the singal trap which calls shutdown, with the exit of the program. Using a channel like above would work perfectly. Shutdown will wait for inflight requests to be cleaned up, but will immediately unblock the main goroutine and the program will exit before the cleanup or its timeout happens.

Also https://gist.github.com/enricofoltran/10b4a980cd07cb02836f70a4ab3e72d7#file-main-go-L69

you will want to check for err != nil as well. Otherwise, a nil error here would also result in a non-zero exit code (because of log.Fatalf). Though I don't know under which circumstances ListenAndServe returns a nil error, instead of blocking indefinitely. But to rely on that being the case would be to rely on that behavior never changing.

Great to see how little code it takes to do something as sophisticated as this. Testament to how great Go is 👍

@enricofoltran What @vic3lord and @djui are pointing to is that the Shutdown call should finished before the program exits.
There is nothing set up to synchronize the goroutine with the singal trap which calls shutdown, with the exit of the program. Using a channel like above would work perfectly. Shutdown will wait for inflight requests to be cleaned up, but will immediately unblock the main goroutine and the program will exit before the cleanup or its timeout happens.

Also https://gist.github.com/enricofoltran/10b4a980cd07cb02836f70a4ab3e72d7#file-main-go-L69

you will want to check for err != nil as well. Otherwise, a nil error here would also result in a non-zero exit code (because of log.Fatalf). Though I don't know under which circumstances ListenAndServe returns a nil error, instead of blocking indefinitely. But to rely on that being the case would be to rely on that behavior never changing.

Great to see how little code it takes to do something as sophisticated as this. Testament to how great Go is 👍

@enricofoltran

This comment has been minimized.

Show comment
Hide comment
@enricofoltran

enricofoltran Jan 7, 2018

@djui @vic3lord @GeorgeMac after some testing I proved myself that you guys are right, thank you very much for pointing that out! I've updated the code

Owner

enricofoltran commented Jan 7, 2018

@djui @vic3lord @GeorgeMac after some testing I proved myself that you guys are right, thank you very much for pointing that out! I've updated the code

@dstroot

This comment has been minimized.

Show comment
Hide comment
@sohymg

This comment has been minimized.

Show comment
Hide comment

sohymg commented Jan 8, 2018

Alternate implementation for consideration https://github.com/moexmen/gokit/blob/master/web/server.go

@Xeoncross

This comment has been minimized.

Show comment
Hide comment
@Xeoncross

Xeoncross Jan 8, 2018

I've had a problem with os.Interrupt (syscall.SIGINT) not being enough depending on deploy method (fine for development). syscall.SIGTERM (not on all systems) on linux/bsd seems to solve this when the process is detached from your user.

signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

You can also listen for os.Kill (https://golang.org/pkg/os/#Signal)

I started a repo based on this: https://github.com/Xeoncross/vanilla-go-server

Xeoncross commented Jan 8, 2018

I've had a problem with os.Interrupt (syscall.SIGINT) not being enough depending on deploy method (fine for development). syscall.SIGTERM (not on all systems) on linux/bsd seems to solve this when the process is detached from your user.

signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

You can also listen for os.Kill (https://golang.org/pkg/os/#Signal)

I started a repo based on this: https://github.com/Xeoncross/vanilla-go-server

@drewblas

This comment has been minimized.

Show comment
Hide comment
@drewblas

drewblas Jan 8, 2018

Given the multiple revisions and 78 forks, would you be willing to put this into a full git repo, so that it's clearer/easier to see the latest version and also have separate Issue discussions about potential changes/improvements?

drewblas commented Jan 8, 2018

Given the multiple revisions and 78 forks, would you be willing to put this into a full git repo, so that it's clearer/easier to see the latest version and also have separate Issue discussions about potential changes/improvements?

@enricofoltran

This comment has been minimized.

Show comment
Hide comment
@enricofoltran

enricofoltran Jan 9, 2018

@Xeoncross can you open a new issue in this repository? I'd like to better understand the problem and find a solution, thank you!

Owner

enricofoltran commented Jan 9, 2018

@Xeoncross can you open a new issue in this repository? I'd like to better understand the problem and find a solution, thank you!

@kingeasternsun

This comment has been minimized.

Show comment
Hide comment
@kingeasternsun

kingeasternsun Jan 10, 2018

what if i want to make the server support both http and https?

what if i want to make the server support both http and https?

@shahidhk

This comment has been minimized.

Show comment
Hide comment
@shahidhk

shahidhk Feb 28, 2018

Here's a git-push to deploy version of this with HTTPS domain: https://hasura.io/hub/projects/shahidhk/simple-go-server

Here's a git-push to deploy version of this with HTTPS domain: https://hasura.io/hub/projects/shahidhk/simple-go-server

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