Oh hi! This is a multi-part tutorial series, please make sure you reads the posts in order! You can find the list at the bottom of the page.
We’ve come a long way, from a simple Hello World to a fully functional dialog search engine for Star Trek: The Next Generation. This is quite an achievement! But how can we improve on it further? Wouldn’t it be nice to see some pictures with our search results? Picard’s shiny head, or Riker’s magical beard for example.
Another API
Unfortunately, the API we’re querying doesn’t contain any image data or URLs. However, it does contain the IMDB ID of the episode, which we can use to query a further API for some image data. To do this, we’re going to use The Movie Database (TMDb), a community driven movie database with a fantastic public API.
To use this API, you will need to sign up for a free TMDb developer account and create your own API key. Full instructions can be found here. Make a note of the key - we’ll use it in a moment.
More structs
We’re going to be passing multiple sources of data to our template, so we’ll need to change the way we’re using our structs. First, we’ll change the Dialog []struct
. Rather than keep this struct as a slice, we’ll make it a singular struct (you’ll see why in a moment). To do this, just change its first line to:
type Dialog struct {
Now we’ll create a new type of struct, which will contain all of the results we want to send to our template. Enter this after the complete defintion for the Dialog
struct:
type Results struct {
SearchKey string
Lines []Dialog
Images map[string]string
}
As you can see, we’re now creating as field called Lines
, which will be the slice of Dialog
s. Inside this new struct, we also store our original SearchKey
, and a new field called Images
. This is a map, a series of key+value pairs. You may be familiar with maps from Java, or dictionaries from Python, which are essentially the same thing.
Next we need to create the struct for the TMDb API. Enter the following:
type TMDBQuery struct {
MovieResults []string `json:"-"`
PersonResults []string `json:"-"`
TvResults []string `json:"-"`
TvEpisodeResults []struct {
AirDate string `json:"air_date"`
EpisodeNumber int `json:"episode_number"`
ID int `json:"id"`
Name string `json:"name"`
Overview string `json:"overview"`
ProductionCode string `json:"production_code"`
SeasonNumber int `json:"season_number"`
ShowID int `json:"show_id"`
StillPath string `json:"still_path"`
VoteAverage float64 `json:"vote_average"`
VoteCount int `json:"vote_count"`
} `json:"tv_episode_results"`
TvSeasonResults string `json:"-"`
}
There’s quite a lot here but it’s easy to break down. First of all - ignore MovieResults
, PersonResults
and TvSeasonResults
. The API we’re querying could potentially return results in these 3 keys, but we don’t want to use them - we know the IDs we are sending in our query will only return TvEpisodeResults
. That’s why we are tagging them with json:"-"
. This tells the JSON Decoder to completely ignore them, and anything they might contain. Inside TvEpisodeResults
we can see all the data we will scrape from this API, the most important part being StillPath
, which will lead us to a public URL for a screenshot from the episode.
TvEpisodeResults
is also a slice, as denoted by []struct
, because the API could potentially return mutliple results in this field. However, as we’re sending the IMDB ID in our query, we can be sure that this slice will only contain a single entry.
Using the API key
Remember that API key you got from TMBb? Store it in a string variable now. Only joking! Of course, hard-coding API keys and other sensitive information inside your program is a terrible idea. Instead, just above the var tpl
defintion, add this line:
var API_KEY string
We’re declaring the variable, but we haven’t stored anything in it yet. Jump down into your main()
function and add this at the start of it:
API_KEY = os.Getenv("API_KEY")
if API_KEY == "" {
fmt.Println("No API_KEY in environment")
os.Exit(1)
}
os.Getenv
grabs the API_KEY from the runtime environment. Then we check to make sure the variable isn’t empty - if it is, we quit with an error message.
On Linux and Mac systems, you can set an environment variable like this:
export API_KEY=EJ5RkgowmPfwSIra9EDqelQirMOSoyd6
(Obviously, replace the key with the one you’ve obtained. That’s not a real key!) I’ve never tried this with Windows, but I googled this for you :)
Updating the search handler
Next, we’ll make several changes to our searchHandler
function. To start with, replace:
dialog = &Dialog{}
with:
results := &Results{}
results.Images = make(map[string]string)
results.SearchKey = searchKey
We’re now creating an instance of our new Results
struct, which can store the search key, lines of dialog, and a map of images. We call make
to set up our empty map, specifying that the keys and values will both be strings. Then we store the searchKey
we retreived from the HTTP request as results.SearchKey
.
The next few parts of our function stay the same. We still conctact the original API and check for errors or return codes that are not OK. But we’ll change the call to json.NewDecoder
to:
err = json.NewDecoder(resp.Body).Decode(&results.Lines)
We’re now storing the results in Lines
, a field of our results
object. Leave the next piece of error checking in place. The next line should be spew.Dump(dialog)
. We’ll delete that because we no longer need this level of debug, and we no longer have a variable called dialog
!
Now comes a rather large chunk of code to enter:
for _, d := range results.Lines {
tmdbquery := &TMDBQuery{}
imdbid := d.Imdbid
_, ok := results.Images[imdbid]
if !ok {
tmdbep := fmt.Sprintf("https://api.themoviedb.org/3/find/%s?api_key=%s&language=en-US&external_source=imdb_id", imdbid, API_KEY)
tmdbresp, err := http.Get(tmdbep)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
defer tmdbresp.Body.Close()
if tmdbresp.StatusCode != 200 {
w.WriteHeader(http.StatusInternalServerError)
return
}
err = json.NewDecoder(tmdbresp.Body).Decode(&tmdbquery)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
if len(tmdbquery.TvEpisodeResults) > 0 {
results.Images[imdbid] = tmdbquery.TvEpisodeResults[0].StillPath
} else {
results.Images[imdbid] = "8do6gZErem4wfdPwALiT8agtJfb.jpg"
}
}
}
Phew! Let’s go through some of that to clarify what it’s doing, although none of it is particularly complicated. This is our first for loop in Go. This is actually the only looping construct in Go - if you’re used to whiles or untils you’ll have to get used to doing everything with for.
Using range results.Lines
will iterate through all the objects in the Lines
slice, providing us with 2 variables at a time: the index of the slice, and the value itself. We don’t need the index, so we use the dummy variable _
, and we store the value as d
.
Next, we create an instance of TMDBQuery
to store the results we’re about to get, and we grab the IMDB ID of our current result and store it as imdbid
. In a moment we’ll start storing image URLs in the map we created earlier.
The next line looks a little odd: _, ok := results.Images[imdbid]
, but all this does is check to see if we already have an image in our map for this IMDB ID. If we do, we skip the next section entirely. This saves time and API calls. For example, if we queried the dialog “Darmok”, we’ll get a few dozen results, but they’ll all have the same IMDB ID (they’re all from the same episode!). So we only need to get the image URL once.
Inside the next block, we’re only proceeding if there’s no match in the map (if !ok
). We prep the API URL as tmdbep
using the IMDB ID and our API key. The next few chunks of code should look very familiar. In the same way as we queried the original API, we send a request, do some error checking on the response, then use the JSON Decoder to store the results in a struct.
Finally, we check to see how many results are in TvEpisodeResults
. If there’s more than zero, we know we have a match (and only one match). So we can grab that index and its image URL: tmbdquery.TvEpisodeResults[0].StillPath
.
If for some reason there was no match, we’ve failed to find an image for this episode. So instead, we fall back to a hard-coded URL, which is a lovely full cast shot of our starship crew.
One last line of code to change! We need to send the new struct to our template. So replace:
err = tpl.Execute(w, dialog)
with:
err = tpl.Execute(w, results)
Updating the template
We’re so close! Because we’ve changed the struct that’s being passed to our template, we need to update the template itself. First, add the following to the end of style.css
:
.episode-image {
width: 200px;
flex-grow: 0;
flex-shrink: 0;
margin-left: 20px;
}
Now update the <section class="container">
part of the index.html
file:
<section class="container">
<ul class="search-results">
{{ range .Lines }}
<li class="dialog">
<div>
<h3 class="title"><a href="https://www.imdb.com/title/{{ .Imdbid }}/">{{ .Episodename }}</a></h3>
{{ if eq .Texttype "speech" }}
<p class="description">{{ .Who }}: <i>"{{ .Text }}"</i></p>
{{ else }}
<p class="description">{{ .Text }}</p>
{{ end }}
<p>Act {{ .Act }} Scene {{ .Scenenumber }}</p>
<p>Season {{ .Season }} Episode {{ .Episode }}</p>
</div>
<img class="episode-image" src="https://image.tmdb.org/t/p/w454_and_h254_bestv2/{{ index $.Images .Imdbid }}">
</li>
{{ end }}
</ul>
</section>
The changes here are quite self-explanatory. Rather than range through a single object (as .
) we use the .Lines
field of the struct. To make things look a bit nicer, we’ve also removed the word “Episode” and again used the Imbdid
to link to the actual IMDB page for an episode.
Then we add an image tag, using the start of the TMDB media URL, but appending a string we grab from the Images
map using Imdbid
as a key. Pretty neat, huh?
Now we’re finally done! Save everything and restart your Go server. I’m sure you know how by now :)
Results!
Conclusion
We’ve successfully built a reasonably complex web application in Go, that queries 2 different HTTP APIs and performs some pretty advanced template rendering.
Does this mean that we’re now at least basically competent in Go? For me, the answer is No. There are still plenty of concepts I’ve yet to grasp. But the difference for me is that now I’ve actually built something.
Before, I would look at a Go program and shudder. I would attempt the Tour of Go or Go by Example and just fall asleep mystified, as I had no context to apply to either of those rather verbose tutorials. Now I’ve seen a program work in the wild, and it all makes just a little bit more sense. It’s a solid place to start the continuing voyage of learning to Go!
For some next steps, I’d encourage you to try a couple of things on your own:
Can you add a count to the number of results?
Can you pre-populate the search box with the search key when you show results? Don’t forget it’s already in the struct being passed to the template.
If you’d like to try another step-by-step Go tutorial before jumping straight back into the comprehensive stuff, I’d also highly recommend Daniela Patruzalek’s Pac Go (a Pac Man clone written in Go). Dani was a huge inspiration for me in writing this series!
I really hope you’ve enjoyed following along. Let me know your thoughts, and share your own Go learning experience :)
If you need to check your code, here are the full gists: main.go, index.html and style.css.