medium.com/@gauravsing… Translator: Oopsguy.com

I started being a Gopher 10 months ago and haven’t looked back. Like many other Gopher, I quickly discovered that simple language features were very useful for quickly building fast, extensible software. When I first started learning Go, I was playing with Different Multiplexer, which could be used as an API server. If you have a Rails background like I do, you might also have trouble building all the functionality that Web frameworks offer. Going back to multiplexers, I found three good things to be very useful, namely Gorilla Mux, Httprouter, and Bone (in descending order of performance). Even with bone’s best performance and simpler handler signatures, it’s still not mature enough for me to use in a production environment. Therefore, I ended up using Httprouter. In this tutorial, I’ll build a simple REST API server using HttprOuter.

If you want to be lazy and just want to get the source code, you can check out my Github repository directly here [4].

Let’s get started. Start by creating a basic endpoint:

package main import ( "fmt" "log" "net/http" "github.com/julienschmidt/httprouter" ) func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { fmt.Fprint(w, "Welcome! \n") } func main() { router := httprouter.New() router.GET("/", Index) log.Fatal(http.ListenAndServe(":8080", router)) }Copy the code

In the code snippet above, Index is a handler function that takes three arguments. After that, the handler will be registered with the GET/path in the main function. Now compile and run your program and go to http:// localhost:8080 to see your API server. Click here [1] to get the current code.

Now we can make the API a little more complicated. We now have an entity named Book that can use the ISDN field as a unique identifier. Let’s create more actions, namely GET /books and GET /books/: ISDN for Index and Show actions. Our main.go file now looks like this:

