Armed with the information from the last section, it is possible to use the HTTP package to create services over HTTP. Earlier we discussed the perils of creating services using raw TCP directly when we created a server for our global monetary currency service. In this section, we explore how to create an API server for the same service using HTTP as the underlying protocol. The new HTTP-based service has the following design goals:
The following shows the code involved in the implementation of the new service. This time, the server will use the curr1
package (see github.com/vladimirvivien/learning-go /ch11/curr1) to load and query ISO 4217 currency data from a local CSV file.
The code in the curr1 package defines two types, CurrencyRequest
and Currency
, intended to represent the client request and currency data returned by the server, respectively as listed here:
type Currency struct { Code string `json:"currency_code"` Name string `json:"currency_name"` Number string `json:"currency_number"` Country string `json:"currency_country"` } type CurrencyRequest struct { Get string `json:"get"` Limit int `json:limit` }
golang.fyi/ch11/curr1/currency.go
Note that the preceding struct types shown are annotated with tags that describe the JSON properties for each field. This information is used by the JSON encoder to encode the key name of JSON objects (see Chapter 10, Data IO in Go, for detail on encoding). The remainder of the code, listed in the following snippet, defines the functions that set up the server and the handler function for incoming requests:
import ( "encoding/json" "fmt" "net/http" " github.com/vladimirvivien/learning-go/ch11/curr1" ) var currencies = curr1.Load("./data.csv") func currs(resp http.ResponseWriter, req *http.Request) { var currRequest curr1.CurrencyRequest dec := json.NewDecoder(req.Body) if err := dec.Decode(&currRequest); err != nil { resp.WriteHeader(http.StatusBadRequest) fmt.Println(err) return } result := curr1.Find(currencies, currRequest.Get) enc := json.NewEncoder(resp) if err := enc.Encode(&result); err != nil { fmt.Println(err) resp.WriteHeader(http.StatusInternalServerError) return } } func main() { mux := http.NewServeMux() mux.HandleFunc("/currency", get) if err := http.ListenAndServe(":4040", mux); err != nil { fmt.Println(err) } }
golang.fyi/ch11/jsonserv0.go
Since we are leveraging HTTP as the transport protocol for the service, you can see the code is now much smaller than the prior implementation which used pure TCP. The currs
function implements the handler responsible for incoming requests. It sets up a decoder to decode the incoming JSON-encoded request to a value of the curr1.CurrencyRequest
type as highlighted in the following snippet:
var currRequest curr1.CurrencyRequest dec := json.NewDecoder(req.Body) if err := dec.Decode(&currRequest); err != nil { ... }
Next, the function executes the currency search by calling curr1.Find(currencies, currRequest.Get)
which returns the slice []Currency
assigned to the result
variable. The code then creates an encoder to encode the result
as a JSON payload, highlighted in the following snippet:
result := curr1.Find(currencies, currRequest.Get) enc := json.NewEncoder(resp) if err := enc.Encode(&result); err != nil { ... }
Lastly, the handler function is mapped to the "/currency"
path in the main
function with the call to mux.HandleFunc("/currency", currs)
. When the server receives a request for that path, it automatically executes the currs
function.
Because the server is implemented over HTTP, it can easily be tested with any client-side tools that support HTTP. For instance, the following shows how to use the cURL
command line tool (http://curl.haxx.se/) to connect to the API end-point and retrieve currency information about the Euro
:
$> curl -X POST -d '{"get":"Euro"}' http://localhost:4040/currency
[
...
{
"currency_code": "EUR",
"currency_name": "Euro",
"currency_number": "978",
"currency_country": "BELGIUM"
},
{
"currency_code": "EUR",
"currency_name": "Euro",
"currency_number": "978",
"currency_country": "FINLAND"
},
{
"currency_code": "EUR",
"currency_name": "Euro",
"currency_number": "978",
"currency_country": "FRANCE"
},
...
]
The cURL
command posts a JSON-formatted request object to the server using the -X POST -d '{"get":"Euro"}'
parameters. The output (formatted for readability) from the server is comprised of a JSON array of the preceding currency items.
An HTTP client can also be built in Go to consume the service with minimal efforts. As is shown in the following code snippet, the client code uses the http.Client
type to communicate with the server. It also uses the encoding/json
sub-package to decode incoming data (note that the client also makes use of the curr1
package, shown earlier, which contains the types needed to communicate with the server):
import ( "bytes" "encoding/json" "fmt" "net/http" " github.com/vladimirvivien/learning-go/ch11/curr1" ) func main() { var param string fmt.Print("Currency> ") _, err := fmt.Scanf("%s", ¶m) buf := new(bytes.Buffer) currRequest := &curr1.CurrencyRequest{Get: param} err = json.NewEncoder(buf).Encode(currRequest) if err != nil { fmt.Println(err) return } // send request client := &http.Client{} req, err := http.NewRequest( "POST", "http://127.0.0.1:4040/currency", buf) if err != nil { fmt.Println(err) return } resp, err := client.Do(req) if err != nil { fmt.Println(err) return } defer resp.Body.Close() // decode response var currencies []curr1.Currency err = json.NewDecoder(resp.Body).Decode(¤cies) if err != nil { fmt.Println(err) return } fmt.Println(currencies) }
golang.fyi/ch11/jsonclient0.go
In the previous code, an HTTP client is created to send JSON-encoded request values as currRequest := &curr1.CurrencyRequest{Get: param}
where param
is the currency string to retrieve. The response from the server is a payload that represents an array of JSON-encoded objects (see the JSON array in the section, Testing the API Server with cURL). The code then uses a JSON decoder, json.NewDecoder(resp.Body).Decode(¤cies)
, to decode the payload from the response body into the slice, []curr1.Currency
.
So far, we have seen how to use the API service using the cURL
command-line tool and a native Go client. This section shows the versatility of using HTTP to implement networked services by showcasing a web-based JavaScript client. In this approach, the client is a web-based GUI that uses modern HTML, CSS, and JavaScript to create an interface that interacts with the API server.
First, the server code is updated with an additional handler to serve the static HTML file that renders the GUI on the browser. This is illustrated in the following code:
// serves HTML gui func gui(resp http.ResponseWriter, req *http.Request) { file, err := os.Open("./currency.html") if err != nil { resp.WriteHeader(http.StatusInternalServerError) fmt.Println(err) return } io.Copy(resp, file) } func main() { mux := http.NewServeMux() mux.HandleFunc("/", gui) mux.HandleFunc("/currency", currs) if err := http.ListenAndServe(":4040", mux); err != nil { fmt.Println(err) } }
golang.fyi/ch11/jsonserv1.go
The preceding code snippet shows the declaration of the gui
handler function responsible for serving a static HTML file that renders the GUI for the client. The root URL path is then mapped to the function with mux.HandleFunc("/", gui)
. So, in addition to the "/currency"
path, which hosts the API end-point the "/"
path will return the web page shown in the following screenshot:
The next HTML page (golang.fyi/ch11/currency.html) is responsible for displaying the result of a currency search. It uses JavaScritpt functions along with the jQuery.js
library (not covered here) to post JSON-encoded requests to the backend Go service as shown in the following abbreviated HTML and JavaScript snippets:
<body> <div class="container"> <h2>Global Currency Service</h2> <p>Enter currency search string: <input id="in"> <button type="button" class="btn btn-primary" onclick="doRequest()">Search</button> </p> <table id="tbl" class="table table-striped"> <thead> <tr> <th>Code</th> <th>Name</th> <th>Number</th> <th>Country</th> </tr> </thead> <tbody/> </table> </div> <script> var tbl = document.getElementById("tbl"); function addRow(code, name, number, country) { var rowCount = tbl.rows.length; var row = tbl.insertRow(rowCount); row.insertCell(0).innerHTML = code; row.insertCell(1).innerHTML = name; row.insertCell(2).innerHTML = number; row.insertCell(3).innerHTML = country; } function doRequest() { param = document.getElementById("in").value $.ajax('/currency', { method: 'PUT', contentType: 'application/json', processData: false, data: JSON.stringify({get:param}) }).then( function success(currencies) { currs = JSON.parse(currencies) for (i=0; i < currs.length; i++) { addRow( currs[i].currency_code, currs[i].currency_name, currs[i].currency_number, currs[i].currency_country ); } }); } </script>
golang.fyi/ch11/currency.html
A line-by-line analysis of the HTML and JavaScript code in this example is beyond the scope of the book; however, it is worth pointing out that the JavaScript doRequest
function is where the interaction between the client and the server happens. It uses the jQuery's $.ajax
function to build an HTTP request with a PUT
method and to specify a JSON-encoded currency request object, JSON.stringify({get:param})
, to send to the server. The then
method accepts the callback function, success(currencies)
, which handles the response from the server that parses displays in an HTML table.
When a search value is provided in the text box on the GUI, the page displays its results in the table dynamically as shown in the following screenshot:
13.59.209.131