Skip to content

Instantly share code, notes, and snippets.

@hugoluchessi
Last active June 12, 2018 02:04
Show Gist options
  • Save hugoluchessi/b4bc481620e28713291d03c74226fcd8 to your computer and use it in GitHub Desktop.
Save hugoluchessi/b4bc481620e28713291d03c74226fcd8 to your computer and use it in GitHub Desktop.

Golang net/http scales well! But does it grow well?

Creating an Api in Go is very simple, the Standard library gives you all tools needed. For instance, imagine you have a movies microservice which is responsible for all CRUD operations for a vintage VHS movie store.

Suppose you have a Movie structure as described below:

package models

import "time"

type Movie struct {
	ID       int
	Name     string
	Release  time.Time
	Synopsis string
	Rating   int
}

And a repository interface which gives you the following functions (parameter names were given only for readability purposes):

package repositories

import "github.com/hugoluchessi/api_std_lib_test/models"

type MovieRepository interface {
	Get(int) (models.Movie, error)
	Save(models.Movie) error
	Delete(int) error
	List(string) ([]models.Movie, error)
}

Note: The concrete implementation of this interface, the database used and any other persistence problems will not be addressed here because it is not the focus.

Nice... assuming that we have a controller layer that will handle parameters and bussiness logic, now we have to create an Web interface to receive all the given parameters from the front end! The interface defined should be:

GET     /movie/{id}
PUT     /movie
POST    /movie
DELETE  /movie/{id}
GET     /movies?term={movie name part}

Wait! Named parameters are not natively suported by the stdlib.

Ok, that is fine.. I guess. Let's assume that those parameters are being send via query string. So the real interface would be:

GET     /movie?id={movie id}
PUT     /movie
POST    /movie
DELETE  /movie?id={movie id}
GET     /movies?term={movie name part}

Possible interface: Check! Now let's begin the main function code:

package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {
    mux := http.NewServeMux()

	mux.HandleFunc("/v1/movie", func(res http.ResponseWriter, req *http.Request) {
		switch req.Method {
		case "GET":
			controllers.GetMovie(res, req)
		case "POST":
			controllers.SaveMovie(res, req)
		case "PUT":
			controllers.SaveMovie(res, req)
		case "DELETE":
			controllers.DeleteMovie(res, req)
		default:
			io.WriteString(res, `Wrong method`)
		}
	})

	mux.HandleFunc("/v1/movies", func(res http.ResponseWriter, req *http.Request) {
		switch req.Method {
		case "GET":
			controllers.GetMovies(res, req)
		default:
			io.WriteString(res, `Wrong method`)
		}
	})

    http.ListenAndServe(":8080", mux)
}

Ok, now our api is ready to go, but wait. Let's put some Logging and Authentication. To keep code organized, let's create a package called middlewares and put the handlers there:

    package middlewares

    import (
        "log"
        "net/http"
    )

    func LoggingHandler(h http.Handler) http.Handler {
        return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
            log.Printf("started request")
            h.ServeHTTP(res, req)
            log.Printf("finished request")
        })
    }

    func AuthHandler(h http.Handler) http.Handler {
        return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
            log.Printf("You are logged!")
            h.ServeHTTP(res, req)
        })
    }

Now we have both handlers let's add it to the main function:

func main() {
    //[code ommited]

    http.ListenAndServe(
        ":8080", 
        middlewares.LoggingHandler(
            middlewares.AuthHandler(
                mux,
            ),
        ),
    )
}

Six months after the fisrt release, we are developing the seconds version of the api. The following requirements were put:

  • A new authentication must be set on the api
  • A new log formatting will be generated for the new routes
  • We need to keep the old log being generated for v1 until all monitoring scripts are updated for the new format
  • We need to keep the old authentication running on the v1 api
  • We do not want another Api, it would be a great devops issue

First let's create our new middlewares on our package:

