Go is getting more and more popular as the go-to language to build web applications.

This is in no small part due to its speed and application performance, as well as its portability. There are many resources on the internet to teach you how to build end to end web applications in Go, but for the most part they are either scattered in the form of isolated blog posts, or get into too much detail in the form of books.

With this tutorial, I hope to find the middle ground and provide a single resource which describes how to make a full stack web application in Go, along with sufficient test cases.

The only prerequisite for this tutorial is a beginner level understanding of the Go programming language.

We are going to build a community encyclopedia of birds. This website will :

Display the different entries submitted by the community, with the name and details of the bird they found.

Allow anyone to post a new entry about a bird that they saw.

This application will require three components :

The web server The front-end (client side) app The database

Setting up your environment

This section describes how to set up your environment and project structure for the first time. If you have built another project in go, or know the standard directory structure, you can skip this section and go to the next one

1. Set up your $GOPATH

Run this command to check the current value of your $GOPATH environment variable :

echo $GOPATH

If you do not see a directory name, add the GOPATH variable to your environment (you can select any directory location you want, but it would be better if you create a 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 permanent.

2. Set up your directory structure

Hereafter, the “Go directory” will refer to the location described by your $GOPATH environment variable. Inside the Go directory, you will have to create 3 folders (if they are not there already) :

# Inside the Go directory mkdir src mkdir pkg mkdir bin

The purpose of each directory can be seen from its name:

bin - is where all the executable binaries created by compiling your code go

- is where all the executable binaries created by compiling your code go pkg - Contains package objects made by libraries (which you don’t have to worry about now)

- Contains package objects made by libraries (which you don’t have to worry about now) src - is where all your Go source code goes. Yes, all of it. Even that weird side project that you are thinking of making.

3. Creating your project directory

The project folders inside the src directory should follow that same location structure as the place where your remote repository lies. So, for example, if I want to make a new project called “birdpedia”, and I make a repository for that under my name on github, such that the location of my project repository would be on “github.com/sohamkamani/birdpedia”, then the location of this project on my computer would be : $GOPATH/src/github.com/sohamkamani/birdpedia

Go ahead and make a similar directory for your project. If you haven’t made an online repo yet, just name the directories according to the location that you plan to put your code in.

This location on your computer will henceforth be referred to as your “project directory”

Starting an HTTP server

Inside your project directory, create a file called main.go inside your project directory :

touch main.go

This file will contain the code to start your server :

package main import ( "fmt" "net/http" ) func main ( ) { http . HandleFunc ( "/" , handler ) http . ListenAndServe ( ":8080" , nil ) } func handler ( w http . ResponseWriter , r * http . Request ) { fmt . Fprintf ( w , "Hello World!" ) }

fmt.Fprintf , unlike the other “printf” statements you may know, takes a “writer” as its first argument. The second argument is the data that is piped into this writer. The output therefore appears according to where the writer moves it. In our case the ResponseWriter w writes the output as the response to the users request.

You can now run this file :

go run main.go

And navigate to http://localhost:8080 in your browser, or by running the command :

curl localhost:8080

And see the output: “Hello World!”

You have now successfully started an HTTP server in Go.

Making routes

Our server is now running, but, you might notice that we get the same “Hello World!” response regardless of the route we hit, or the HTTP method that we use. To see this yourself, run the following curl commands, and observe the response that the server gives you :

curl localhost:8080/some-other-route curl -X POST localhost:8080 curl -X PUT localhost:8080/samething

All three commands still give you “Hello World!”

We would like to give our server a little more intelligence than this, so that we can handle a variety of paths and methods. This is where routing comes into play.

Although you can achieve this with Go’s net/http standard library, there are other libraries out there that provide a more idiomatic and declarative way to handle http routing.

Installing external libraries

We will be installing a few external libraries through this tutorial, where the standard libraries don’t provide the features that we want.

When we install libraries, we need a way to ensure that other people who work on our code also have the same version of the library that we do.

In order to do this, we use a “package manager” tool. This tool serves a few purposes:

It makes sure the versions of any external libraries we install are locked down, so that breaking changes in any of the libraries do not affect our code.

It fetches the required external libraries and stores them locally, so that different projects can use different versions of the same library, if they need to.

It stores the names and versions of all our external libraries, so that others can install the same versions that we are working with on our system.

The official package manager for Go (or rather “official experiment” that is “safe for production use” as described on its homepage) is called dep . You can install dep by following the setup guide. You can verify its installation by running :

dep version

which should output some information on the version if successful.

To initialize package management for our project, run the command :

dep init

THis will create the Gopkg.toml and Gopkg.lock files, which are the files that are used to record and lock dependencies in our project.

Next, we install our routing library:

dep ensure -add github.com/gorilla/mux

This will add the gorilla/mux library to your project.

Now, we can modify our code to make use of the functionality that this library provides :

package main import ( "fmt" "net/http" "github.com/gorilla/mux" ) func main ( ) { r := mux . NewRouter ( ) r . HandleFunc ( "/hello" , handler ) . Methods ( "GET" ) http . ListenAndServe ( ":8080" , r ) } func handler ( w http . ResponseWriter , r * http . Request ) { fmt . Fprintf ( w , "Hello World!" ) }

Testing

Testing is an essential part of making any application “production quality”. It ensures that our application works the way that we expect it to.

Lets start by testing our handler. Create a file called main_test.go :

package main import ( "net/http" "net/http/httptest" "testing" ) func TestHandler ( t * testing . T ) { req , err := http . NewRequest ( "GET" , "" , nil ) if err != nil { t . Fatal ( err ) } recorder := httptest . NewRecorder ( ) hf := http . HandlerFunc ( handler ) hf . ServeHTTP ( recorder , req ) if status := recorder . Code ; status != http . StatusOK { t . Errorf ( "handler returned wrong status code: got %v want %v" , status , http . StatusOK ) } expected := `Hello World!` actual := recorder . Body . String ( ) if actual != expected { t . Errorf ( "handler returned unexpected body: got %v want %v" , actual , expected ) } }

Go uses a convention to ascertains a test file when it has the pattern *_test.go

To run this test, just run :

go test ./...

from your project root directory. You should see a mild message telling you that everything ran ok.

Making our routing testable

If you notice in our previous snippet, we left the “route” blank while creating our mock request using http.newRequest . How does this test still pass if the handler is defined only for “GET /handler” route?

Well, turns out that this test was only testing our handler and not the routing to our handler. In simpler terms, this means that the above test ensures that the request coming in will get served correctly provided that it’s delivered to the correct handler.

In this section, we will test this routing, so that we can be sure that each handler is mapped to the correct HTTP route.

Before we go on to test our routing, it’s necessary to make sure that our code can be tested for this. Modify the main.go file to look like this:

package main import ( "fmt" "net/http" "github.com/gorilla/mux" ) func newRouter ( ) * mux . Router { r := mux . NewRouter ( ) r . HandleFunc ( "/hello" , handler ) . Methods ( "GET" ) return r } func main ( ) { r := newRouter ( ) http . ListenAndServe ( ":8080" , r ) } func handler ( w http . ResponseWriter , r * http . Request ) { fmt . Fprintf ( w , "Hello World!" ) }

Once we’ve separated our route constructor function, let’s test our routing:

func TestRouter ( t * testing . T ) { r := newRouter ( ) mockServer := httptest . NewServer ( r ) resp , err := http . Get ( mockServer . URL + "/hello" ) if err != nil { t . Fatal ( err ) } if resp . StatusCode != http . StatusOK { t . Errorf ( "Status should be ok, got %d" , resp . StatusCode ) } defer resp . Body . Close ( ) b , err := ioutil . ReadAll ( resp . Body ) if err != nil { t . Fatal ( err ) } respString := string ( b ) expected := "Hello World!" if respString != expected { t . Errorf ( "Response should be %s, got %s" , expected , respString ) } }

Now we know that every time we hit the GET /hello route, we get a response of hello world. If we hit any other route, it should respond with a 404. In fact, let’s write a test for precisely this requirement :

func TestRouterForNonExistentRoute ( t * testing . T ) { r := newRouter ( ) mockServer := httptest . NewServer ( r ) resp , err := http . Post ( mockServer . URL + "/hello" , "" , nil ) if err != nil { t . Fatal ( err ) } if resp . StatusCode != http . StatusMethodNotAllowed { t . Errorf ( "Status should be 405, got %d" , resp . StatusCode ) } 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 ) } }

