Are
you a MEAN Stack developer who knows some Go and is curious about
trying out web development in the language? If so, you've come to
the right place! In this tutorial, you'll learn the basics of Go's
http
package and Gorilla's mux
package from Express.js.
As
the title suggests, this tutorial assumes you know Express. This
tutorial also assumes you know the basics of Go. If you don't I
recommend going to tour.golang.org for an interactive tutorial. I
found Go really easy to learn from C++, so if you know C++ or
JavaScript, it'll be a quick read.
In
this tutorial we're going to make a simple Go/Gorilla web server and
you will be able to find the final product on this GitHub repository. If you're interested in a more flashy example, my last
two tutorials, which you can find here and here will teach you
how to make this app, Slothful Soda, with the GGAP Stack (Go,
Gorilla, Angular.js, PostgreSQL). Those tutorials do throw more concepts at you
at once, though, so if you're just starting Go web development I'd
recommend reading this tutorial first.
To
get started, create a directory in the src
directory of your GOPATH
called gorilla-tutorial.
In this tutorial I will have Node.js examples alongside the Go
examples, so if you want to follow along in Node as well as Go, add a
file to the directory called server.js.
A
hello world example
In
Go, HTTP functionality is handled with the standard net/http
package. Handling HTTP requests in Go looks very similar to handling
HTTP requests on an Express server. To demonstrate that similarity,
here's a hello world example in Express. If you're following along
in Node, run npm
install express
and then put this code in server.js.
var express = require('express'); var app = express(); var serveHelloWorld = function(req, res){ //1 res.send('Hello world!'); } app.use('/', serveHelloWorld); //2 app.listen(1123);
1.
We have an Express middleware function serveHelloWorld
that takes in a request object and a response object and sends the
message “Hello world!”
2.
We register serveHelloWorld
with the catch-all '/'
route, so serveHelloWorld
handles all requests to our server.
Pretty
simple, now let's see how we would make this in Go. In
gorilla-tutorial
make a server.go
file with this code:
package main import ( "fmt" "net/http" ) func serveHelloWorld(w http.ResponseWriter, r *http.Request){ //1 fmt.Fprintf(w, "Hello world!") } func main(){ http.HandleFunc("/", serveHelloWorld) //2 http.ListenAndServe(":1123", nil) //3 }
Here's
what's going on:
1.
Functions handling HTTP requests in Go all take in a ResponseWriter
and a Request
pointer. Like with serveHelloWorld
in the Express version, we are writing “Hello world!” as our
response.
2.
We register serveHelloWorld
to the catch-all "/"
route with HandleFunc.
3.
We have our server listen on port 1123 with http.ListenAndServe.
Now
run go
install
and then run gorilla-tutorial
and on localhost:1123
you should get:
So
far the Go code looks like the Express version. Before we move on
I'd like to quickly point out how in Go routing we needed to
explicitly say that serveHelloWorld
takes in a ResponseWriter
and a Request
pointer. Because of that we know at compile time what types go into
all calls to serveHelloWorld
and any other functions handling HTTP requests. This type safety in
Go really comes in handy for preventing a lot of annoying
hard-to-detect type errors.
Let's
add some more routes
Here's
a couple more Express routes. If you're following along in Express,
add these routes to server.js
before the catch-all '/'
route:
app.use(/\/sloths$/, function(req, res){ //1 res.send('Sloths rule!'); }); app.use('/images', function(req, res){ //2 res.send('You are in the images directory'); });
1.
Requests to localhost:1123/sloths
get the response “Sloths rule!” (the path uses a regular
expression to
only match the exact URL /sloths).
2.
Requests to any path that starts with /images/
get the response “You are in the images directory”.
For
the Go implementation add these routes in main:
http.HandleFunc("/sloths", func(w http.ResponseWriter, r *http.Request){ //1 fmt.Fprintf(w, "Sloths rule!") }) http.HandleFunc("/images/", func(w http.ResponseWriter, r *http.Request){//2 fmt.Fprintf(w, "You are in the images directory") })
1.
In Go routing if
a path doesn't end with a slash, it only matches that exact URL. So
“/sloths”
only matches requests to localhost:1123/sloths.
2.
A
path ending with a slash in Go matches all URLs with that path plus
anything after the slash, so to match URLs that start with /images/,
we use the path “/images/”.
The
rule about slash and no-slash paths explains why “/”
is Go's catch-all path; “/” matches any path starting with /.
All paths start with / so “/”
matches all requests to the server.
Run
go
install
and gorilla-tutorial
and on localhost:1123/sloths
you will get:
Since
we have that images directory route, let's make it actually serve
images.
Statically
serving images
In
Express, static files like images can be served with express.static.
To make our image server, first make a directory called public
and in it make a directory called images
and download these pictures into the images
directory as sloth.jpg
and lemur.jpg.
If
you're following along in Express, in server.js
replace the images route with:
app.use('/images',
express.static(__dirname+'/public/images'));
This
puts express.static
in charge of requests to /images, serving images in public/images.
Here's
the equivalent in Go:
//1 //3 http.Handle("/images/", http.StripPrefix("/images/", http.FileServer(http.Dir("public/images")))) //2
1.
For our images route, notice we're using Handle
instead of HandleFunc.
While HandleFunc
takes in a function to handle requests, Handle
takes in an object called a Handler
to handle the requests.
2.
We're using the FileServer
function to make our Handler,
which serves files in public/images.
3.
The Handler created by FileServer
is almost what we want. FileServer's
Handler
serves images in the right directory but it looks for them using the
request's whole
URL.
So for a request to /images/sloth.jpg,
the Handler would look for the image in
public/images/images/sloth.jpg,
not public/images/sloth.jpg.
To
fix this, StripPrefix
takes our request URL, removes the /images/
prefix, and then
lets the FileServer's
Handler serve our images. So if we request /images/sloth.jpg,
the path our Handler
gets is now just sloth.jpg
so the Handler
looks in the right file path.
Here's
a diagram of how that works:
Also
note that FileServer
isn't just for images. You can also use it for other files like HTML,
CSS, and JavaScript files if you add calls to Handle
with FileServers
for the directories you keep those files in.
Introducing
Gorilla
We've
done some pretty straightforward routing so far. But there are more
complex routes out there to serve. Go's standard HTTP package
doesn't have rules for paths with routing parameters or regular
expressions. For that, Gorilla's got us covered, so let's move our
routing logic to a Gorilla router.
First,
go
get github.com/gorilla/mux.
Now
make a new file called router.go
and give it this code:
package main import( "net/http" "fmt" "github.com/gorilla/mux" ) func initRouter() *mux.Router{ r := mux.NewRouter() //1 r.HandleFunc("/sloths", func(w http.ResponseWriter, r *http.Request){//2 fmt.Fprintf(w, "Sloths rule!") }) r.PathPrefix("/images/").Handler(http.StripPrefix("/images/", //3 http.FileServer( http.Dir("public/images")))) r.PathPrefix("/").HandlerFunc(serveHelloWorld) //4 return r }
Here's
what's going on:
1.
We create a Gorilla Router
with mux.NewRouter()
2.
Like with the standard http package, we add routes to a Gorilla
router with Handle
and HandleFunc.
3.
Unlike in Go http, in Gorilla a basic path (with no parameters) only
matches that exact URL even if the path ends with a /. So to serve
our images, instead of doing r.Handle(“/images/”
…
we pass “/images/” into r.PathPrefix
to handle requests whose URLs start with /images/.
4.
We use PathPrefix
again with the “/”
path to make a catch-all route for serveHelloWorld
To
use our new router, replace the main
function in server.go
with
func main(){ router := initRouter() http.Handle("/", router) http.ListenAndServe(":1123", nil) }
Since "/"
in Go http is a catch-all route, so http.Handle("/",
router)
tells our server to have the Gorilla router handle our requests.
Note
that we are able to pass a Gorilla mux Router
into http.Handle.
Why is that? Remember how Handle
takes in a Handler?
Well Gorilla Routers
are a type of Handler
so they can be used in http.Handle.
URL
parameters and regular expressions
Now
that we've started making our Gorilla router, let's add some routes
with parameters. But before we do that, if you're following along in
Express, run npm
install html-escape
and in server.js
add the line
var
htmlEscape = require('html-escape');
We
need to do this because we are working with user input that we're
putting into our HTML, so we need a way to sanitize our user input to
prevent cross-site scripting vulnerabilities.
Now
for the Express version add these routes before the catch-all “/”
route:
app.use('/tea/:flavor', function(req, res){ var tea = req.params.flavor + ' tea'; //1 var html = '<img src="/images/sloth.jpg" /><br />'+ '<h2>I could use some ' + htmlEscape(tea) + '!</h2>'; res.send(html); //2 }); app.use(/\/(coffee)+/, function(req, res){ //3 var html = '<img src="/images/lemur.jpg" /><br />'+ '<h2>Lemurs = sloths that had too much coffee!</h2>'; res.send(html); });
1.
In this first route :flavor
is a route parameter which matches anything after /tea/.
We can get and use whatever is passed into that parameter with
req.params.
2.
After we get the text in :flavor
we insert it into some HTML (with
htmlEscape)
and serve it. So localhost:1123/tea/hibiscus
serves a picture of a sloth saying “I could use some hibiscus tea!”
3.
For our regular expression route, we are using the regular expression
/\/(coffee)+/,
which matches
any
route that says “coffee” one or more times to serve a picture of
a lemur saying “Lemurs = sloths that had too much coffee!”.
Now
let's add our tea route in Gorilla. For the HTML escaping Go has an
html package built into the standard library so to get it just add
"html"
to the list of packages in the import
statement in router.go.
For
our tea route in router.go
add this function before the catch-all "/"
route in initRouter:
r.HandleFunc("/tea/{flavor}", func(w http.ResponseWriter, r *http.Request){//1 params := mux.Vars(r) //2 tea := html.EscapeString(params["flavor"]) + " tea" html := `<body><img src="/images/sloth.jpg" /><br />` + //3 `<h2>I could use some ` + tea + `!</h2></body>` fmt.Fprintf(w, html) })
1.
We give our path a flavor
parameter with curly braces.
2.
We get a request's route parameters for by passing the Request
into the mux.Vars
function, and escape
the route parameters with html.EscapeString.
3.
Like in the Express version we then put our data into some HTML to
serve. Note, by the way, that I made the HTML string with backtick `
quotes; in Go there are no single-quoted strings so if you don't want
to escape double quotes in a string, you can put a string between
backticks instead of double quotes.
Now
if you run go
install
and then run gorilla-tutorial
and go to localhost:1123/tea/hibiscus
you should get:
Here's our regular expression route:
r.HandleFunc(`/{drink:(coffee)+}`, func(w http.ResponseWriter, r *http.Request){ html := `<body><img src="/images/lemur.jpg" /><br />`+ `<h2>Lemurs = sloths that had too much coffee!</h2></body>` fmt.Fprintf(w, html) })
The
syntax in Gorilla mux
for a regular expression route parameter is to have the parameter's
name (drink), then a colon, then the regular expression. Now if you
run go
install
and then run gorilla-tutorial
and go to localhost:1123/coffeecoffeecoffeecoffee
you should get:
Handling
POST requests in Go
So
far we've only been working with GET requests, so to finish off this
tutorial let's try some POST requests. We are going to make a simple
HTML form for ordering at a coffee shop.
For
Express, we will need a way to parse POST data so for that we'll use
body-parser.
If you're following along in Express, run npm
install body-parser
and at the beginning of server.js
add the line
var
bodyParser = require('body-parser');
And
then add these Express routes before the catch-all route:
app.get('/coffee-shop', function(req, res){ //1 res.send('<form action="/order" method="POST">'+ 'Your name <input type="text" name="name" /><br/ >'+ 'Your beverage order <input type="text" name="beverage" /><br/ >'+ '<input type="submit" value="Submit"/>'+ '</form>'); }); app.post('/order', bodyParser.urlencoded({extended:true}), function(req, res){ //2 var name = htmlEscape(req.body.name), beverage = htmlEscape(req.body.beverage); res.send('<h1>One '+ beverage + ' coming right up ' + name + '!</h1>'); });
1.
The server handles GET requests to /coffee-shop
by serving a form asking for your name and beverage order. The form
sends its data to /order.
2.
The server handles POST requests to /order
by parsing the form's data with body-parser,
converting the data to HTML-escaped
strings,
and then serving the message “One (beverage) coming right up,
(name)!”
Note
that instead of app.use,
we use app.get
for /coffee-shop
and app.post
for /order,
so the /coffee-shop
route only handles GET requests and /order
only handles POST requests.
For
the Gorilla implementation, include “log”
in the list of packages you import in router.go
and add this code before the catch-all route:
r.HandleFunc("/coffee-shop", func(w http.ResponseWriter, r *http.Request){ html := `<body><form action="order" method="POST">`+ `Your name <input type="text" name="name"><br />`+ `Your beverage order <input type="text" name="beverage"><br />`+ `<input type="submit" value="Submit">`+ `</form></body>` fmt.Fprintf(w, html) }).Methods("GET") //1 r.HandleFunc("/order", func(w http.ResponseWriter, r *http.Request){ err := r.ParseForm() //2 if err != nil { log.Fatal(err.Error()) } name := html.EscapeString(r.Form.Get("name")) //3 beverage := html.EscapeString(r.Form.Get("beverage")) html := `<body><h1>One ` + beverage + ` coming right up, ` + name + `!</h1></body>` fmt.Fprintf(w, html) }).Methods("POST") //4
Overall
the code is similar to the Express version. Here's what's going on:
1.
We serve the form in /coffee-shop
with r.HandleFunc.
To make /coffee-shop
only handle GET requests, after the Router.HandleFunc()
call we add .Methods("GET").
Methods
is a function in Gorilla for specifying which HTTP methods a route
handles.
2.
Request.ParseForm()
in Go's standard http package parses the form data and puts it in an
object in the Request
called Form.
3.
We then convert the Form
data to HTML-escaped
strings
and serve the data in our HTML message.
4.
To make /order
only handle POST requests, we use .Methods("POST").
So
if in localhost:1123/coffee-shop
you fill in the form with the name “&y” and the beverage
“dirty chai”
You
should get:
But
if you try to go directly to localhost:1123/order
in your browser, the /order
route will not handle your GET request, so your request will go to
the “/”
catch-all route.
In
this tutorial we covered how to serve basic routes in Go's standard
http
package and Gorilla mux,
using FileServer,
serving URL parameters and regular expressions in Gorilla, and
working with POST requests in Gorilla. For the next tutorial, we
will be taking a closer look at how Go http Handlers
work and use that to make Express-like middleware. Stay slothful!
Image credits:
- The author of the picture of the sloth in the examples is Stefan Laube and the picture is public domain.
- The author of the picture of the lemur in the examples is Alex Dunkel and the picture is licensed under CC BY 3.0.
- The author of the picture of the sloth at the end of the tutorial is Roy Luck and the picture is licensed under CC BY 2.0.