Skip to content

Instantly share code, notes, and snippets.

@pcasaretto
Forked from hugoluchessi/CodeRotting.md
Last active June 7, 2018 12:36
Show Gist options
  • Save pcasaretto/20b9779f80889c833ba9520c8ca0765c to your computer and use it in GitHub Desktop.
Save pcasaretto/20b9779f80889c833ba9520c8ca0765c to your computer and use it in GitHub Desktop.

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

Creating an Api in Go is very simple, the Standard library gives you all the 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:

type Movie struct {
    id          int
    name        string
    release     time.Time
    sinopsis    string
    rating      int
}

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

type MovieRepository interface {
    func Get(id int) (Movie, error)
    func Save(m Movie) error
    func Delete(id int) error
    func List(term string) ([]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 business logic, now we have to create a Web interface to receive all the given parameters from the front end! The interface defined should be:

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

package main

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

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

	mux.HandleFunc("/v1/product", 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:
			res.WriteHeader(http.StatusMethodNotAllowed)
			io.WriteString(res, `Wrong method`)
		}
	})

	mux.HandleFunc("/v1/products", func(res http.ResponseWriter, req *http.Request) {
		switch req.Method {
		case "GET":
			controllers.GetMovies(res, req)
		default:
			res.WriteHeader(http.StatusMethodNotAllowed)
			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 added:

  • A new method of authentication must be set on the api
  • A new log format 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 controllers have already implemented the second version of the business logic in a new set of methods:

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

	mux.HandleFunc("/v1/product", 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/products", 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/product", 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/products", 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/product",
		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/products",
		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/product",
		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/products",
		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...

  • Almost a hundred lines of code to make 10 routes to work?
  • 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?

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

I wrote 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:

  • Route grouping
  • Handler methods for specific http methods
  • Native Middlewares

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 explain it afterwards:

func main() {
	mux := badger.NewMux()

	v1Router := mux.AddRouter("v1")
	v1Router.Get("product", http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
		controllers.GetMovie(res, req)
	}))

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

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

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

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

	v1Router.Use(middlewares.AuthHandler)
	v1Router.Use(middlewares.Auth2Handler)
	v1Router.Use(middlewares.LoggingHandler)
	v1Router.Use(middlewares.Logging2Handler)

	v2Router := mux.AddRouter("v2")
	v2Router.Get("product", http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
		controllers.GetMovieV2(res, req)
	}))

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

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

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

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

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

	http.ListenAndServe(":8080", mux)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment