Minimal Go HTTP Servers

Hi there. In this post, I’m going to go over a couple snippets of minimal HTTP servers that I use from time to time when I’m tinkering with Go.
Using net/http
⌗
For starters, here’s the boilerplate code I start with when I want to make a quick server with the net/http
package:
package main
import (
"encoding/json"
"fmt"
"net/http"
)
func ping() http.Handler {
type response struct {
Message string `json:"message"`
}
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/json")
resp := response{Message: "pong"}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(resp)
})
}
func main() {
http.Handle("/ping", hello())
http.ListenAndServe(":8080", nil)
}
This is great if you just need something with a pulse. But what about one that does a little bit more, like routing, middleware, and static files? This includes some of the patterns from my posts on the adapter pattern Part 1, Part 2, and Part 3. Here’s what that looks like flattened into one file:
package main
import (
"embed"
"encoding/json"
"fmt"
"net/http"
"github.com/gorilla/mux"
)
//go:embed static
var static embed.FS
type service struct {
server *http.Server
router *mux.Router
}
type serviceAdapter func(*service) *service
func adaptService(s *service, opts ...serviceAdapter) *service {
for i := range opts {
opt := opts[len(opts)-1-i]
s = opt(s)
}
return s
}
func withServer(baseServer *http.Server) serviceAdapter {
return func(s *service) *service {
s.server = baseServer
return s
}
}
func withRouter(r *mux.Router) serviceAdapter {
return func(s *service) *service {
s.router = r
return s
}
}
func newService(opts ...serviceAdapter) *service {
s := &service{}
s = adaptService(s, opts...)
return s
}
type handlerAdapter func(http.HandlerFunc) http.HandlerFunc
func adaptHandler(h http.HandlerFunc, opts ...handlerAdapter) http.HandlerFunc {
for i := range opts {
opt := opts[len(opts)-1-i]
h = opt(h)
}
return h
}
func setContentType(content string) handlerAdapter {
return func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", content)
next(w, r)
}
}
}
func (s *service) setupHTTPRoutes() {
// http basic routes
s.router.Handle("/ping", adaptHandler(
ping(),
setContentType("application/json"),
)).Methods("GET")
// static files
staticFS, err := fs.Sub(static, "static")
if err != nil {
log.Fatal(err)
}
fs := http.FileServer(http.FS(staticFS)))
s.router.PathPrefix("/docs").Handler(http.StripPrefix("/static", fs))
}
func ping() http.Handler {
type response struct {
Message string `json:"message,omitempty"`
}
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
resp := response{Message: "pong"}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(resp)
})
}
func (s *service) ListenAndServe() error {
return s.server.ListenAndServe()
}
func (s *service) Shutdown(ctx context.Context) error {
return s.server.Shutdown(ctx)
}
func main() {
srv := &http.Server{
Handler: mux.NewRouter(),
Addr: ":8080",
}
service = NewService(
withServer(srv),
withRouter(router),
)
service.setupHTTPRoutes()
// listen forever
go func() {
log.Printf("Starting the server on %s", service.server.Addr)
if err := service.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Error shutting down the server: %s", err)
}
}()
// block for interrupt
shutdown := make(chan os.Signal, 1)
signal.Notify(shutdown, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
<-shutdown
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := service.server.Shutdown(ctx); err != nil {
log.Fatalf("Error shutting down the server: %s", err)
}
}
This is a pretty extensible approach and I’ve been more or less happy building projects using this sort of scaffolding. The upside is that it’s all quite straightforward and you can add custom functionality/handlers very easily. This is one of the things I love about Go; you can write and extend applications like this really easily. However, reimplementing basic HTTP functionality (e.g., security middleware) for each project is error prone, and I have no motivation to maintain my own library of HTTP utilities when there’s already perfectly fine solutions out there.
Using echo
⌗
One of the nicer HTTP “frameworks” I’ve seen is Echo. I saw it “in the wild” when I was looking at the Temporal UI backend. The main thing I like about it is that you can quickly browse through the docs (and code!) to get a decent census of what you can do out of the box and get rolling with minimal friction. They’ve also got some great “cookbook” examples that show marginally more advanced patterns. This is in contrast to something like go-kit
that requires a bit more cognitive load than is necessary for small hobby projects. Additionally, while the Gorilla toolkit will probably remain a mainstay in the ecosystem for the foreseeable future, it is in “archive” mode. If you’re on the fence about which toolkit to “git gud” with, Echo is worth consideration (note that even Echo relies on Gorilla under the hood in some places). Here’s a dead simple HTTP server example:
package main
import (
"net/http"
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New()
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
})
e.Logger.Fatal(e.Start(":8080"))
}
Here’s a slightly more involved example that shows conventional uses of some middleware and is still remarkably straightforward:
package server
import (
"fmt"
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
type Server struct {
httpServer *echo.Echo
}
func NewServer() *Server {
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"http://localhost:3000"},
AllowHeaders: []string{
echo.HeaderOrigin,
echo.HeaderContentType,
echo.HeaderAccept,
echo.HeaderXCSRFToken,
echo.HeaderAuthorization,
},
AllowCredentials: true,
}))
e.Use(middleware.Secure())
e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
CookiePath: "/",
CookieHTTPOnly: false,
CookieSameSite: http.SameSiteStrictMode,
}))
s := &Server{
httpServer: e,
}
return s
}
func (s *Server) Start() error {
fmt.Println("Starting server on :8080...")
s.httpServer.Logger.Fatal(s.httpServer.Start(":8080"))
return nil
}
func (s *Server) Stop() {
fmt.Println("Stopping server...")
if err := s.httpServer.Close(); err != nil {
s.httpServer.Logger.Warn(err)
}
}
Rather than repeating examples from the documentation here, I’m just going to suggest you go check out the Echo docs. They’re quick to navigate and read, and they include a bunch of recipes and examples that show how easy it is to develop with Echo.
Bonus: Using Python + FastAPI
⌗
While I’m at it, I might as well show one last spiritually related example: my FastAPI starter snippet. This is mostly taken from the FastAPI docs.
First, set up your virtual env and dependencies:
pyenv virtualenv 3.9.1 python-fastapi-server
pyenv activate python-fastapi-server
pip install fastapi
pip install "uvicorn[standard]"
Then in main.py
:
"""main.py"""
from typing import Union
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def index():
"""Root path."""
return {"status": "ok"}
@app.get("/ping")
def ping():
"""Say hello."""
return "pong"
@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
"""Read item with path variable and query param."""
return {"item_id": item_id, "q": q}
Then you can run the app:
uvicorn main:app --reload
Check out the FastAPI docs for examples of how to leverage Pydantic
models to build these APIs. That’s all from me, until next time!