Interested in this project?
Continue LearningIntroduction
Dependencies
It would be beneficial to have a basic understanding of HTML and CSS before starting this tutorial. If you are comfortable with the concepts in the To-Do List App, you should be more than fine for this tutorial!
What is Go?
Go or Golang is a language created by Google. Rumor has it that it was conceived by Google developers when they were waiting for the code compilation to complete in a project.
The reason that Go is special is mostly because of its speed. It compiles and runs very quickly. It also has many features that allows processes to run concurrently (known as goroutines).
When should I use Go?
Go should be used for anything that requires speed and has precise memory or hardware requirements. It is most similar to C++ but it has more functionality. Additionally, with Go you are highly unlikely to run into errors while its running because it pre-checks all of your code (it is a compiled language).
You might use Go over Python or JavaScript for web development if you are looking for speed or require high amounts of processing.
Getting Started and Set Up
Setup Go
Download the Go tools here: https://golang.org/doc/install. Next, you will want to download VSCode if you haven't already. VSCode has extensions that make golang development very nice.
In VSCode, click the icon on the left that looks like building blocks as shown in this photo.
Search for "Go" and install the first extension you see. It should be called "Go". This gives language support so that you can start coding and get highlighting to help you find out when something is wrong.
Now you are all set up!
Starting with the Webpage
Create the Webpage
We will be building a web app in which you can search for restaurants near you with the Yelp API!
Start by creating a file: index.html
. This will be the template for your main page. Let's add the boilerplate code for the webpage:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Golang Restaurant Finder</title>
</head>
<body>
<h1>Find a Restaurant Here</h1>
</body>
</html>
Serve the Webpage
Now let's get this file on a server. A server is basically somewhere on the internet that files can reside, allowing them to communicate with the internet. When you open an html page in your browser without a server, it cannot actually communicate with other computers / servers.
Create a main.go
file. We must create a template out of the index.html
file that Go can display.
package main
import (
"fmt"
"html/template"
"net/http"
)
var tpl = template.Must(template.ParseFiles("index.html")) // creates the index.html page
func indexHandler(w http.ResponseWriter, r *http.Request) {
tpl.Execute(w, nil) // display template
}
func main() {
fmt.Println("App Started")
mux := http.NewServeMux() // helps to call the correct handler based on the URL
mux.HandleFunc("/", indexHandler) // what function to call on main page
http.ListenAndServe(":3000", mux) // start a local server to run files
}
Let's break this down.
The general way that Go works (in this context) is similar to Flask or Django, if you are familiar with it. Based on the url, Go will execute different functions.
First off, to go over some of the basics, variables in Go have the type (int
, bool
, etc.) after the name. As you may notice in the indexHandler
parameters (arguments), w
is the name and http.ResponseWriter
is the type. Additionally, variables can be defined in different ways:
var a int = 10
This creates a variable called a
with a type of int
.
var b = 10
This creates a variable called b
with a type of int
b
is defined in the same way that tpl
is defined.
c := 10
This creates a variable called c
with a type of int
.
c
is defined in the same way that mux
is defined. This declaration is specific to Go. It is called the shorthand variable creator. It automatically assigns a type to c
.
var d int
d = 10
This creates a variable called d
with a type of int
.
As you can see, all three methods create the same variable.
Secondly, we must import some libraries from the standard library to help us with the project. We are mainly using the http library for this project. The fmt
library helps us print to the command line.
Now, to go over the code, we start func main()
with a print statement that shows up in the command line telling us that our App is running. Then, we use mux := http.NewServeMux()
to create what's called an HTTP request multiplexer . This is fancy talk for something that helps to handle which function to call based on the URL. Then we tell mux
to call indexHandler
on the main page. Finally we start our local server so that it can "serve" the files.
In indexHandler
we execute and display the template.
At the very top of our file we see the definition of tpl
. This variable creates the template that displays content on the page. the template.Must()
is there so that if the template is not able to render, the app crashes and errors. We want this to happen because without this template we have nothing to show the user. The template.ParseFiles()
takes index.html
and interprets it.
Now run
go run main.go
In your command line where your files are (or press F5
in VSCode) and go to localhost:3000 in your browser. You should see something like this!
Congrats! Your code is being hosted and displayed on a local server!
Styles
Now let's get some styles going. If we want our styles to show up, we need a styles.css
file to also be on our local server.
Create a folder called assets
. Inside this folder, create a file called styles.css
.
Copy and paste these styles into that file. I thought these styles looked fairly nice, but there is tons of room for improvement. Feel free to change things up as you go. These are all the styles needed for the entire project.
.restaurantCard {
max-width: 250px;
text-align: left;
margin: auto;
padding: 0;
border: 1px solid #999;
margin-bottom: 20px;
}
a {
color: black;
text-decoration: none;
cursor: pointer;
}
input {
margin-bottom: 20px;
outline: none;
border: 1px solid #999;
padding: 5px 10px;
}
img {
max-width: 250px;
padding: 0;
margin: 0;
}
h3, p {
margin-left: 10px;
}
body {
text-align: center;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
Now, let's get a file server running. This will allow us to make our styles accessible from our index.html
file. Change your main.go
file to this:
package main
import (
"fmt"
"html/template"
"net/http"
)
var tpl = template.Must(template.ParseFiles("index.html")) // creates the index.html page
func indexHandler(w http.ResponseWriter, r *http.Request) {
tpl.Execute(w, nil) // create template
}
func main() {
fmt.Println("App Started")
mux := http.NewServeMux() // helps to call the correct handler based on the URL
fs := http.FileServer(http.Dir("assets")) // put the "static" files on the server like the CSS file
mux.Handle("/assets/", http.StripPrefix("/assets/", fs))
mux.HandleFunc("/", indexHandler) // what function to call on main page
http.ListenAndServe(":3000", mux) // start a local server to run files
}
Now we have created fs
, an http.FileServer
, which is something that puts everything in our assets folder (i.e. styles.css
) onto our local server. Reminder -- a local server is something that can understand URLs and communicate with the internet.
Awesome now do
go run main.go
In your command line where your files are (or press F5
in VSCode) and go to localhost:3000 in your browser. You should see something like this:
The text is now centered and the font should have changed! If this doesn't seem like as big of a change as expected, know that many of the styles will help make other elements look better.
If you ever encounter trouble with the styles updating, it is likely that the browser has "cached" the previous version of the file. To fix this, go to localhost:3000/assets and click on styles.css
. You should see your styles file. Refresh the page and the new styles should update. Return to localhost:3000 and you're good to go.
Create the form to Search
Now let's create the form on our webpage so that we can search for the restaurants we want. Change your index.html
file to this:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Golang Restaurant Finder</title>
<link rel="stylesheet" href="assets/styles.css" />
</head>
<body>
<h1>Find a Restaurant Here</h1>
<form action="/search" method="GET">
<input
autofocus
id="search"
type="search"
name="q"
placeholder="Enter a search term..."
/>
<input
id="location"
type="search"
name="location"
placeholder="Enter your city..."
/>
<input type="submit" value="Submit" />
</form>
</body>
</html>
Save and run your code (as we did before with go run main.go
). What happens when you submit something into the form?
The URL changed! It became locahost:8000/search?q=query&location=city
where query
and city
are replaced with whatever you typed into the box. Notice where in the attributes of the form you typed q
and location
.
We also have put a placeholder
for each input box so that users know what to type into each box. They are also both type
search
boxes.
What's happening is that this form submits a GET
request to our own local server by changing the URL to have the parameters (q
and location
) that were inputted. This way, Golang will be able to see when the URL has changed and respond accordingly.
If you're wondering what the difference between GET and POST requests are, check out this link.
Now let's figure out how to actually get data.
The Yelp API in Golang
Creating a Yelp API Account
Create a Yelp API account here and click Sign up.
Once you have created an account, go here to "Create a New App".
Fill in the App Name, pick an Industry (randomly) and fill out the contact email and put a small description. The information in this form is irrelevant to the API's function.
Next you should get a page like this:
Your API key is basically the password to access the resources of the API and the Client ID is a way to identify your App.
You will need the API Key soon.
The SearchHandler
The way this will all come together is by creating a function that runs when the form on the page is filled out. Luckily our mux
Http multiplexer makes it easy. We can call a function when our url has /search
in it.
Try to have mux
call searchHandler when the URL has /search
in it. Think about how you did it for indexHandler
.
func main() {
fmt.Println("App Started")
mux := http.NewServeMux() // helps to call the correct handler based on the URL
fs := http.FileServer(http.Dir("assets")) // put the "static" files on the server
mux.Handle("/assets/", http.StripPrefix("/assets/", fs))
mux.HandleFunc("/", indexHandler) // what function to call on main page
mux.HandleFunc("/search", searchHandler) // function to call on search
http.ListenAndServe(":3000", mux) // start a localserver to run files
}
Here we are adding one line to call searchHandler
when the url contains /search
. You might be wondering, where's searchHandler
. Let's make it
Start with this code for searchHandler
:
func searchHandler(w http.ResponseWriter, r *http.Request) {
u, err := url.Parse(r.URL.String()) // gets the string from the URL and splits it up
if err != nil { // check for errors
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Internal server error"))
return
}
params := u.Query() // get the parts at the end of the url
searchKey := params.Get("q") // get the search Query
location := params.Get("location") // get the inputted location
searchKey = strings.ReplaceAll(searchKey, " ", "") // take out spaces
location = strings.ReplaceAll(location, " ", "")
fmt.Println("Search Query is: ", searchKey)
fmt.Println("Location is: ", location)
}
We first start by "parsing" the URL string (analyzing and separating).
A request is created when we submit the form (and the URL changes). So, if there is an error, we should still give back a response. That is why we must call WriteHeader
, to give it a response with an http.StatusInternalServerError
.
Next, we get the parameters (params
) from the end of our url, finding the query we typed in and the location we typed in. Then we get rid of all the spaces in our submission because the Yelp API doesn't like spaces. Finally, we print it out.
When you run it, what happens?
indexHandler
you had to Execute
a template for something to show up. Similarly, you must do the same in searchHandler
for anything to show.
However, let's first get some data!
Get the Data
Here is how we are going to get the data:
client := &http.Client{}
req, err := http.NewRequest("GET", "https://api.yelp.com/v3/businesses/search?location="+location+"&term="+searchKey, nil) // create request for yelp api
if err != nil {
fmt.Println(err)
}
req.Header.Set("Authorization", "Bearer <Put API Key Here>") // set authorization token
response, err := client.Do(req) // do the request
if err != nil {
fmt.Println(err) // print the error if there is one
}
fmt.Println(response)
Put this code right under location = strings.ReplaceAll(location, " ", "")
.
We first create an http.Client
which allows us to make requests to the API. Then we start formatting the request. We are making what's called a GET
request to this API because we are getting data from it without sending our own data. Using the documentation of the Yelp API, we can see that location
and term
are the two url parameters we need to add to our url that we are requesting from. We put in the location and searchKey that we grabbed from the URL earlier.
We also need to authorize our request by sending our 'password', also known as the API Key, to the server. We put this in the Header
of our request.
Then we Do
the request and check for errors.
What happens?
You should have a bunch of gibberish show up in your terminal / command line once you submit something to the form on your page. Believe it or not, that is exactly what we want. However, we need to parse this response to get what we want out of it.
So, let's parse it!
Parsing the Received Data
If you go to the Yelp Documentation and scroll down, you will see an example Response Body. That is what the response looks like once parsed and formatted. We want to take this and have it get parsed in Golang. Therefore, we are going to put this data into a Go struct.
Copy the example response data and put it into JSON-To-Go so that it generates a struct that will parse the received data. If you get an error, you're going to have to remove the // ...
and the comma before it near the bottom of the copied response data. Like so:
{
"total": 8228,
"businesses": [
{
"rating": 4,
"price": "$",
"phone": "+14152520800",
"id": "E8RJkjfdcwgtyoPMjQ_Olg",
"alias": "four-barrel-coffee-san-francisco",
"is_closed": false,
"categories": [
{
"alias": "coffee",
"title": "Coffee & Tea"
}
],
"review_count": 1738,
"name": "Four Barrel Coffee",
"url": "https://www.yelp.com/biz/four-barrel-coffee-san-francisco",
"coordinates": {
"latitude": 37.7670169511878,
"longitude": -122.42184275
},
"image_url": "http://s3-media2.fl.yelpcdn.com/bphoto/MmgtASP3l_t4tPCL1iAsCg/o.jpg",
"location": {
"city": "San Francisco",
"country": "US",
"address2": "",
"address3": "",
"state": "CA",
"address1": "375 Valencia St",
"zip_code": "94103"
},
"distance": 1604.23,
"transactions": ["pickup", "delivery"]
}
],
"region": {
"center": {
"latitude": 37.767413217936834,
"longitude": -122.42820739746094
}
}
}
Awesome you should get something like this:
type AutoGenerated struct {
Total int `json:"total"`
Businesses []struct {
Rating int `json:"rating"`
Price string `json:"price"`
Phone string `json:"phone"`
ID string `json:"id"`
Alias string `json:"alias"`
IsClosed bool `json:"is_closed"`
Categories []struct {
Alias string `json:"alias"`
Title string `json:"title"`
} `json:"categories"`
ReviewCount int `json:"review_count"`
Name string `json:"name"`
URL string `json:"url"`
Coordinates struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
} `json:"coordinates"`
ImageURL string `json:"image_url"`
Location struct {
City string `json:"city"`
Country string `json:"country"`
Address2 string `json:"address2"`
Address3 string `json:"address3"`
State string `json:"state"`
Address1 string `json:"address1"`
ZipCode string `json:"zip_code"`
} `json:"location"`
Distance float64 `json:"distance"`
Transactions []string `json:"transactions"`
} `json:"businesses"`
Region struct {
Center struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
} `json:"center"`
} `json:"region"`
}
But, this looks like a mess, so let's simplify a little bit.
type Business struct {
Rating int `json:"rating"`
Price string `json:"price"`
Phone string `json:"phone"`
ID string `json:"id"`
Alias string `json:"alias"`
IsClosed bool `json:"is_closed"`
Categories []struct {
Alias string `json:"alias"`
Title string `json:"title"`
} `json:"categories"`
ReviewCount int `json:"review_count"`
Name string `json:"name"`
URL string `json:"url"`
Coordinates struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
} `json:"coordinates"`
ImageURL string `json:"image_url"`
Location struct {
City string `json:"city"`
Country string `json:"country"`
Address2 string `json:"address2"`
Address3 string `json:"address3"`
State string `json:"state"`
Address1 string `json:"address1"`
ZipCode string `json:"zip_code"`
} `json:"location"`
Distance float64 `json:"distance"`
Transactions []string `json:"transactions"`
}
type Results struct {
Total int `json:"total"`
Businesses []Business `json:"businesses"`
}
Put this at the very bottom of your main.go
. We will use the Results
struct to store all the data (which contains multiple Business
structs).
Let's put the data into this struct!
At the top of main.go
, right after the import statement, put this
var results Results
Then, searchHandler
should look like this:
func searchHandler(w http.ResponseWriter, r *http.Request) {
u, err := url.Parse(r.URL.String()) // gets the string from the URL and splits it up
if err != nil { // check for errors
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Internal server error"))
return
}
params := u.Query() // get the parts at the end of the url
searchKey := params.Get("q") // get the search Query
location := params.Get("location") // get the inputted location
searchKey = strings.ReplaceAll(searchKey, " ", "") // take out spaces
location = strings.ReplaceAll(location, " ", "")
client := &http.Client{}
req, err := http.NewRequest("GET", "https://api.yelp.com/v3/businesses/search?location="+location+"&term="+searchKey, nil) // create request for yelp api
if err != nil {
fmt.Println(err)
}
req.Header.Set("Authorization", "Bearer <API Key Goes Here>") // set authorization token
response, err := client.Do(req) // do the request
if err != nil {
fmt.Println(err) // print the error if there is one
}
err = json.NewDecoder(response.Body).Decode(&results) // parse the response
fmt.Println(results.Businesses) // print the businesses
fmt.Println("Search Query is: ", searchKey)
fmt.Println("Location is: ", location)
}
The only two lines added are the NewDecoder
and the print statement right after! Congrats on making it here!
Now we take the Body
of the response we get from our request and Decode
it to put it into our results
struct that we defined at the top. If you are wondering about the &
, check out this link. The Decode
function takes a pointer to a struct as a parameter.
Then we print out the businesses in results
. The .
operator is how we can reference things inside structs and nested structs.
What happens?
When you submit the form, all of the businesses get printed to the terminal (doesn't look to pretty, but its certainly in the terminal).
Awesome! We are getting close!
Displaying the data
We are able to make use of the templates in our Go file to display a Go struct in html!
index.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Golang Restaurant Finder</title>
<link rel="stylesheet" href="/assets/styles.css" />
</head>
<body>
<h1>Find a Restaurant Here</h1>
<form action="/search" method="GET">
<input
autofocus
id="search"
type="search"
name="q"
placeholder="Enter a search term..."
/>
<input
autofocus
id="location"
type="search"
name="location"
placeholder="Enter your city..."
/>
<input type="submit" value="Submit" />
</form>
{{ range .Businesses }}
<div class="restaurantCard">
<img src="{{.ImageURL}}" />
<a href="{{ .URL }}" target="_blank">
<h3>{{ .Name }}</h3>
<p>Review Count: {{ .ReviewCount }}</p>
<p>Price: {{ .Price }}</p>
</a>
</div>
{{ end }}
</body>
</html>
Between the {{
and the }}
, we are able to put Golang.
At the end of searchHandler
put this line:
err = tpl.Execute(w, results) // run the template, passing in the results from API
Everything works! But why?
At the end of searchHandler
in main.go
we tell Go to execute our template and pass to it our struct that contains all the businesses.
Inside index.html
, whatever we passed to it (results
) becomes the .
. So when we do {{ range .Businesses }}
, we can loop through every Business
in results.Businesses
.
Now, on each loop, the .
becomes the Business
that is currently being looped through. So, we grab the different information from each business to make a card for the business and it displays on the screen!
Congratulations!
How to take this further?
- See what other information you can put in the card to display!
- Play around with the styles and share it in the discord
- See if you can include another parameter to the yelp API to filter further i.e. "categories"
- Try to implement autocomplete in the search boxes using the Yelp autocomplete endpoint
Share what you've made!
Comments (0)