Consuming APIs With Go

http://tracehelms.com / Twitter: @tracehelms

Who am I?

I'm a Ruby dev from Florida.

Moved here a year ago with my dog Baron.

Pretty new to Golang.

Let's Set The Mood

It's a weekday...

You're at work...

You're hungry...

And someone mentions lunch!

It's lunch time! Everybody, let's get some grub!

...But where do we go?

...

Uhh...

...

Decision Fatigue

"Decision fatigue refers to the deteriorating quality of decisions made by an individual, after a long session of decision making."

- Wikipedia

Enter Golunch


~/ $ golunch -location=80304 mexican
Time for grub! Your destination:
Name:      Pica's Taqueria
Rating:    3.5
Location:  [5360 Arapahoe Ave Ste F Boulder, CO 80303]
URL:       http://www.yelp.com/biz/picas-taqueria-boulder
          

Yelp APi

  • Search for restaurants
    • Keyword
    • Location
  • OAuth 1.0a
  • JSON response

Program Flow

  1. *** Parse user input, i.e. "-location=80304 mexican"
  2. Set up necessary authentication stuff
  3. Make HTTP GET request (credentials are passed in Authentication header)
  4. Parse JSON response into something we can work with in Go
  5. Pick a random restaurant and show it to the user

Command Line Arguments

package main

import (
  "flag"
  "github.com/tracehelms/golunch/yelp"
)

func main() {
  location := flag.String("location", "80304",
    "the location you want to search in")
  flag.Parse()

  query := flag.Args()[0]
  results := yelp.New().Search(query, *location)
  ...
}

Note: "os" package won't work with "flag" package

Program Flow

  1. Parse user input, i.e. "-location=80304 mexican"
  2. *** Set up necessary authentication stuff
  3. Make HTTP GET request (credentials are passed in Authentication header)
  4. Parse JSON response into something we can work with in Go
  5. Pick a random restaurant and show it to the user

HTTP Client

  • Client holds our HTTP client (for making requests, required by "oauth" package) and our credentials
  • We can also call the Search method on our client.
package yelp

type Client struct {
  Options *Credentials
  Client  *http.Client
}

func New() *Client {
  ...
  return &Client{
    Client:  http.DefaultClient,
    Options: &creds, // comma, wtf?!
  }
}
# Usage:
# client := yelp.New()

Authentication

File: yelp_keys.json

Keep this out of version control!

{
  "ConsumerKey": "nacho_key",
  "ConsumerSecret": "nacho_secret",
  "AccessToken": "nacho_token",
  "AccessTokenSecret": "nacho_token_secret"
}

Convert it to a struct like so

type Credentials struct {
  ConsumerKey       string
  ConsumerSecret    string
  AccessToken       string
  AccessTokenSecret string
}

HTTP Client (revisited)

Remember the New() method? Now we have credentials!

func New() *Client {
  data, err := ioutil.ReadFile("yelp_keys.json")
  if err != nil {
    panic(err)
  }
  var creds Credentials
  err = json.Unmarshal(data, &creds)
  if err != nil {
    panic(err)
  }

  return &Client{
    Client:  http.DefaultClient,
    Options: &creds, // comma, wtf?!
  }
}

Program Flow

  1. Parse user input, i.e. "-location=80304 mexican"
  2. Set up necessary authentication stuff
  3. *** Make HTTP GET request (credentials are passed in Authentication header)
  4. Parse JSON response into something we can work with in Go
  5. Pick a random restaurant and show it to the user

Searching

  • Method on the Client struct
  • Usage: client.Search("bacon", "80304")
func (client *Client) Search(query string, location string)
  SearchResult {

  params := map[string]string{
    "term":     query,
    "location": location,
    "limit":    "20",
  }

  resp := MakeRequest(client, params)

  ...
}

MakeRequest() ....what does that do?

Making Request

  • Remember: client contains an HTTP client for making requests and our API credentials
  • We set credentials (and our HTTP client) on oauth object
