Not sure how to structure your Go web application?

My new book guides you through the start-to-finish build of a real world web application in Go — covering topics like how to structure your code, manage dependencies, create dynamic database-driven pages, and how to authenticate and authorize users securely.

Take a look!

Simple Flash Messages in Go

Last updated:

Often in web applications you need to temporarily store data in-between requests, such as an error or success message during the Post-Redirect-Get process for a form submission. Frameworks such as Rails and Django have the concept of transient single-use flash messages to help with this.

In this post I'm going to look at a way to create your own cookie-based flash messages in Go.

We'll start by creating a directory for the project, along with a flash.go file for our code and a main.go file for an example application.

$ mkdir flash-example
$ cd flash-example
$ touch flash.go main.go

In order to keep our request handlers nice and clean, we'll create our primary SetFlash() and GetFlash() helper functions in the flash.go file.

File: flash.go
package main

import (
	"encoding/base64"
	"net/http"
	"time"
)

func SetFlash(w http.ResponseWriter, name string, value []byte) {
	c := &http.Cookie{Name: name, Value: encode(value)}
	http.SetCookie(w, c)
}

func GetFlash(w http.ResponseWriter, r *http.Request, name string) ([]byte, error) {
	c, err := r.Cookie(name)
	if err != nil {
		switch err {
		case http.ErrNoCookie:
			return nil, nil
		default:
			return nil, err
		}
	}
	value, err := decode(c.Value)
	if err != nil {
		return nil, err
	}
	dc := &http.Cookie{Name: name, MaxAge: -1, Expires: time.Unix(1, 0)}
	http.SetCookie(w, dc)
	return value, nil
}

// -------------------------

func encode(src []byte) string {
	return base64.URLEncoding.EncodeToString(src)
}

func decode(src string) ([]byte, error) {
	return base64.URLEncoding.DecodeString(src)
}

Our SetFlash() function is pretty succinct.

It creates a new http.Cookie, containing the name of the flash message and the content. You'll notice that we're encoding the content – this is because RFC 6265 is quite strict about the characters cookie values can contain, and encoding to base64 ensures our value satisfies the permitted character set. We then use the http.SetCookie() function to write the cookie to the response.

In the GetFlash() helper we use the r.Cookie() method to load up the cookie containing the flash message – returning nil if it doesn't exist – and then decode the value from base64 back into a byte array.

Because we want a flash message to only be available once, we need to instruct clients to not resend the cookie with future requests. We can do this by setting a new cookie with exactly the same name, with MaxAge set to a negative number and Expiry set to a historical time (to cater for old versions of IE). You should note that Go will only set an expiry time on a cookie if it is after the Unix epoch, so we've set ours for 1 second after that.

Let's use these helper functions in a short example:

File: main.go
package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/set", set)
	http.HandleFunc("/get", get)
	fmt.Println("Listening...")
	http.ListenAndServe(":3000", nil)
}

func set(w http.ResponseWriter, r *http.Request) {
	fm := []byte("This is a flashed message!")
	SetFlash(w, "message", fm)
}

func get(w http.ResponseWriter, r *http.Request) {
	fm, err := GetFlash(w, r, "message")
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	if fm == nil {
		fmt.Fprint(w, "No flash messages")
		return
	}
	fmt.Fprintf(w, "%s", fm)
}

Run the application:

$ go run main.go flash.go
Listening...

And make some requests against it using curl:

$ curl -i --cookie-jar cj localhost:3000/set
HTTP/1.1 200 OK
Set-Cookie: message=VGhpcyBpcyBhIGZsYXNoZWQgbWVzc2FnZSE=
Content-Type: text/plain; charset=utf-8
Content-Length: 0

$ curl -i --cookie-jar cj --cookie cj localhost:3000/get
HTTP/1.1 200 OK
Set-Cookie: message=; Expires=Thu, 01 Jan 1970 00:00:01 UTC; Max-Age=0
Content-Type: text/plain; charset=utf-8
Content-Length: 26

This is a flashed message!

$ curl -i --cookie-jar cj --cookie cj localhost:3000/get
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 17

No flash messages

You can see our flash message being set, retrieved, and then not passed with subsequent requests as expected.

Additional Tools

If you don't want to roll your own helpers for flash messages then the gorilla/sessions package is a good option. This stores session data in a signed (and optionally encrypted) cookie, which stops the data from being tampered with. Here's the previous example implemented with this package instead:

File: main.go
package main

import (
	"fmt"
	"net/http"

	"github.com/gorilla/sessions"
)

func main() {
	http.HandleFunc("/set", set)
	http.HandleFunc("/get", get)
	fmt.Println("Listening...")
	http.ListenAndServe(":3000", nil)
}

var store = sessions.NewCookieStore([]byte("a-secret-string"))

func set(w http.ResponseWriter, r *http.Request) {
	session, err := store.Get(r, "flash-session")
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	session.AddFlash("This is a flashed message!", "message")
	session.Save(r, w)
}

func get(w http.ResponseWriter, r *http.Request) {
	session, err := store.Get(r, "flash-session")
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	fm := session.Flashes("message")
	if fm == nil {
		fmt.Fprint(w, "No flash messages")
		return
	}

	session.Save(r, w)
	fmt.Fprintf(w, "%v", fm[0])
}