func Logging2Handler(h http.Handler) http.Handler {
	return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
		log.Printf("started request on mw 2")
		h.ServeHTTP(res, req)
		log.Printf("finished request on mw 2")
	})
}

func Auth2Handler(h http.Handler) http.Handler {
	return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
		log.Printf("You are logged on the new handler!")
		h.ServeHTTP(res, req)
	})
}

Now we have to add the "v2" routes for our api, again, assume controller have already implemented the seconds version of the business logic in a new set of methods:

func main() {
	mux := http.NewServeMux()

	mux.HandleFunc("/v1/movie", func(res http.ResponseWriter, req *http.Request) {
		switch req.Method {
		case "GET":
			controllers.GetMovie(res, req)
		case "POST":
			controllers.SaveMovie(res, req)
		case "PUT":
			controllers.SaveMovie(res, req)
		case "DELETE":
			controllers.DeleteMovie(res, req)
		default:
			io.WriteString(res, `Wrong method`)
		}
	})

	mux.HandleFunc("/v1/movies", func(res http.ResponseWriter, req *http.Request) {
		switch req.Method {
		case "GET":
			controllers.GetMovies(res, req)
		default:
			io.WriteString(res, `Wrong method`)
		}
	})

	mux.HandleFunc("/v2/movie", func(res http.ResponseWriter, req *http.Request) {
		switch req.Method {
		case "GET":
			controllers.GetMovieV2(res, req)
		case "POST":
			controllers.SaveMovieV2(res, req)
		case "PUT":
			controllers.SaveMovieV2(res, req)
		case "DELETE":
			controllers.DeleteMovieV2(res, req)
		default:
			io.WriteString(res, `Wrong method`)
		}
	})

	mux.HandleFunc("/v2/movies", func(res http.ResponseWriter, req *http.Request) {
		switch req.Method {
		case "GET":
			controllers.GetMoviesV2(res, req)
		default:
			io.WriteString(res, `Wrong method`)
		}
	})

	http.ListenAndServe(
		":8080",
		middlewares.LoggingHandler(
			middlewares.AuthHandler(
				mux,
			),
		),
	)
}

Now let's add the new middlewares:

func main() {
	//[code ommited]

	http.ListenAndServe(
		":8080",
		middlewares.Logging2Handler(
			middlewares.Auth2Handler(
				middlewares.LoggingHandler(
					middlewares.AuthHandler(
						mux,
					),
				),
			),
		),
	)
}

This will work, but the v2 will use the same middlewares as v1. That's not possible, because we cannot allow old auth users to be able to use the api v2. We now have to send middlewares on each handle func specifically:

func main() {
	mux := http.NewServeMux()

	mux.Handle(
		"/v1/movie",
		middlewares.Logging2Handler(
			middlewares.Auth2Handler(
				middlewares.LoggingHandler(
					middlewares.AuthHandler(
						http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
							switch req.Method {
							case "GET":
								controllers.GetMovie(res, req)
							case "POST":
								controllers.SaveMovie(res, req)
							case "PUT":
								controllers.SaveMovie(res, req)
							case "DELETE":
								controllers.DeleteMovie(res, req)
							default:
								io.WriteString(res, `Wrong method`)
							}
						}),
					),
				),
			),
		),
	)

	mux.Handle(
		"/v1/movies",
		middlewares.Logging2Handler(
			middlewares.Auth2Handler(
				middlewares.LoggingHandler(
					middlewares.AuthHandler(
						http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
							switch req.Method {
							case "GET":
								controllers.GetMovies(res, req)
							default:
								io.WriteString(res, `Wrong method`)
							}
						}),
					),
				),
			),
		),
	)

	mux.Handle(
		"/v2/movie",
		middlewares.Logging2Handler(
			middlewares.Auth2Handler(
				http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
					switch req.Method {
					case "GET":
						controllers.GetMovieV2(res, req)
					case "POST":
						controllers.SaveMovieV2(res, req)
					case "PUT":
						controllers.SaveMovieV2(res, req)
					case "DELETE":
						controllers.DeleteMovieV2(res, req)
					default:
						io.WriteString(res, `Wrong method`)
					}
				}),
			),
		),
	)

	mux.Handle(
		"/v2/movies",
		middlewares.Logging2Handler(
			middlewares.Auth2Handler(
				http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
					switch req.Method {
					case "GET":
						controllers.GetMoviesV2(res, req)
					default:
						io.WriteString(res, `Wrong method`)
					}
				}),
			),
		),
	)

	http.ListenAndServe(":8080", mux)
}

