After reading a neat article whose title I stole about making a guestbook app in Flask, I decided to see how it would compare to my favorite language of the year, Go. So here’s my take.
First Steps
Let’s create a new directory to hold the project. I’m gonna host the code on github so let’s make the local directory match the import path.
$ cd ~/Code/go/src
$ mkdir -p github.com/zeebo/gostbook
$ cd github.com/zeebo/gostbook/
$ git init
Initialized empty Git repository in /Users/zeebo/Code/go/src/github.com/zeebo/gostbook/.git/
Note that ~/Code/go is a directory in my GOPATH environment variable, the only
piece of configuration I need to do to have the build tool know how to fetch and
build any code that uses these conventions. Lets put in a little hello world code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
This registers a handler that will match any path and write
Hello World! in the response. Building and running this code
runs a server that listens on port 8080, so lets visit it.
$ go build
$ ./gostbook &
[1] 39629
$ curl localhost:8080
Hello World!
$ kill 39629
Neat!
Commit
Let’s do our source control duty, and make a commit with our super simple app.
$ cat .gitignore
*
!.gitignore
!*.go
!*.html
$ git status
# On branch master
#
# Initial commit
#
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
# .gitignore
# main.go
nothing added to commit but untracked files present (use "git add" to track)
$ git add .
$ git commit -m 'initial commit'
[master (root-commit) de0b184] initial commit
2 files changed, 21 insertions(+)
create mode 100644 .gitignore
create mode 100644 main.go
Templates
The next step is to put templates in. Lets make a template directory and some basic templates in there. I’ll steal the templates from Eevee’s post and change them to use the built in html/template package from the standard library. Here’s the source:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
1 2 3 4 5 6 7 8 9 10 11 | |
Updating the Go code is a little more work, but not much.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
Building and running again, we see it’s working:
$ go build
$ ./gostbook &
[1] 39918
$ curl localhost:8080
<!DOCTYPE html>
<html lang="en">
<head>
<title>Guestbook</title>
</head>
<body>
<section id="content">
<h1>Guestbook</h1>
<p>Hello, and welcome to my guestbook, because it's 1997!</p>
<ul class="guests">
<li>...</li>
</ul>
</section>
<footer id="footer">
My Cool Guestbook 2000 © me forever
</footer>
</body>
</html>
$ kill 39918
Let’s be diligent and make another commit. On to data!
Databases
Go has many database bindings but the one I find easiest to work with would be MongoDB with the excellent mgo driver. Let’s create our data model.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
We just create a struct with some fields. The mgo driver uses runtime
reflection to look up the information about the struct for setting and reading
the values. For the ID field add some tags to it to instruct bson to omit it if
the value is empty, and name it _id when serializing, to have MongoDB pick the
id for us on insertion, and name it what it’s expecting. We also provide a
NewEntry function for creating an Entry at the current time.
Now lets add support to the handler.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | |
Interacting with the databse requires a little boilerplate in the handler,
but this can easily be removed by clever use of Go’s interfaces. The net/http
package will serve anything with a ServeHTTP(ResponseWriter, *Request) method,
so you can decorate handlers by wrapping them in simple types that implement
that interface. Doing that is left as an exercise to the reader :)
Here’s how we change the template:
1 2 3 4 5 6 7 8 | |
Notice we don’t worry about any kind of injection. The html/template package is super awesome and handles that by knowing what it’s outputing and the context in which the data is being used. If you’re in an html context, it will escape the html properly. If you’re in a script or url context, it knows and will apply the appropriate esacping. No modifying the data in the database. No “sanitizing”. Just doing the right thing, every time.
Signing it
Time to add the handler to sign the guest book. Let’s start with the html for the form.
1 2 3 4 5 6 7 | |
And now the handler:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | |
All we need to do is add a single line to main.go to make it handle the new handler:
http.HandleFunc("/sign", sign)
And we can sign, and view our guestbook. Lets commit again.
Some issues
Now the astute reader will notice a couple little pain points.
We had to check in the
signhandler if the method wasPOST. This can be fixed by using a more sophisticated muxer than the built in one in net/http. Like all good packages in Go, all of these things are just interfaces and so you can swap them out with many community driven packages. An exellent one is the gorilla muxer at code.google.com/p/gorilla/mux.We had to hard code the urls. Once again, this is solved by using a more sophisticated muxer. code.google.com/p/gorilla/mux supports building urls from names you give to the routes.
Boilerplate in the handlers to specify a database/collection every time. I typically solve this how I wrote earlier by making a type that implements the
ServeHTTPmethod and passes in a request context containing everything I need to use for that request, including sessions and database connections. It’s only a couple lines of code to make, but outside the scope of this post.
Other than that, I found it to be pretty painless and about as easy to do as the Flask version. Considering this is a statically typed compiled language, that’s quite the feat.
Deployment
It wouldn’t be useful if it wasn’t deployed. Fortunately, Go compiles down into a static binary. This can be shipped to any system that it was compiled for, and just ran. Go also allows you to easily cross compile for any system, so thats a non-issue as well. The built in web server is comparable in performance to things like Apache and nginx from my tests. So for most cases, it’s as simple as running a binary and either proxy passing it through from your front end server, or just letting the world hit it directly.
But, since that’s not cool enough, we’re also going to deploy on Heroku.
Buildpacks and a Note About Getting Code
Unfortunately, Go isn’t a supported platform on Heroku. Fortunately, it’s just a buildpack away. The Cedar stack is excellent and allows you to run any binary you want to host your web site, so we just have to tell Heroku how to build our code. I’m a little biased so I’m going to use the buildpack I modified to do this, although there are alternatives.
The cool part about hosting our code on github is that anyone with Go installed can just grab it with a single command:
go get github.com/zeebo/gostbook
That will download, compile, and install a binary named “gostbook” in our bin directory in our GOPATH. The buildpack I created uses this to build the code we’ll be deploying. First we make a little file that describes how to do it, and a Procfile to describe what to run:
1 2 | |
1
| |
Then we have to be nice and listen on the port Heroku tells us to. This is a one line change:
if err = http.ListenAndServe(":"+os.Getenv("PORT"), nil); err != nil {
Lastly, we have to dail out to the mongo config they ask too:
session, err = mgo.Dial(os.Getenv("DATABASE_URL"))
I use DATABASE_URL as the key. We’ll have to set it later in the deployment.
Let’s commit that.
Deployment (again)
Lets create the heroku app.
$ heroku create --stack cedar --buildpack http://github.com/zeebo/buildpack.git
Creating tranquil-refuge-9104... done, stack is cedar
http://tranquil-refuge-9104.herokuapp.com/ | git@heroku.com:tranquil-refuge-9104.git
Git remote heroku added
Add in a free mongo database and configure the DATABASE_URL:
$ heroku addons:add mongolab:starter
-----> Adding mongolab:starter to tranquil-refuge-9104... done, v3 (free)
Welcome to MongoLab.
$ heroku config
BUILDPACK_URL => http://github.com/zeebo/buildpack.git
MONGOLAB_URI => ...snip...
$ heroku config:add DATABASE_URL=...snip...
Adding config vars and restarting app... done, v4
DATABASE_URL => ...snip...
If I was smarter, I would have just used MONGOLAB_URI in the code, but I’m not
so here we are. Finally, we can just push it up and watch the magic:
$ git push heroku master
Counting objects: 24, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (21/21), done.
Writing objects: 100% (24/24), 3.41 KiB, done.
Total 24 (delta 4), reused 0 (delta 0)
-----> Heroku receiving push
-----> Fetching custom buildpack... done
-----> Go app detected
-----> Configuration
GO_VERSION=go1.0.2
BASE=github.com/zeebo/gostbook
+ github.com/zeebo/gostbook
-----> Using Go go1.0.2.linux-amd64
-----> Fetching Go go1.0.2.linux-amd64
-----> Checking for Mercurial and Bazaar
Fetching hg and bzr
..snip...
Successfully installed mercurial
...snip...
Successfully installed bzr
Cleaning up...
-----> Running go get -u -v all
-----> Copying sources into GOPATH/src/github.com/zeebo/gostbook
-----> Running go get -v github.com/zeebo/gostbook
Fetching https://labix.org/v2/mgo?go-get=1
Parsing meta tags from https://labix.org/v2/mgo?go-get=1 (status code 200)
get "labix.org/v2/mgo": found meta tag main.metaImport{Prefix:"labix.org/v2/mgo", VCS:"bzr", RepoRoot:"https://launchpad.net/mgo/v2"} at https://labix.org/v2/mgo?go-get=1
labix.org/v2/mgo (download)
Fetching https://labix.org/v2/mgo/bson?go-get=1
Parsing meta tags from https://labix.org/v2/mgo/bson?go-get=1 (status code 200)
get "labix.org/v2/mgo/bson": found meta tag main.metaImport{Prefix:"labix.org/v2/mgo", VCS:"bzr", RepoRoot:"https://launchpad.net/mgo/v2"} at https://labix.org/v2/mgo/bson?go-get=1
get "labix.org/v2/mgo/bson": verifying non-authoritative meta tag
Fetching https://labix.org/v2/mgo?go-get=1
Parsing meta tags from https://labix.org/v2/mgo?go-get=1 (status code 200)
labix.org/v2/mgo/bson
labix.org/v2/mgo
github.com/zeebo/gostbook
-----> Discovering process types
Procfile declares types -> web
-----> Compiled slug size is 1.4MB
-----> Launching... done, v6
http://tranquil-refuge-9104.herokuapp.com deployed to Heroku
To git@heroku.com:tranquil-refuge-9104.git
* [new branch] master -> master
And we have a nice guestbook at http://tranquil-refuge-9104.herokuapp.com
A snag
It seems like the database name is specified by the host in this case. We can’t just go and create whatever database we want. So we have to update the code to grab this information and use it when we’re making queries. The patch to fix it was pretty easy. Just add a global variable and parse the URL to put the database into it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | |
We just rely on the net/url package to parse the url and grab the database out of the path argument. Since the path contains the leading forward slash, we just slice that off. All thats left is a redeploy:
$ git add .
$ git commit -m 'fixes for database'
[master 2b4bf78] fixes for database
2 files changed, 11 insertions(+), 3 deletions(-)
$ git push heroku master
Counting objects: 7, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (4/4), 493 bytes, done.
Total 4 (delta 3), reused 0 (delta 0)
-----> Heroku receiving push
-----> Fetching custom buildpack... done
-----> Go app detected
-----> Configuration
GO_VERSION=go1.0.2
BASE=github.com/zeebo/gostbook
+ github.com/zeebo/gostbook
-----> Using Go go1.0.2.linux-amd64
-----> Checking for Mercurial and Bazaar
/app/tmp/repo.git/.cache/venv/bin/hg
/app/tmp/repo.git/.cache/venv/bin/bzr
-----> Running go get -u -v all
-----> Copying sources into GOPATH/src/github.com/zeebo/gostbook
-----> Running go get -v github.com/zeebo/gostbook
github.com/zeebo/gostbook
-----> Discovering process types
Procfile declares types -> web
-----> Compiled slug size is 1.4MB
-----> Launching... done, v7
http://tranquil-refuge-9104.herokuapp.com deployed to Heroku
To git@heroku.com:tranquil-refuge-9104.git
52a2171..2b4bf78 master -> master
And to my surprise, it worked on the second try!
Closing remarks
I hope this post showed some of what can be done with Go. In little time and code I was able to construct that awesome 1997 guestbook. This just scratched the surface of the cool stuff going on in the Go ecosystem. There’s code competion, sublime text integration, hosted automatically generated documentation, and continuous integration. The Go tool is awesome and able to build the vast majority of Go code that lives anywhere with one command. I highly recommend looking into Go for your next project.