Welcome to the First Part of the Tutorial on Build Complete Full Stack Web App Using Go Programming Language
What Will I Learn?
- You will learn how to develop full stack web application using Go
- You will learn how to create HTTP server and making routes
- You will learn about static files and their creation
- Your will learn about the REST API handlers and testing
Requirements
System Requirements:
- Go Programming Language for building App
- The Basic Command Line
OS Support:
- Windows 7/8/10
- macOS
- Linux
Difficulty
- Intermediate
Required Understanding
- You just need beginner level understanding of the Go programming language.
- A fair understanding of Programming Languages
- A thirst for learning and developing something new
Description
- Go gets increasingly more well-known as the go-to language to develop web based applications.
- All you need is a little bit of understanding of web design. If you are a web developer and want to build Full Stack Apps, this tutorial is for you.With this particular tutorial, I really hope to get the center ground and provide a single source that explains steps to make a full stack web app in Go, together with adequate test instances.
What is Go Programming Language ?
Go is an open source programming language which makes it simple to develop easy, dependable, and effective software.
Go is surely an incredible option for a language as it was developed by a few of the exact same individuals who created the C programming language, Unix, and UTF-8 - probably the most important efforts to computer science. Robert Griesemer, Rob Pike, and Ken Thompson developed Go to become a modern language which easily uses several cores, easily implements concurrency, very easily works in distributed environments, and simply enables the developer to write applications - it has a really lean and user friendly format.
What is Full Stack ?
We will develop a community encyclopedia of animals. This website will :
-Display the various records submitted through the community, using the name and information on the animal they will located.
-Permit anyone to post a fresh entry about a animal which they found.
This application will require three components :
- The web server
- The front-end app
- The database
Setting up the environment
This section explains how you can set up your own environment and project structure the first time
1. Set up your $GOPATH
Run this command to check the current value of your $GOPATH
environment variable :
echo $GOPATH
If you do not get a directory name, include the GOPATH
variable to your environment (you are able to choose any directory location you would like, however it will be much better if you create a brand new directory for this) :
export GOPATH="/location/of/your/gopath/directory"
You can paste the above line in you .bashrc
or .zshrc
file, in case you wish to make the variable to be permanent.
2. Set up directory structure
Hereafter, the actual “Go directory” will certainly make reference to the place described from your $GOPATH
environment variable. Within the Go directory, you will need to create 3 folders (if they happen to be not there previously) :
# Inside the Go directory
mkdir src
mkdir pkg
mkdir bin
The purpose of each directory can be seen from its name:
• bin - is actually wherever all of the executable binaries produced by compiling your code go
• pkg - Consists of package objects created by libraries (that you do not have to think about now)
• src - is actually wherever all of your Go source code will go. Yes, the whole thing. Even which weird side project that you will be thinking of building.
3. Making your project directory
The project folders within the src directory must stick to that exact same location structure since the place wherever your remote repository is situated. So, for instance, if I make a new project known as “animalpedia”, and i also make a repository for that below my name on github, in a way that the location of the project repository will be on github.com/exploringworld/animalpedia
, then the place of this project in the computer could be :
$GOPATH/src/github.com/exploringworld/animalpedia
Go on and create a comparable directory for your project. In case you have not created an online repo yet, simply name the directories based on the location which you intend to put your own code in.
This particular location on your pc will henceforth become known as your project directory
Getting HTTP server
Within your project directory, make a file known as main.go
inside your project directory :
touch main.go
This particular file will contain the program code to start your server :
// Anything within our this package name can see everything
package main
// Below are the names of libraries we going to use
// Both "fmt" and "net" are part of the Go Language standard library
import (
// "fmt" has methods for formatted I/O operations "fmt"
// The "net/http" library contains methods to implement HTTP clients and servers
"net/http"
)
func main() {
// The "HandleFunc" method takes a path and a function as arguments
// (Yes, we can pass functions as arguments, and even treat them like variables in Go Programming language)
// BUT, the handler function has to must have the appropriate signature
http.HandleFunc("/", handler)
// After we define our server, we now "listen and serve" on port 8080
// The second argument is the handler, which we will come to later on, but for now it is left as nil,
// and the handler defined above (in "HandleFunc") is used
http.ListenAndServe(":8080", nil)
}
// "handler" is our handler function. It has to follow the function signature of a ResponseWriter and Request type
// as the arguments.
func handler(w http.ResponseWriter, r *http.Request) {
// For this, we would always pipe the "Hello World" into the response writer
fmt.Fprintf(w, "Hello World!")
}
fmt.Fprintf,
in contrast to another printf
statements you might know, requires a “writer” as its 1st argument. The 2nd argument will be the data which is piped in to this writer. The outcome therefore seems based on where the writer goes it. In our situation the ResponseWriter
w writes the output as the response to users request.
`
You now run this file :
go run main.go
And navigate to http://localhost:8080 in your browser, or by running the command through CMD.
curl localhost:8080
And see the code output: “Hello World!”
You have now successfully started an HTTP server in Go Language.
Making the routes
Our own server is currently operating, but, you may observe that we get exactly the same “Hello World!”response whatever the route we strike, or the HTTP method we utilize. To find out this on your own, run the following curl commands, and take notice of the response that this server will give you :
curl localhost:8080/some-other-route
curl -X POST localhost:8080
curl -X PUT localhost:8080/samething
All three commands are still going to give you “Hello World!”
We would like to provide our server a bit more intelligence compared to this, to ensure that we are able to manage a number of paths and methods. This is where routing is necessary.
Even though you are able to accomplish this with Go’s net/http
standard library, there are many libraries available that offer a more idiomatic as well as declarative method to handle http routing.
Installing external libraries into Go
We are setting up a few external libraries through the following tutorial, in which the standard libraries do not give the features that we desire.
Whenever we install libraries, we want a way to make sure that others who work with our code also provide exactly the same version of the library that we perform.
To do this, we make use of a “package manager” tool. This specific tool acts several purposes:
• It ensures the versions associated with any external libraries we all install tend to be locked down, to ensure that breaking changes in some of the libraries usually do not affect our program code.
• It brings the necessary external libraries and stores them locally, in order that various projects may use diverse versions of the same library, when they need to.
• It stores the names and versions of most our external libraries, so others may set up the same versions which we are working along with on this system.
The official package manager for Go (to be more exact “official experiment” which is “safe for production use” as explained on the homepage) is known as dep. You are able to install dep by following the actual setup guide https://github.com/golang/dep
. You can actually confirm its installation after running :
dep version
which will output a few information on the particular version when successful.
In order to load package management for the project, run the command :
dep init
This can produce theGopkg.toml
and Gopkg.lock
files, which are the files which are useful to record and lock dependencies within our project.
Next, we set up our routing library:
dep ensure -add github.com/gorilla/mux
This will add the gorilla/mux
library to your project.
Right now, we are able to change the code to work with features this library provides :
package main
import (
// Import the gorilla/mux library we just installed
"fmt"
"net/http"
"github.com/gorilla/mux"
)
func main() {
// Declare a new router
r := mux.NewRouter()
// This is where the router is useful, it allows us to declare methods that
// this path will be valid for
r.HandleFunc("/hello", handler).Methods("GET")
// We can then pass our router (after declaring all our routes) to this method
// (where previously, we were leaving the secodn argument as nil)
http.ListenAndServe(":8080", r)
}
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World!")
}
Testing Our Application
Testing is definitely an important section of building any application “production quality”. It makes sure that our app functions like we anticipate that it will.
Lets begin by testing our handler. Create a file named main_test.go:
//main_test.go
package main
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestHandler(t *testing.T) {
//Here, we form a new HTTP request. This is the request that's going to be
// passed to our handler.
// The first argument is the method, the second argument is the route (which
//we leave blank for now, and will get back to soon), and the third is the
//request body, which we don't have in this case.
req, err := http.NewRequest("GET", "", nil)
// In case there is an error in forming the request, we fail and stop the test
if err != nil {
t.Fatal(err)
}
// We use Go's httptest library to create an http recorder. This recorder
// will act as the target of our http request
// (you can think of it as a mini-browser, which will accept the result of
// the http request that we make)
recorder := httptest.NewRecorder()
// Create an HTTP handler from our handler function. "handler" is the handler
// function defined in our main.go file that we want to test
hf := http.HandlerFunc(handler)
// Serve the HTTP request to our recorder. This is the line that actually
// executes our the handler that we want to test
hf.ServeHTTP(recorder, req)
// Check the status code is what we expect.
if status := recorder.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
// Check the response body is what we expect.
expected := `Hello World!`
actual := recorder.Body.String()
if actual != expected {
t.Errorf("handler returned unexpected body: got %v want %v", actual, expected)
}
}
Go utilizes a convention that will determines the test file when it has got the pattern *_test.go
To perform this test, simply run :
go test ./...
from the project main directory. You need to see a mild message suggesting that every thing went ok.
Making our routing to test
If you see in our earlier snippet, we kept the “route” empty while creating the mock request applying http.newRequest
. So how exactly does this particular test continue to pass if your handler will be defined just for “GET /handler”
route?
Well, ends up this test was just testing our handler and not the actual routing to the handler. In other words, which means that the above mentioned test makes sure that the request arriving will get functioned properly provided that it is sent to the right handler.
In this portion, we are going to test this routing, to ensure that we are able to make sure that every handler will be mapped on the correct HTTP route.
Prior to we continue to test our routing, it is essential to ensure that the code can be analyzed for this. Change the main.go file to appear like this:
package main
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
)
// The new router function creates the router and
// returns it to us. We can now use this function
// to instantiate and test the router outside of the main function
func newRouter() *mux.Router {
r := mux.NewRouter()
r.HandleFunc("/hello", handler).Methods("GET")
return r
}
func main() {
// The router is now formed by calling the `newRouter` constructor function
// that we defined above. The rest of the code stays the same
r := newRouter()
http.ListenAndServe(":8080", r)
}
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World!")
}
Now our route constructor function is separated, let us see test our routing:
func TestRouter(t *testing.T) {
// Instantiate the router using the constructor function that
// we defined previously
r := newRouter()
// Create a new server using the "httptest" libraries `NewServer` method
// Documentation : https://golang.org/pkg/net/http/httptest/#NewServer
mockServer := httptest.NewServer(r)
// The mock server we created runs a server and exposes its location in the
// URL attribute
// We make a GET request to the "hello" route we defined in the router
resp, err := http.Get(mockServer.URL + "/hello")
// Handle any unexpected error
if err != nil {
t.Fatal(err)
}
// We want our status to be 200 (ok)
if resp.StatusCode != http.StatusOK {
t.Errorf("Status should be ok, got %d", resp.StatusCode)
}
// In the next few lines, the response body is read, and converted to a string
defer resp.Body.Close()
// read the body into a bunch of bytes (b)
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
// convert the bytes to a string
respString := string(b)
expected := "Hello World!"
// We want our response to match the one defined in our handler.
// If it does happen to be "Hello world!", then it confirms, that the
// route is correct
if respString != expected {
t.Errorf("Response should be %s, got %s", expected, respString)
}
}
Now we all know that each time we hit the actual GET /hello route
, we have a response of hello world. When we hit some other route, it should respond having a 404. Actually, let us write a test for exactly this condition :
func TestRouterForNonExistentRoute(t *testing.T) {
r := newRouter()
mockServer := httptest.NewServer(r)
// Most of the code is similar. The only difference is that now we make a
//request to a route we know we didn't define, like the `POST /hello` route.
resp, err := http.Post(mockServer.URL+"/hello", "", nil)
if err != nil {
t.Fatal(err)
}
// We want our status to be 405 (method not allowed)
if resp.StatusCode != http.StatusMethodNotAllowed {
t.Errorf("Status should be 405, got %d", resp.StatusCode)
}
// The code to test the body is also mostly the same, except this time, we
// expect an empty body
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
respString := string(b)
expected := ""
if respString != expected {
t.Errorf("Response should be %s, got %s", expected, respString)
}
}
Since we have figured out how you can make a simple http server, we are able to serve static files from this for the users to interact with.
Serving the Assets files
“Static files” are the HTML, CSS, JavaScript, images, and also the additional static asset files which are required to form a web site.
You will find 3 actions we must take in so that it will create our server.Now serve these types oo static assets.
- Make static assets
- Change our router in order to assist static assets
- Include tests to confirm our new server can easily serve static files
Create static assets
To create static assets, make a directory in your project root directory, and name it assets
:
mkdir assets
Following, create an HTML file within this directory. This is actually the file we will serve, together with any file which goes in the assets directory :
touch assets/index.html
Modify the router
Interestingly sufficient, the whole file server could be enabled in only adding Three lines of code within the router. The newest router constructor may be like this :
func newRouter() *mux.Router {
r := mux.NewRouter()
r.HandleFunc("/hello", handler).Methods("GET")
// Declare the static file directory and point it to the
// directory we just made
staticFileDirectory := http.Dir("./assets/")
// Declare the handler, that routes requests to their respective filename.
// The fileserver is wrapped in the `stripPrefix` method, because we want to
// remove the "/assets/" prefix when looking for files.
// For example, if we type "/assets/index.html" in our browser, the file server
// will look for only "index.html" inside the directory declared above.
// If we did not strip the prefix, the file server would look for
// "./assets/assets/index.html", and yield an error
staticFileHandler := http.StripPrefix("/assets/", http.FileServer(staticFileDirectory))
// The "PathPrefix" method acts as a matcher, and matches all routes starting
// with "/assets/", instead of the absolute route itself
r.PathPrefix("/assets/").Handler(staticFileHandler).Methods("GET")
return r
}
Testing of static file server
It's hard to really say that you will have finished a feature unless you possess tests for it. We can easily check the static file server by having another test function to main_test.go :
func TestStaticFileServer(t *testing.T) {
r := newRouter()
mockServer := httptest.NewServer(r)
// We want to hit the `GET /assets/` route to get the index.html file response
resp, err := http.Get(mockServer.URL + "/assets/")
if err != nil {
t.Fatal(err)
}
// We want our status to be 200 (ok)
if resp.StatusCode != http.StatusOK {
t.Errorf("Status should be 200, got %d", resp.StatusCode)
}
// It isn't wise to test the entire content of the HTML file.
// Instead, we test that the content-type header is "text/html; charset=utf-8"
// so that we know that an html file has been served
contentType := resp.Header.Get("Content-Type")
expectedContentType := "text/html; charset=utf-8"
if expectedContentType != contentType {
t.Errorf("Wrong content type, expected %s, got %s", expectedContentType, contentType)
}
}
To actually test your work, run the server :
go run main.go
then, navigate to http://localhost:8080/assets/ in your web browser.
Making of web browser application
Because we have to make our animal encyclopedia, lets make a simple HTML file which shows record of animals, as well as brings the list from an API on-page load, plus offers a form to up-date checklist of animals:
<!DOCTYPE html>
<html lang="en">
<head>
<title>The animal encyclopedia</title>
</head>
<body>
<h1>The animal encyclopedia</h1>
(html comment removed:
This section of the document specifies the table that will
be used to display the list of animals and their description
)
<table>
<tr>
<th>Species</th>
<th>Description</th>
</tr>
<td>Lion</td>
<td>Common between cities</td>
</tr>
</table>
<br/>
(html comment removed:
This section contains the form, that will be used to hit the
`POST /animal` API that we will build in the next section
)
<form action="/animal" method="post">
Species:
<input type="text" name="species">
<br/> Description:
<input type="text" name="description">
<br/>
<input type="submit" value="Submit">
</form>
(html comment removed:
Finally, the last section is the script that will
run on each page load to fetch the list of animals
and add them to our existing table
)
<script>
animalTable = document.querySelector("table")
/*
Use the browsers `fetch` API to make a GET call to /animal
We expect the response to be a JSON list of animals, of the
form :
[
{"species":"...","description":"..."},
{"species":"...","description":"..."}
]
*/
fetch("/animal")
.then(response => response.json())
.then(animalList => {
//Once we fetch the list, we iterate over it
animalList.forEach(animal => {
// Create the table row
row = document.createElement("tr")
// Create the table data elements for the species and
// description columns
species = document.createElement("td")
species.innerHTML = animal.species
description = document.createElement("td")
description.innerHTML = animal.description
// Add the data elements to the row
row.appendChild(species)
row.appendChild(description)
// Finally, add the row element to the table itself
animalTable.appendChild(row)
})
})
</script>
</body>
Addition of animal REST API handlers
We are able to see, we will have to apply 2 APIs to ensure that this app to function:
- GET /animal - which will retrieve record of all animals presently within the system
- POST /animal - which will create an entrance to the current set of animals
In this, we are going to write the related handlers.
Create a new file named animal_handlers.go, next to the main.go file.
Initially, we are going to include the definition of the Animal struct as well as initialize a common animal variable:
type Animal struct {
Species string `json:"species"`
Description string `json:"description"`
}
var animals []Animal
Next, define the handler to get all animals :
func getAnimalHandler(w http.ResponseWriter, r *http.Request) {
//Convert the "animals" variable to json
animalListBytes, err := json.Marshal(animals)
// If there is an error, print it to the console, and return a server
// error response to the user
if err != nil {
fmt.Println(fmt.Errorf("Error: %v", err))
w.WriteHeader(http.StatusInternalServerError)
return
}
// If all goes well, write the JSON list of animals to the response
w.Write(animalListBytes)
}
Next, the handler to create a new entry of animals :
func createAnimalHandler(w http.ResponseWriter, r *http.Request) {
// Create a new instance of Animal
animal := Animal{}
// We send all our data as HTML form data
// the `ParseForm` method of the request, parses the
// form values
err := r.ParseForm()
// In case of any error, we respond with an error to the user
if err != nil {
fmt.Println(fmt.Errorf("Error: %v", err))
w.WriteHeader(http.StatusInternalServerError)
return
}
// Get the information about the animal from the form info
animal.Species = r.Form.Get("species")
animal.Description = r.Form.Get("description")
// Append our existing list of animals with a new entry
animals = append(animals, animal)
//Finally, we redirect the user to the original HTMl page
// (located at `/assets/`), using the http libraries `Redirect` method
http.Redirect(w, r, "/assets/", http.StatusFound)
}
The final stage, is to include these handler to our router, to be able to allow them to use by the app :
// These lines are added inside the newRouter() function before returning r
r.HandleFunc("/animal", getAnimalHandler).Methods("GET")
r.HandleFunc("/animal", createAnimalHandler).Methods("POST")
return r
The tests regarding the two handlers and also the routing included are similar to the last tests we had written for the GET /hello handler and static file server, and they are still left as an exercise for the readers.
Summary
Up to now, we now have included persistence to the application, using the information about various animals getting saved and retrieved.
Nevertheless, that persistence is temporary, because it is in memory space. Which means that at any time you reboot your application, all of the information will get removed. To be able to bring correct persistence, we are going to must add a database to the stack.
So far, the code had been simple to reason about and test, as it was obviously a standalone application. Including a database will certainly add an additional layer of communication.
We will be going to create a database and connected to our application in the next part of the tutorial
Posted on Utopian.io - Rewarding Open Source Contributors
Your contribution cannot be reviewed because it does not follow the Utopian Rules, and is considered as plagiarism. Plagiarism is not allowed on Utopian, and posts that engage in plagiarism will be flagged and hidden forever.
Need help? Write a ticket on https://support.utopian.io.
Chat with us on Discord.
[utopian-moderator]