Awesome! Now it is working, but wait...

  • What happens when I need to have more complex middlewares combinations between versions?
  • When v3 lauches I will have to copy and paste all these switches?
  • The code looks like a christmas tree

Yes your code just rotted fortunatelly there are plenty of tools to work this around. Some of those available tools are GorillaMux, Gin, my just created Badger and many others.

I developed a study on why I chose to develop my own route Multiplexer later it will become a post as well and the details of the study will be there, but for now let's focus on the problem of growing the Api.

The main features we will use from this mux will be:

  • Group routing
  • Handler methods for specific http methods
  • Native Middlewares
  • Named parameters (changing query string to named parameters would break the interface, but let's ignore that fact for now)

And another good thing of this mux is that it is 100% compatible with stdlib interface, so our middlewares and handlers will be reused, we will focus on dealing ONLY with the routing problem.

I'll rewrite the entire main function and explaing afterwards:

func main() {
	// Creating a new Mux
	mux := badger.NewMux()

	// Creating 2 routing groups (Routers)
	v1Router := mux.AddRouter("v1")
	v2Router := mux.AddRouter("v2")

	// Defining routes for the first version router
	v1Router.Get("movie", http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
		controllers.GetMovie(res, req)
	}))

	v1Router.Post("movie", http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
		controllers.SaveMovie(res, req)
	}))

	v1Router.Put("movie", http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
		controllers.SaveMovie(res, req)
	}))

	v1Router.Delete("movie", http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
		controllers.DeleteMovie(res, req)
	}))

	v1Router.Get("movies", http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
		controllers.GetMovies(res, req)
	}))

	// Defining routes for the second version router
	v2Router.Get("movie", http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
		controllers.GetMovieV2(res, req)
	}))

	v2Router.Post("movie", http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
		controllers.SaveMovieV2(res, req)
	}))

	v2Router.Put("movie", http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
		controllers.SaveMovieV2(res, req)
	}))

	v2Router.Delete("movie", http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
		controllers.DeleteMovieV2(res, req)
	}))

	v2Router.Get("movies", http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
		controllers.GetMoviesV2(res, req)
	}))

	// Adding middlewares for the first version router
	v1Router.Use(middlewares.AuthHandler)
	v1Router.Use(middlewares.Auth2Handler)
	v1Router.Use(middlewares.LoggingHandler)
	v1Router.Use(middlewares.Logging2Handler)

	// Adding middlewares for the second version router
	v2Router.Use(middlewares.Auth2Handler)
	v2Router.Use(middlewares.Logging2Handler)

	// Serve api
	http.ListenAndServe(":8080", mux)
}

The code:

mux := badger.NewMux()
v1Router := mux.AddRouter("v1")
v2Router := mux.AddRouter("v2")

Is just creating a new mux and defining the major routing groups, after that, we define the routes for each router, for instance:

v2Router.Post("movie", http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
	controllers.SaveMovieV2(res, req)
}))

After that we define middlewares for each router, always with stdlib interface:

v1Router.Use(middlewares.Logging2Handler)
v2Router.Use(middlewares.Auth2Handler)

And, finally, we serve application, again, stdlib interfaces:

http.ListenAndServe(":8080", mux)

The code is smaller, we can work it to be more modular, but still is pretty smaller than the other. But small is not always better the fact that the code is much more readable and provides the tools to ensure the api will grow on functionality not complexity.

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