func MakeRequest(client *Client, params map[string]string)
  *http.Response {

  c := oauth.NewCustomHttpClientConsumer(
    client.Options.ConsumerKey,
    client.Options.ConsumerSecret,
    oauth.ServiceProvider{RequestTokenUrl: "",
      AuthorizeTokenUrl: "", AccessTokenUrl: "", },
    client.Client,
  )

  token := &oauth.AccessToken{
    client.Options.AccessToken,
    client.Options.AccessTokenSecret,
    make(map[string]string),
  }

Making Request (cont.)

With our credentials set, now we can call the Get() method

  ...
  resp, err := c.Get(url, params, token)
  if err != nil {
    panic(err)
  }
  return resp
}

Program Flow

  1. Parse user input, i.e. "-location=80304 mexican"
  2. Set up necessary authentication stuff
  3. Make HTTP GET request (credentials are passed in Authentication header)
  4. *** Parse JSON response into something we can work with in Go
  5. Pick a random restaurant and show it to the user

Parsing Response

Remember our friend the Search() method?

func (client *Client) Search(query string, location string)
  SearchResult {

  params := ...
  resp := MakeRequest(client, params)

  defer resp.Body.Close()

  var r SearchResult
  err := json.NewDecoder(resp.Body).Decode(&r)
  if err != nil {
    panic(err)
  }
}

Note: SearchResult

Also note: Error handling sucks

Parsing Response (cont.)

Example JSON Response

{
  "businesses": [
    {
      "name": "Big Kahuna Burger",
      "rating": "5",
      "url": "http://bigkahuna.com",
      "location": {
        "display_address": ["123 Pearl St.", "Boulder CO", "80304"]
      }
    },
    more businesses...
  ]
}
          

Parsing Response (cont.)

import (
  "encoding/json"
)

type SearchResult struct {
  Businesses []Business `json:"businesses"`
}
type Business struct {
  Name     string   `json:"name"`
  Rating   float32  `json:"rating"`
  Url      string   `json:"url"`
  Location Location `json:"location"`
}
type Location struct {
  Address []string `json:"display_address"`
}

Note: nested structs

We finally have results!

We can eat soon!

Program Flow

  1. Parse user input, i.e. "-location=80304 mexican"
  2. Set up necessary authentication stuff
  3. Make HTTP GET request (credentials are passed in Authentication header)
  4. Parse JSON response into something we can work with in Go
  5. *** Pick a random restaurant and show it to the user

Random Restaurant

func main() {
  location := flag.String("location", "80304",
    "the location you want to search in")
  flag.Parse()
  query := flag.Args()[0]

  results := yelp.New().Search(query, *location)
  count := len(results.Businesses)
  if count <= 0 {
    fmt.Println("No results were found!")
    return
  }
  r := rand.New(rand.NewSource(time.Now().UnixNano()))
  randNumber := r.Intn(count)

  printResult(results.Businesses[randNumber])
}

results is a slice of Businesses, let's pick one!

Show the User

func printResult(business yelp.Business) {
  fmt.Println("Time for grub! Your destination:")
  fmt.Println("Name:     ", business.Name)
  fmt.Println("Rating:   ", business.Rating)
  fmt.Println("Location: ", business.Location.Address)
  fmt.Println("URL:      ", business.Url)
}

Reminder: This is what you sat here for

~/ $ golunch -location=80304 mexican
Time for grub! Your destination:
Name:      Pica's Taqueria
Rating:    3.5
Location:  [5360 Arapahoe Ave Ste F Boulder, CO 80303]
URL:       http://www.yelp.com/biz/picas-taqueria-boulder

Recap

  1. Parse user input, i.e. "-location=80304 mexican"
  2. Set up necessary authentication stuff
  3. Make HTTP GET request (credentials are passed in Authentication header)
  4. Parse JSON response into something we can work with in Go
  5. Pick a random restaurant and show it to the user

Now We Can Eat

Gotchas

  • No tests (shame-face)
  • Bad error handling
  • Must have yelp_keys.json in whatever folder you run golunch from...looking to fix this

Sources

Thanks!

GitHub

http://github.com/tracehelms/golunch

Slides

http://tracehelms.com/talk_consuming_apis_with_go