I've been wanting to play around with the Go programming language for a while now so I figured a blog post would be a great opportunity to do so. Today we'll be building Go functions which will interact with the JSON API for the HostIP service. This simple service offers location information for an IP address. The API documentation is located here. The complete code for this post is located as a gist on my Github. For the sake of simplicity, I will be requesting location data encoded in JSON. Here is a sample request and response:

Request: GET http://api.hostip.info/get_json.php?position=true&ip=198.252.206.16

Response:
{
    "country_name": "UNITED STATES",
    "country_code": "US",
    "city": "Cambridge, MA",
    "ip": "198.252.206.16",
    "lat": "42.3758",
    "lng": "-71.1187"
}


Note: The lat and lang field can sometimes return null, so we will have to take this into consideration when working with the data.

The first thing we will be doing is creating a generic function which sends a HTTP GET request and receives a response. For larger API clients you'd want to generalize this for any type of HTTP request but for the sake of simplicity and shortness we will stick to GETs. We will be using the net/http and the io/ioutil packages for sending HTTP requests and reading byte arrays respectively. We begin with these imports:

1
2
3
4
import (
    "io/ioutil"
    "net/http"
)

Our function will accept a URL as a string, create a HTTP client, send the request, and return the response (or any errors that may have occured along the way). Thus our function returns a pair (byte[], error). So our function shell will look like:

1
2
3
func getContent(url string) ([]byte, error) {
    // magic
}

First thing we will do is build a new request using the NewRequest function. If we encounter any error, we simply exit out of our function returning nil as our byte array and the error itself. We then make a new HTTP client and send off the request using Client.Do. The good thing about the Do function is that it can be generalized to any type of HTTP method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Build the request
req, err := http.NewRequest("GET", url, nil)
if err != nil {
    return nil, err
}

// Send the request via a client
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
    return nil, err
}

Now that we've actualy sent off our request we want to get our received content. Note that the HTTP Request type wraps the body in a ReadCloser, so once we've obtained our content it's important to close the body. To make sure we don't forget to do so, we defer a call to the close function right way by using the defer keyword. This special keyword allows us to delay the execution of the statement until we return from the function. That way we can defer our call right away to make sure we don't forget about it and then do all the processing we need within the function while knowing our stream will safely be closed afterwards. Once that's done, we simply read from the body into a byte array using ReadAll, which takes in a reader and reads until we hit an EOF (or an error). We finish off our method like so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// This function fetch the content of a URL will return it as an
// array of bytes if retrieved successfully.
func getContent(url string) ([]byte, error) {
    // Build the request
    req, err := http.NewRequest("GET", url, nil)
    if err != nil {
      return nil, err
    }
    // Send the request via a client
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
      return nil, err
    }
    // Defer the closing of the body
    defer resp.Body.Close()
    // Read the content into a byte array
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
      return nil, err
    }
    // At this point we're done - simply return the bytes
    return body, nil
}

We now have an easy way to get the content from a webpage. If you want to test this out you can convert a byte array to a string like so:

1
2
3
4
5
6
7
content, err := getContent("http://www.codingcookies.com/")
if err != nil {
    // Uh-oh! 
} else {
    // Note that this will require you add fmt to your list of imports.
    fmt.Println(string(content))
}

At this point we're ready to convert our JSON into someting useful! We'll be creating an IpRecord type which will be loaded with all our data received. The encoding/json package makes this extremely easy so go ahead and add that to your imports:

1
2
3
4
5
import (
    "encoding/json"
    "io/ioutil"
    "net/http"
)

We now want to define our structure. We will be using the Unmarshal function to transform our JSON bytes into the appropriate structure. This function is extremely handy and takes care of mapping the fields in JSON objects to the corresponding named field in the structure. Note that the function first searches for fields in the structure with the exact same name (case sensitive) followed by fields with the same name but varying in case, so we aren't restricted by case. We can also tag a field in the stucture to map to a different named field in the JSON object. An example of this is the country_name JSON field, which we will map to a field in the structure named CountryName. So our basic structure will look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
type IpRecord struct {
    // These two fields use the json: tag to specify which field they map to
    CountryName string `json:"country_name"`
    CountryCode string `json:"country_code"`
    // These fields are mapped directly by name (note the different case)
    City string
    Ip   string
    // As these fields can be nullable, we use a pointer 
    // to a string rather than a string
    Lat *string
    Lng *string
}

As you can see, the structure matches the JSON object quite closely. The Unmarshal function accepts a byte array and a reference to the object which shall be filled with the JSON data (this is simplifying, it actually accepts an interface). Note that this function will return either nil if successful, else an error. We can combine this with our getContent function to make a function which accepts an IP in a string and will return the IpRecord for it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// This function will attempt to get the IP record for
// a given IP. If no errors occur, it will return a pair
// of the record and nil. If it was not successful, it will
// return a pair of nil and the error.
func GetIpRecord(ip string) (*IpRecord, error) {
    // Fetch the JSON content for that given IP
    content, err := getContent(
      fmt.Sprintf("http://api.hostip.info/get_json.php?position=true&ip=%s", ip))
    if err != nil {
        // An error occurred while fetching the JSON
        return nil, err
    }
    // Fill the record with the data from the JSON
    var record IpRecord
    err = json.Unmarshal(content, &record)
    if err != nil {
        // An error occurred while converting our JSON to an object
        return nil, err
    }
return &record, err
}

And with this we are done! Feel free to play around it with it! Here's some example output (note that the memory address gets printed for latitude and longitude):

1
2
3
4
5
6
7
    // Note this example requires fmt in the list of imports
    record, _ := GetIpRecord("72.21.194.212")
    fmt.Printf("amazon.com information:\n%v\n", record)
    record, _  = GetIpRecord("74.125.131.141")
    fmt.Printf("golang.org information:\n%v\n", record)
    record, _  = GetIpRecord("108.162.195.222")
    fmt.Printf("codingcookies.com information:\n%v\n", record)

This will output:

stackoverflow.com information:
&{UNITED STATES US Cambridge, MA 198.252.206.16 0x122b0888 0x122b08d0}
hostip.info information:
&{NETHERLANDS NL Enschede 184.72.186.1 0x122b0b20 0x122b0b68}
codingcookies.com information:
&{(Unknown Country?) XX (Unknown City?) 108.162.195.222 <nil> <nil>}


As a challenge to your self, modify the GetIpRecord function to work so that when an empty string (or a null string) is passed, it will fetch your own IP information (hint: check the API documentation). Again all the code from this post is located on this gist.

I hope you enjoyed this post, thanks for reading!

Dominic