Golang: Building an API server with Httprouter

Source: Internet
Author: User
Tags gopher response code
This is a creation in Article, where the information may have evolved or changed. I began to become a Gopher 10 months ago, without turning back. Like many other gopher, I quickly discovered that simple language features are very useful for quickly building fast, scalable software. When I first started learning Go, I was playing with a different multiplexer (multiplexer) that could be used as an API server. If you have Rails backgrounds like me, you may also encounter difficulties in building all the features that the WEB framework provides. Back to the multiplexer, I found 3 very useful good things, namely [Gorilla MUX] (Https://github.com/gorilla/mux), [Httprouter] (https://github.com/ Julienschmidt/httprouter) and [bone] (Https://github.com/go-zoo/bone) (ranked by performance from low to high). Even though the bone has the best performance and a simpler handler signature, it is still not mature enough for me to use in the production environment. So, I ended up using Httprouter. In this tutorial, I'll use httprouter to build a simple REST API server. <!--more-and if you want to be lazy, just want to get the source code, you can in [here] (HTTPS://GITHUB.COM/GSINGHAROY/HTTPROUTER-TUTORIAL/TREE/MASTER/PART4) [4 ] to check out my GitHub repository directly. Let's get started. First create a basic endpoint: "' Gopackage mainimport (" 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)} ' "In the above code snippet, ' Index ' is a handler function that requires passing three parameters. After that, the HandlER will be registered to 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] (HTTPS://GITHUB.COM/GSINGHAROY/HTTPROUTER-TUTORIAL/TREE/MASTER/PART1) [1] to get the current code. Now we can make the API a little more complicated. We now have an entity called ' book ' that can use the ' ISDN ' field as a unique identifier. Let's create more actions, that is, the sub-table represents the Get '/books ' and get '/books/:isdn ' of the Index and Show actions. Our ' main.go ' file is now as follows: ' ' Gopackage mainimport ("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 would 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 Responsemeta 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 databasevar bookstore = m Ake (Map[string]*book)//Handler for the books Index action//get/booksfunc 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/:isdnfunc 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 at the URL has been FOUNDW. 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 entriesbookstore["123"] = &book{isdn: "123", Title: "Sil Ence 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)} ' If you now attempt to request ' GET https://Localhost:8080/books ', you will get the following response: ' ' go{' 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}]} "We have hardcoded these two book entities in the ' main ' function. Click [here] (HTTPS://GITHUB.COM/GSINGHAROY/HTTPROUTER-TUTORIAL/TREE/MASTER/PART2) [2] to get the current stage of theCode. Let's refactor the code. So far, all of our code has been placed in the same file: ' Main.go '. We can move them to individual files. At this point we have a directory: ". ├──handlers.go├──main.go├──models.go└──responses.go" We move all structures associated with the ' JSON ' response to ' responses.go ', The handler function moves to ' handlers.go ' and moves the ' book ' Structure to ' models.go '. Click [here] (HTTPS://GITHUB.COM/GSINGHAROY/HTTPROUTER-TUTORIAL/TREE/MASTER/PART3) [3] To view the code for the current stage. Now, let's jump over and write some tests. In Go, the ' *_test.go ' file is used for testing. So let's create a ' handlers_test.go '. "' Gopackage mainimport (" Net/http "" Net/http/httptest "" Testing "" Github.com/julienschmidt/httprouter ") func Testbookindex (t *testing. T) {//Create an entry of the bookstore Maptestbook: = &book{isdn: "111", Title: "Test title", Author: "Test Author ", pages:42,}bookstore[" 111 "] = testbook//A request with an existing isdnreq1, err: = http. Newrequest ("Get", "/books", nil) if err! = Nil {t.fatal (err)}rr1: = Newrequestrecorder (req1, "get", "/books", Bookindex) if Rr1. Code! = T.error ("Expected response code to be")}//expected Responseer1: = "{\" 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. Responserecorderfunc 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} ' We use the ' httptest ' package of Recorder to mock handler. Similarly, you can write tests for handler ' bookshow '. Let's do some refactoring a little bit. We still have all the routes defined in the ' main ' function, Handler looks a bit bloated, we can do some DRY, we still output some log messages in the terminal, and can add a ' bookcreate ' handler to create a new book. First, let's solve the ' handlers.go '. "' Gopackage mainimport (" 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/booksfunc 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] = Bookwriteokresponse (w, book)}//Handler for the books Index action//get/booksfunc bookindex (w http. Responsewriter, R *http. Request, _ Httprouter. Params) {Books: = []*book{}for _, Book: = Range Bookstore {books = append (books, book)}writeokresponse (w, books)}//Handle R for the books Show action//get/books/:isdnfunc 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 URLHas been Foundwriteerrorresponse (W, http. Statusnotfound, "Record not Found") Return}writeokresponse (w, book)}//writes the response as a standard JSON response with Statusokfunc 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 R Esponse codefunc 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 Handlerfunc 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} ' I created two functions, ' writeokresponse ' is used to write ' Statusok ' to the response, which returns a model or a model slice, ' Writeerro Rresponse ' JSON ' error will be responded to when an expected or unexpected error occurs. Like any good gopher, we should not panic. I also added a function called ' Populatemodelfromhandler ', which parses the content from the body into any model (struct) that is required. In this case, we use it in the ' bookcreate ' handler to populate a ' book '. Now, let's take a look at the logs. We simply create a ' Logger ' function that wraps the handler function and prints the log message before and after the handler function is executed. "' Gopackage mainimport (" Log "" Net/http "" Time "" github.com/julienschmidt/httprouter ")//A Logger function which simply Wraps the handler function around some log messagesfunc 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)}} "" Let's look at the route. First, all routes are defined centrally in one place, such as ' Routes.go '. "' Gopackage mainimport" Github.com/julienschmidt/httprouter "/*define all the routes here. A new Route entry passed to the routes slice would be automaticallytranslated to a handler with the Newrouter () function*/t ype Route struct {Name stringmethod stringpath stringhandlerfunc httprouter. Handle}type Routes []routefunc 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} "Let's create a ' newrouter ' function that can be called in the ' main ' function, which reads all the routes defined above and returns a usable ' Httprouter. Router '. So create a file ' Router.go '. We will also use the newly created ' Logger ' function to wrap the handler. "' Gopackage mainimport" Github.com/julienschmidt/httprouter "//reads from the routes slice to translate the values to HTTP Router. HanDlefunc Newrouter (routes routes) *httprouter. Router {Router: = Httprouter. New () For _, route: = Range routes {var handle httprouter. Handlehandle = route. Handlerfunchandle = Logger (handle) router. Handle (route. Method, route. Path, handle)}return router} "Your directory should look like this at this time:". ├──handlers.go├──handlers_test.go├──logger.go├──main.go├── Models.go├──responses.go├──router.go└──routes.go "in [here] (https://github.com/gsingharoy/httprouter-tutorial/ TREE/MASTER/PART4) [4] View the full code. This should allow you to start writing your own API server. Of course you need to put your functions in different packages, so a good way is: ". ├──license├──readme.md├──handlers│├──books_test.go│└──books.go├──models│├── book.go│└──*├──store│├──*└──lib| ├──*├──main.go├──router.go├──rotes.go "If you have a large single server, you can also place ' handlers ', ' models ' and all routing functions in another package called ' app '. Just remember that go doesn't have a looping package call like Java or Scala. So you have to be extra careful with your package structure. This is the whole content, I hope this tutorial can be useful to you. Cheers! # # NOTE-[1] https://github.com/gsingharoy/httprouter-tutorial/tree/master/part1-[2] https://github.com/gsingharoy/ httprouter-tutorial/tree/master/part2-[3] Https://github.com/gsingharoy/httprouter-tutorial/tree/master/part3-[4] https://github.com/gsingharoy/httprouter-tutorial/ tree/master/part4-[Gorilla mux] https://github.com/gorilla/mux-[Httprouter] https://github.com/julienschmidt/ httprouter-[Bone] https://github.com/go-zoo/bone> [https://medium.com/@gauravsingharoy/ BUILD-YOUR-FIRST-API-SERVER-WITH-HTTPROUTER-IN-GOLANG-732B7B01F6AB] (https://medium.com/@gauravsingharoy/ BUILD-YOUR-FIRST-API-SERVER-WITH-HTTPROUTER-IN-GOLANG-732B7B01F6AB) > Author: Gaurav Singha roy> Translator: oopsguy.com2618 Click  ∙  1 likes  
Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.