package main import ( "encoding/json" "fmt" "log" "net/http" "github.com/julienschmidt/httprouter" ) func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { fmt.Fprint(w, "Welcome! \n") } type Book struct { // The main identifier for the Book. This will be unique. ISDN string `json:"isdn"` Title string `json:"title"` Author string `json:"author"` Pages int `json:"pages"` } type JsonResponse struct { // Reserved field to add some meta information to the API response Meta interface{} `json:"meta"` Data interface{} `json:"data"` } type JsonErrorResponse struct { Error *ApiError `json:"error"` } type ApiError struct { Status int16 `json:"status"` Title string `json:"title"` } // A map to store the books with the ISDN as the key // This acts as the storage in lieu of an actual database var bookstore = make(map[string]*Book) // Handler for the books index action // GET /books func BookIndex(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { books := []*Book{} for _, book := range bookstore { books = append(books, book) } response := &JsonResponse{Data: &books} w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(response); err ! = nil { panic(err) } } // Handler for the books Show action // GET /books/:isdn func BookShow(w http.ResponseWriter, r *http.Request, params httprouter.Params) { isdn := params.ByName("isdn") book, ok := bookstore[isdn] w.Header().Set("Content-Type", "application/json; charset=UTF-8") if ! ok { // No book with the isdn in the url has been found w.WriteHeader(http.StatusNotFound) response := JsonErrorResponse{Error: &ApiError{Status: 404, Title: "Record Not Found"}} if err := json.NewEncoder(w).Encode(response); err ! = nil { panic(err) } } response := JsonResponse{Data: book} if err := json.NewEncoder(w).Encode(response); err ! = nil { panic(err) } } func main() { router := httprouter.New() router.GET("/", Index) router.GET("/books", BookIndex) router.GET("/books/:isdn", BookShow) // Create a couple of sample Book entries bookstore["123"] = &Book{ ISDN: "123", Title: "Silence of the Lambs", Author: "Thomas Harris", Pages: 367, } bookstore["124"] = &Book{ ISDN: "124", Title: "To Kill a Mocking Bird", Author: "Harper Lee", Pages: 320, } log.Fatal(http.ListenAndServe(":8080", router)) }Copy the code

If you now try to request GET https:// localhost:8080/books, you will GET the following response:

{
    "meta": null,
    "data": [
        {
            "isdn": "123",
            "title": "Silence of the Lambs",
            "author": "Thomas Harris",
            "pages": 367
        },
        {
            "isdn": "124",
            "title": "To Kill a Mocking Bird",
            "author": "Harper Lee",
            "pages": 320
        }
    ]
}Copy the code

We hard-coded the two Book entities in the main function. Click here [2] to get the code for the current stage.

Let’s refactor the code. So far, all of our code has been placed in the same file: main.go. We can move them into separate files. At this point we have a directory:

│ ├── hand and hand │ ├── modelsCopy the code

We moved all JSON response-related constructs to lires.go, the handler function to handlers.go, and the Book structure to models.go. Click here [3] to see the code for the current stage. Now, let’s jump in and write some tests. In Go, the *_test.go file is used for testing. So let’s create handlers_test.go.

package main import ( "net/http" "net/http/httptest" "testing" "github.com/julienschmidt/httprouter" ) func TestBookIndex(t *testing.T) { // Create an entry of the book to the bookstore map testBook := &Book{ ISDN: "111", Title: "test title", Author: "test author", Pages: 42, } bookstore["111"] = testBook // A request with an existing isdn req1, err := http.NewRequest("GET", "/books", nil) if err ! = nil { t.Fatal(err) } rr1 := newRequestRecorder(req1, "GET", "/books", BookIndex) if rr1.Code ! = 200 { t.Error("Expected response code to be 200") } // expected response er1 := "{\"meta\":null,\"data\":[{\"isdn\":\"111\",\"title\":\"test title\",\"author\":\"test author\",\"pages\":42}]}\n" if rr1.Body.String() ! = er1 { t.Error("Response body does not match") } } // Mocks a handler and returns a httptest.ResponseRecorder func newRequestRecorder(req *http.Request, method string, strPath string, fnHandler func(w http.ResponseWriter, r *http.Request, param httprouter.Params)) *httptest.ResponseRecorder { router := httprouter.New() router.Handle(method, strPath, fnHandler) // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. rr := httptest.NewRecorder() // Our handlers satisfy http.Handler, so we can call their ServeHTTP method // directly and pass in our Request and ResponseRecorder. router.ServeHTTP(rr, req) return rr }Copy the code

We mock the Handler using the Recorder of the HttpTest package. Similarly, you can write tests for the Handler BookShow. Let’s do a little refactoring. We still define all the routes in the main function, the handler looks a little bloated, we can DRY it out, we can still print some log messages in the terminal, and we can add a BookCreate handler to create a new Book. First, let’s deal with handlers. Go.

package main import ( "encoding/json" "fmt" "io" "io/ioutil" "net/http" "github.com/julienschmidt/httprouter" ) func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { fmt.Fprint(w, "Welcome! \n") } // Handler for the books Create action // POST /books func BookCreate(w http.ResponseWriter, r *http.Request, params httprouter.Params) { book := &Book{} if err := populateModelFromHandler(w, r, params, book); err ! = nil { writeErrorResponse(w, http.StatusUnprocessableEntity, "Unprocessible Entity") return } bookstore[book.ISDN] = book writeOKResponse(w, book) } // Handler for the books index action // GET /books func BookIndex(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { books := []*Book{} for _, book := range bookstore { books = append(books, book) } writeOKResponse(w, books) } // Handler for the books Show action // GET /books/:isdn func BookShow(w http.ResponseWriter, r *http.Request, params httprouter.Params) { isdn := params.ByName("isdn") book, ok := bookstore[isdn] if ! ok { // No book with the isdn in the url has been found writeErrorResponse(w, http.StatusNotFound, "Record Not Found") return } writeOKResponse(w, book) } // Writes the response as a standard JSON response with StatusOK func writeOKResponse(w http.ResponseWriter, m interface{}) { w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(&JsonResponse{Data: m}); err ! = nil { writeErrorResponse(w, http.StatusInternalServerError, "Internal Server Error") } } // Writes the error response as a Standard API JSON response with a response code func writeErrorResponse(w http.ResponseWriter, errorCode int, errorMsg string) { w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.WriteHeader(errorCode) json. NewEncoder(w). Encode(&JsonErrorResponse{Error: &ApiError{Status: errorCode, Title: errorMsg}}) } //Populates a model from the params in the Handler func populateModelFromHandler(w http.ResponseWriter, r *http.Request, params httprouter.Params, model interface{}) error { body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576)) if err ! = nil { return err } if err := r.Body.Close(); err ! = nil { return err } if err := json.Unmarshal(body, model); err ! = nil { return err } return nil }Copy the code

I created two functions, writeOKResponse to write StatusOK to the response, which returns either a Model or a Model slice, and writeErrorResponse will respond with a JSON error in the event of an expected or unexpected error. Like any good Gopher, we shouldn’t panic. I also added a function called populateModelFromHandler, which parses the content from the body into whatever model (struct) it needs. In this case, we use it in the BookCreate handler to populate a Book. Now, let’s look at the log. We simply create a Logger function that wraps the handler function and prints log messages before and after executing the handler function.

package main
import (
	"log"
	"net/http"
	"time"
	"github.com/julienschmidt/httprouter"
)
// A Logger function which simply wraps the handler function around some log messages
func Logger(fn func(w http.ResponseWriter, r *http.Request, param httprouter.Params)) func(w http.ResponseWriter, r *http.Request, param httprouter.Params) {
	return func(w http.ResponseWriter, r *http.Request, param httprouter.Params) {
		start := time.Now()
		log.Printf("%s %s", r.Method, r.URL.Path)
		fn(w, r, param)
		log.Printf("Done in %v (%s %s)", time.Since(start), r.Method, r.URL.Path)
	}
}Copy the code

Let’s look at the routing. First, define all the routes in one place, such as routes.go.

package main import "github.com/julienschmidt/httprouter" /* Define all the routes here. A new Route entry passed to the  routes slice will be automatically translated to a handler with the NewRouter() function */ type Route struct { Name string Method string Path string HandlerFunc httprouter.Handle } type Routes []Route func AllRoutes() Routes { routes :=  Routes{ Route{"Index", "GET", "/", Index}, Route{"BookIndex", "GET", "/books", BookIndex}, Route{"Bookshow", "GET", "/books/:isdn", BookShow}, Route{"Bookshow", "POST", "/books", BookCreate}, } return routes }Copy the code

Let’s create a NewRouter function, which can be called from the main function, that reads all the routes defined above and returns an available Httprouter.router. So create a file called router.go. We’ll also use the newly created Logger function to wrap the handler.

package main
import "github.com/julienschmidt/httprouter"
//Reads from the routes slice to translate the values to httprouter.Handle
func NewRouter(routes Routes) *httprouter.Router {
	router := httprouter.New()
	for _, route := range routes {
		var handle httprouter.Handle
		handle = route.HandlerFunc
		handle = Logger(handle)
		router.Handle(route.Method, route.Path, handle)
	}
	return router
}Copy the code

Your directory should now look like this:

. ├ ─ ─ handlers. Go ├ ─ ─ handlers_test. Go ├ ─ ─ logger. Go ├ ─ ─ main. Go ├ ─ ─ models. Go ├ ─ ─ responses. Go ├ ─ ─ the router. Go └ ─ ─ routes.goCopy the code

See the full code here [4].

This should get you started writing your own API server. You certainly need to put your functionality in different packages, so a good way to do this is:

. ├ ─ ─ LICENSE ├ ─ ─ the README. Md ├ ─ ─ handlers │ ├ ─ ─ books_test. Go │ └ ─ ─ books. Go ├ ─ ─ models │ ├ ─ ─ the book. Go │ └ ─ ─ * ├ ─ ─ store │ * └ ├ ─ ─ ─ ─ the lib | ├ ─ ─ * ├ ─ ─ main. Go ├ ─ ─ the router. Go ├ ─ ─ rotes. GoCopy the code

If you have a large singleton server, you can also put Handlers, Models, and all routing capabilities in a separate package called app. Just keep in mind that GO doesn’t have cyclic package calls like Java or Scala. So you have to be extra careful with your package structure.

That’s all, and I hope you found this tutorial useful. Cheers!

note

  • [1] github.com/gsingharoy/…
  • [2] github.com/gsingharoy/…
  • [3] github.com/gsingharoy/…
  • [4] github.com/gsingharoy/…
  • [Gorilla mux] github.com/gorilla/mux
  • [httprouter] github.com/julienschmi…
  • [bone] github.com/go-zoo/bone