In order to obtain the places from which our code will randomly build up recommendations, we need to query the Google Places API. In the meander
folder, add the following query.go
file:
package meander type Place struct { *googleGeometry `json:"geometry"` Name string `json:"name"` Icon string `json:"icon"` Photos []*googlePhoto `json:"photos"` Vicinity string `json:"vicinity"` } type googleResponse struct { Results []*Place `json:"results"` } type googleGeometry struct { *googleLocation `json:"location"` } type googleLocation struct { Lat float64 `json:"lat"` Lng float64 `json:"lng"` } type googlePhoto struct { PhotoRef string `json:"photo_reference"` URL string `json:"url"` }
This code defines the structures we will need to parse the JSON response from the Google Places API into usable objects.
Head over to the Google Places API documentation for an example of the response we are expecting. See http://developers.google.com/places/documentation/search.
Most of the preceding code will be obvious, but it's worth noticing that the Place
type embeds the googleGeometry
type, which allows us to represent the nested data as per the API, while essentially flattening it in our code. We do the same with googleLocation
inside googleGeometry
, which means that we will be able to access the Lat
and Lng
values directly on a Place
object, even though they're technically nested in other structures.
Because we want to control how a Place
object appears publically, let's give this type the following Public
method:
func (p *Place) Public() interface{} { return map[string]interface{}{ "name": p.Name, "icon": p.Icon, "photos": p.Photos, "vicinity": p.Vicinity, "lat": p.Lat, "lng": p.Lng, } }
Like with most APIs, we will need an API key in order to access the remote services. Head over to the Google APIs Console, sign in with a Google account, and create a key for the Google Places API. For more detailed instructions, see the documentation on Google's developer website.
Once you have your key, let's make a variable inside the meander
package that can hold it. At the top of query.go
, add the following definition:
var APIKey string
Now nip back into main.go
, remove the double slash //
from the APIKey
line, and replace the TODO
value with the actual key provided by the Google APIs console.
To handle the various cost ranges for our API, it makes sense to use an enumerator (or enum) to denote the various values and to handle conversions to and from string representations. Go doesn't explicitly provide enumerators, but there is a neat way of implementing them, which we will explore in this section.
A simple flexible checklist for writing enumerators in Go is:
iota
keyword to set the values in a const
block, disregarding the first zero valueString
method on the type that returns the appropriate string representation from the mapParseType
function that converts from a string to your type using the mapNow we will write an enumerator to represent the cost levels in our API. Create a new file called cost_level.go
inside the meander
folder and add the following code:
package meander type Cost int8 const ( _ Cost = iota Cost1 Cost2 Cost3 Cost4 Cost5 )
Here we define the type of our enumerator, which we have called Cost
, and since we only need to represent a few values, we have based it on an int8
range. For enumerators where we need larger values, you are free to use any of the integer types that work with iota
. The Cost
type is now a real type in its own right, and we can use it wherever we need to represent one of the supported values—for example, we can specify a Cost
type as an argument in functions, or use it as the type for a field in a struct.
We then define a list of constants of that type, and use the iota
keyword to indicate that we want incrementing values for the constants. By disregarding the first iota
value (which is always zero), we indicate that one of the specified constants must be explicitly used, rather than the zero value.
To provide a string representation of our enumerator, we need only add a String
method to the Cost
type. This is a useful exercise even if you don't need to use the strings in your code, because whenever you use the print calls from the Go standard library (such as fmt.Println
), the numerical values will be used by default. Often those values are meaningless and will require you to look them up, and even count the lines to determine the numerical value for each item.
For more information about the String()
method in Go, see the Stringer
and GoStringer
interfaces in the fmt
package at http://golang.org/pkg/fmt/#Stringer.
To be sure that our enumerator code is working correctly, we are going to write unit tests that make some assertions about expected behavior.
Alongside cost_level.go
, add a new file called cost_level_test.go
, and add the following unit test:
package meander_test import ( "testing" "github.com/cheekybits/is" "path/to/meander" ) func TestCostValues(t *testing.T) { is := is.New(t) is.Equal(int(meander.Cost1), 1) is.Equal(int(meander.Cost2), 2) is.Equal(int(meander.Cost3), 3) is.Equal(int(meander.Cost4), 4) is.Equal(int(meander.Cost5), 5) }
You will need to run go get
to get the CheekyBits' is
package (from github.com/cheekybits/is).
Normally, we wouldn't worry about the actual integer value of constants in our enumerator, but since the Google Places API uses numerical values to represent the same thing, we need to care about the values.
You might have noticed something strange about this test file that breaks from convention. Although it is inside the meander
folder, it is not a part of the meander
package; rather it's in meander_test
.
In Go, this is an error in every case except for tests. Because we are putting our test code into its own package, it means that we no longer have access to the internals of the meander
package—notice how we have to use the package prefix. This may seem like a disadvantage, but in fact it allows us to be sure that we are testing the package as though we were a real user of it. We may only call exported methods and only have visibility into exported types; just like our users.
Run the tests by running go test
in a terminal, and notice that it passes.
Let's add another test to make assertions about the string representations for each Cost
constant. In cost_level_test.go
, add the following unit test:
func TestCostString(t *testing.T) { is := is.New(t) is.Equal(meander.Cost1.String(), "$") is.Equal(meander.Cost2.String(), "$$") is.Equal(meander.Cost3.String(), "$$$") is.Equal(meander.Cost4.String(), "$$$$") is.Equal(meander.Cost5.String(), "$$$$$") }
This test asserts that calling the String
method for each constant yields the expected value. Running these tests will of course fail, because we haven't yet implemented the String
method.
Underneath the Cost
constants, add the following map and the String
method:
var costStrings = map[string]Cost{ "$": Cost1, "$$": Cost2, "$$$": Cost3, "$$$$": Cost4, "$$$$$": Cost5, } func (l Cost) String() string { for s, v := range costStrings { if l == v { return s } } return "invalid" }
The map[string]Cost
variable maps the cost values to the string representation, and the String
method iterates over the map to return the appropriate value.
Now if we were to print out the Cost3
value, we would actually see $$$
, which is much more useful than numerical vales. However, since we do want to use these strings in our API, we are also going to add a ParseCost
method.
In cost_value_test.go
, add the following unit test:
func TestParseCost(t *testing.T) { is := is.New(t) is.Equal(meander.Cost1, meander.ParseCost("$")) is.Equal(meander.Cost2, meander.ParseCost("$$")) is.Equal(meander.Cost3, meander.ParseCost("$$$")) is.Equal(meander.Cost4, meander.ParseCost("$$$$")) is.Equal(meander.Cost5, meander.ParseCost("$$$$$")) }
Here we assert that calling ParseCost
will in fact yield the appropriate value depending on the input string.
In cost_value.go
, add the following implementation code:
func ParseCost(s string) Cost { return costStrings[s] }
Parsing a Cost
string is very simple since this is how our map is laid out.
As we need to represent a range of cost values, let's imagine a CostRange
type, and write the tests out for how we intend to use it. Add the following tests to cost_value_test.go
:
func TestParseCostRange(t *testing.T) { is := is.New(t) var l *meander.CostRange l = meander.ParseCostRange("$$...$$$") is.Equal(l.From, meander.Cost2) is.Equal(l.To, meander.Cost3) l = meander.ParseCostRange("$...$$$$$") is.Equal(l.From, meander.Cost1) is.Equal(l.To, meander.Cost5) } func TestCostRangeString(t *testing.T) { is := is.New(t) is.Equal("$$...$$$$", (&meander.CostRange{ From: meander.Cost2, To: meander.Cost4, }).String()) }
We specify that passing in a string with two dollar characters first, followed by three dots and then three dollar characters should create a new meander.CostRange
type that has From
set to meander.Cost2
, and To
set to meander.Cost3
. The second test does the reverse by testing that the CostRange.String
method returns the appropriate value.
To make our tests pass, add the following CostRange
type and associated String
and ParseString
functions:
type CostRange struct { From Cost To Cost } func (r CostRange) String() string { return r.From.String() + "..." + r.To.String() } func ParseCostRange(s string) *CostRange { segs := strings.Split(s, "...") return &CostRange{ From: ParseCost(segs[0]), To: ParseCost(segs[1]), } }
This allows us to convert a string such as $...$$$$$
to a structure that contains two Cost
values; a From
and To
set and vice versa.
Now that we are capable of representing the results of the API, we need a way to represent and initiate the actual query. Add the following structure to query.go
:
type Query struct { Lat float64 Lng float64 Journey []string Radius int CostRangeStr string }
This structure contains all the information we will need to build up the query, all of which will actually come from the URL parameters in the requests from the client. Next, add the following find
method, which will be responsible for making the actual request to Google's servers:
func (q *Query) find(types string) (*googleResponse, error) { u := "https://maps.googleapis.com/maps/api/place/nearbysearch/json" vals := make(url.Values) vals.Set("location", fmt.Sprintf("%g,%g", q.Lat, q.Lng)) vals.Set("radius", fmt.Sprintf("%d", q.Radius)) vals.Set("types", types) vals.Set("key", APIKey) if len(q.CostRangeStr) > 0 { r := ParseCostRange(q.CostRangeStr) vals.Set("minprice", fmt.Sprintf("%d", int(r.From)-1)) vals.Set("maxprice", fmt.Sprintf("%d", int(r.To)-1)) } res, err := http.Get(u + "?" + vals.Encode()) if err != nil { return nil, err } defer res.Body.Close() var response googleResponse if err := json.NewDecoder(res.Body).Decode(&response); err != nil { return nil, err } return &response, nil }
First we build the request URL as per the Google Places API specification, by appending the url.Values
encoded string of the data for lat
, lng
, radius
, and of course the APIKey
values.
The types
value we specify as an argument represents the kind of business to look for. If there is a CostRangeStr
, we parse it and set the minprice
and maxprice
values, before finally calling http.Get
to actually make the request. If the request is successful, we defer the closing of the response body and use a json.Decoder
method to decode the JSON that comes back from the API into our googleResponse
type.
Next we need to write a method that will allow us to make many calls to find, for the different steps in a journey. Underneath the find
method, add the following Run
method to the Query
struct:
// Run runs the query concurrently, and returns the results. func (q *Query) Run() []interface{} { rand.Seed(time.Now().UnixNano()) var w sync.WaitGroup var l sync.Mutex places := make([]interface{}, len(q.Journey)) for i, r := range q.Journey { w.Add(1) go func(types string, i int) { defer w.Done() response, err := q.find(types) if err != nil { log.Println("Failed to find places:", err) return } if len(response.Results) == 0 { log.Println("No places found for", types) return } for _, result := range response.Results { for _, photo := range result.Photos { photo.URL = "https://maps.googleapis.com/maps/api/place/photo?" + "maxwidth=1000&photoreference=" + photo.PhotoRef + "&key=" + APIKey } } randI := rand.Intn(len(response.Results)) l.Lock() places[i] = response.Results[randI] l.Unlock() }(r, i) } w.Wait() // wait for everything to finish return places }
The first thing we do is set the random seed to the current time in nanoseconds past since January 1, 1970 UTC. This ensures that every time we call the Run
method and use the rand
package, the results will be different. If we didn't do this, our code would suggest the same recommendations every time, which defeats the object.
Since we need to make many requests to Google—and since we want to make sure this is as quick as possible—we are going to run all the queries at the same time by making concurrent calls to our Query.find
method. So we next create a sync.WaitGroup
method, and a map to hold the selected places along with a sync.Mutex
method to allow many go routines to access the map concurrently.
We then iterate over each item in the Journey
slice, which might be bar
, cafe
, movie_theater
. For each item, we add 1
to the WaitGroup
object, and call a goroutine. Inside the routine, we first defer the w.Done
call informing the WaitGroup
object that this request has completed, before calling our find
method to make the actual request. Assuming no errors occurred, and it was indeed able to find some places, we iterate over the results and build up a usable URL for any photos that might be present. According to the Google Places API, we are given a photoreference
key, which we can use in another API call to get the actual image. To save our clients from having to have knowledge of the Google Places API at all, we build the complete URL for them.
We then lock the map locker and with a call to rand.Intn
, pick one of the options at random and insert it into the right position in the places
slice, before unlocking the sync.Mutex
method.
Finally, we wait for all goroutines to complete with a call to w.Wait
, before returning the places.
Now we need to wire up our /recommendations
call, so head back to main.go
in the cmd
folder, and add the following code inside the main
function:
http.HandleFunc("/recommendations", func(w http.ResponseWriter, r *http.Request) { q := &meander.Query{ Journey: strings.Split(r.URL.Query().Get("journey"), "|"), } q.Lat, _ = strconv.ParseFloat(r.URL.Query().Get("lat"), 64) q.Lng, _ = strconv.ParseFloat(r.URL.Query().Get("lng"), 64) q.Radius, _ = strconv.Atoi(r.URL.Query().Get("radius")) q.CostRangeStr = r.URL.Query().Get("cost") places := q.Run() respond(w, r, places) })
This handler is responsible for preparing the meander.Query
object and calling its Run
method, before responding with the results. The http.Request
type's URL value exposes the Query
data that provides a Get
method that, in turn, looks up a value for a given key.
The journey string is translated from the bar|cafe|movie_theater
format to a slice of strings, by splitting on the pipe character. Then a few calls to functions in the strconv
package turn the string latitude, longitude, and radius values into numerical types.
The final piece of the first version of our API will be to implement CORS as we did in the previous chapter. See if you can solve this problem yourself before reading on to the solution in the next section.
If you are going to tackle this yourself, remember that your aim is to set the Access-Control-Allow-Origin
response header to *
. Also consider the http.HandlerFunc
wrapping we did in the previous chapter. The best place for this code is probably in the cmd
program, since that is what exposes the functionality through an HTTP endpoint.
In main.go
, add the following cors
function:
func cors(f http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") f(w, r) } }
This familiar pattern takes in an http.HandlerFunc
type and returns a new one that sets the appropriate header before calling the passed-in function. Now we can modify our code to make sure the cors
function gets called for both of our endpoints. Update the appropriate lines in the main
function:
func main() { runtime.GOMAXPROCS(runtime.NumCPU()) meander.APIKey = "YOUR_API_KEY" http.HandleFunc("/journeys", cors(func(w http.ResponseWriter, r *http.Request) { respond(w, r, meander.Journeys) })) http.HandleFunc("/recommendations", cors(func(w http.ResponseWriter, r *http.Request) { q := &meander.Query{ Journey: strings.Split(r.URL.Query().Get("journey"), "|"), } q.Lat, _ = strconv.ParseFloat(r.URL.Query().Get("lat"), 64) q.Lng, _ = strconv.ParseFloat(r.URL.Query().Get("lng"), 64) q.Radius, _ = strconv.Atoi(r.URL.Query().Get("radius")) q.CostRangeStr = r.URL.Query().Get("cost") places := q.Run() respond(w, r, places) })) http.ListenAndServe(":8080", http.DefaultServeMux) }
Now calls to our API will be allowed from any domain without a cross-origin error occurring.
Now that we are ready to test our API, head to a console and navigate to the cmd
folder. Because our program imports the meander
package, building the program will automatically build our meander
package too.
Build and run the program:
go build –o meanderapi ./meanderapi
To see meaningful results from our API, let's take a minute to find your actual latitude and longitude. Head over to http://mygeoposition.com/ and use the web tools to get the x,y
values for a location you are familiar with.
Or pick from these popular cities:
51.520707 x 0.153809
40.7127840 x -74.0059410
35.6894870 x 139.6917060
37.7749290 x -122.4194160
Now open a web browser and access the /recommendations
endpoint with some appropriate values for the fields:
http://localhost:8080/recommendations? lat=51.520707&lng=-0.153809&radius=5000& journey=cafe|bar|casino|restaurant& cost=$...$$$
The following screenshot shows what a sample recommendation around London might look like:
Feel free to play around with the values in the URL to see how powerful the simple API is by trying various journey strings, tweaking the locations, and trying different cost range value strings.
We are going to download a complete web application built to the same API specifications, and point it at our implementation to see it come to life before our eyes. Head over to https://github.com/matryer/goblueprints/tree/master/chapter7/meanderweb and download the meanderweb
project into your GOPATH
.
In a terminal, navigate to the meanderweb
folder, and build and run it:
go build –o meanderweb ./meanderweb
This will start a website running on localhost:8081
, which is hardcoded to look for the API running at localhost:8080
. Because we added the CORS support, this won't be a problem despite them running on different domains.
Open a browser to http://localhost:8081/
and interact with the application, while somebody else built the UI it would be pretty useless without the API that we built powering it.