Now that we’ve learned how to create a simple http server, we can serve static files from it for our users to interact with.

Serving static files

“Static files” are the HTML, CSS, JavaScript, images, and the other static asset files that are needed to form a website.

There are 3 steps we need to take in order to make our server serve these static assets.

Create static assets Modify our router to serve static assets Add tests to verify that our new server can serve static files

Create static assets

To create static assets, create a directory in your project root directory, and name it assets :

mkdir assets

Next, create an HTML file inside this directory. This is the file we are going to serve, along with any other file that goes inside the assets directory :

touch assets/index.html

Modify the router

Interestingly enough, the entire file server can be enabled in just adding 3 lines of code in the router. The new router constructor will look like this :

func newRouter ( ) * mux . Router { r := mux . NewRouter ( ) r . HandleFunc ( "/hello" , handler ) . Methods ( "GET" ) staticFileDirectory := http . Dir ( "./assets/" ) staticFileHandler := http . StripPrefix ( "/assets/" , http . FileServer ( staticFileDirectory ) ) r . PathPrefix ( "/assets/" ) . Handler ( staticFileHandler ) . Methods ( "GET" ) return r }

Testing the static file server

You cannot truly say that you have completed a feature until you have tests for it. We can test the static file server by adding another test function to main_test.go :

func TestStaticFileServer ( t * testing . T ) { r := newRouter ( ) mockServer := httptest . NewServer ( r ) resp , err := http . Get ( mockServer . URL + "/assets/" ) if err != nil { t . Fatal ( err ) } if resp . StatusCode != http . StatusOK { t . Errorf ( "Status should be 200, got %d" , resp . StatusCode ) } 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

And navigate to http://localhost:8080/assets/ in your browser.

Making a simple browser app

Since we need to create our bird encyclopedia, lets create a simple HTML document that displays the list of birds, and fetches the list from an API on page load, and also provides a form to update the list of birds :

<!DOCTYPE html> < html lang = " en " > < head > < title > The bird encyclopedia </ title > </ head > < body > < h1 > The bird encyclopedia </ h1 > < table > < tr > < th > Species </ th > < th > Description </ th > </ tr > < td > Pigeon </ td > < td > Common in cities </ td > </ tr > </ table > < br /> < form action = " /bird " method = " post " > Species: < input type = " text " name = " species " > < br /> Description: < input type = " text " name = " description " > < br /> < input type = " submit " value = " Submit " > </ form > < script > birdTable = document . querySelector ( "table" ) fetch ( "/bird" ) . then ( response => response . json ( ) ) . then ( birdList => { birdList . forEach ( bird => { row = document . createElement ( "tr" ) species = document . createElement ( "td" ) species . innerHTML = bird . species description = document . createElement ( "td" ) description . innerHTML = bird . description row . appendChild ( species ) row . appendChild ( description ) birdTable . appendChild ( row ) } ) } ) </ script > </ body >

Adding the bird REST API handlers

As we can see, we will need to implement two APIs in order for this application to work:

GET /bird - that will fetch the list of all birds currently in the system POST /bird - that will add an entry to our existing list of birds

For this, we will write the corresponding handlers.

Create a new file called bird_handlers.go , adjacent to the main.go file.

First, we will add the definition of the Bird struct and initialize a common bird variable:

type Bird struct { Species string `json:"species"` Description string `json:"description"` } var birds [ ] Bird

Next, define the handler to get all birds :

func getBirdHandler ( w http . ResponseWriter , r * http . Request ) { birdListBytes , err := json . Marshal ( birds ) if err != nil { fmt . Println ( fmt . Errorf ( "Error: %v" , err ) ) w . WriteHeader ( http . StatusInternalServerError ) return } w . Write ( birdListBytes ) }

Next, the handler to create a new entry of birds :

func createBirdHandler ( w http . ResponseWriter , r * http . Request ) { bird := Bird { } err := r . ParseForm ( ) if err != nil { fmt . Println ( fmt . Errorf ( "Error: %v" , err ) ) w . WriteHeader ( http . StatusInternalServerError ) return } bird . Species = r . Form . Get ( "species" ) bird . Description = r . Form . Get ( "description" ) birds = append ( birds , bird ) http . Redirect ( w , r , "/assets/" , http . StatusFound ) }

The last step, is to add these handler to our router, in order to enable them to be used by our application :

r . HandleFunc ( "/bird" , getBirdHandler ) . Methods ( "GET" ) r . HandleFunc ( "/bird" , createBirdHandler ) . Methods ( "POST" ) return r

The tests for both these handlers and the routing involved are similar to the previous tests we wrote for the GET /hello handler and static file server, and are left as an exercise for the reader.

If you’re lazy, you can still see the tests in the source code

Adding a database

So far, we have added persistence to our application, with the information about different birds getting stored and retrieved.

However, this persistence is short lived, since it is in memory. This means that anytime you restart your application, all the data gets erased. In order to add true persistence, we will need to add a database to our stack.

Until now, our code was easy to reason about and test, since it was a standalone application. Adding a database will add another layer of communication.

You can read about how to integrate a postgres database into your Go application in my next post

_You can find the source code for this post [here](https://github.com/sohamkamani/blog_examplegowebapp)___