Working with session data
In this chapter let’s put the session functionality to work and use it to persist the confirmation flash message between HTTP requests that we discussed earlier.
We’ll begin in our cmd/web/handlers.go
file and update our snippetCreatePost
method so that a flash message is added to the user’s session data if — and only if — the snippet was created successfully. Like so:
package main ... func (app *application) snippetCreatePost(w http.ResponseWriter, r *http.Request) { var form snippetCreateForm err := app.decodePostForm(r, &form) if err != nil { app.clientError(w, http.StatusBadRequest) return } form.CheckField(validator.NotBlank(form.Title), "title", "This field cannot be blank") form.CheckField(validator.MaxChars(form.Title, 100), "title", "This field cannot be more than 100 characters long") form.CheckField(validator.NotBlank(form.Content), "content", "This field cannot be blank") form.CheckField(validator.PermittedValue(form.Expires, 1, 7, 365), "expires", "This field must equal 1, 7 or 365") if !form.Valid() { data := app.newTemplateData(r) data.Form = form app.render(w, r, http.StatusUnprocessableEntity, "create.tmpl", data) return } id, err := app.snippets.Insert(form.Title, form.Content, form.Expires) if err != nil { app.serverError(w, r, err) return } // Use the Put() method to add a string value ("Snippet successfully // created!") and the corresponding key ("flash") to the session data. app.sessionManager.Put(r.Context(), "flash", "Snippet successfully created!") http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther) }
That’s nice and simple, but there are a couple of things to point out:
The first parameter that we pass to
app.sessionManager.Put()
is the current request context. We’ll talk properly about what the request context is and how to use it later in the book, but for now you can just think of it as somewhere that the session manager temporarily stores information while your handlers are dealing with the request.The second parameter (in our case the string
"flash"
) is the key for the specific message that we are adding to the session data. We’ll subsequently retrieve the message from the session data using this key too.If there’s no existing session for the current user (or their session has expired) then a new, empty, session for them will automatically be created by the session middleware.
Next up we want our snippetView
handler to retrieve the flash message (if one exists in the session for the current user) and pass it to the HTML template for subsequent display.
Because we want to display the flash message once only, we actually want to retrieve and remove the message from the session data. We can do both these operations at the same time by using the PopString()
method.
I’ll demonstrate:
package main ... func (app *application) snippetView(w http.ResponseWriter, r *http.Request) { id, err := strconv.Atoi(r.PathValue("id")) if err != nil || id < 1 { http.NotFound(w, r) return } snippet, err := app.snippets.Get(id) if err != nil { if errors.Is(err, models.ErrNoRecord) { http.NotFound(w, r) } else { app.serverError(w, r, err) } return } // Use the PopString() method to retrieve the value for the "flash" key. // PopString() also deletes the key and value from the session data, so it // acts like a one-time fetch. If there is no matching key in the session // data this will return the empty string. flash := app.sessionManager.PopString(r.Context(), "flash") data := app.newTemplateData(r) data.Snippet = snippet // Pass the flash message to the template. data.Flash = flash app.render(w, r, http.StatusOK, "view.tmpl", data) } ...
If you try to run the application now, the compiler will (rightly) grumble that the Flash
field isn’t defined in our templateData
struct. Go ahead and add it in like so:
package main import ( "html/template" "path/filepath" "time" "snippetbox.alexedwards.net/internal/models" ) type templateData struct { CurrentYear int Snippet models.Snippet Snippets []models.Snippet Form any Flash string // Add a Flash field to the templateData struct. } ...
And now, we can update our base.tmpl
file to display the flash message, if one exists.
{{define "base"}} <!doctype html> <html lang='en'> <head> <meta charset='utf-8'> <title>{{template "title" .}} - Snippetbox</title> <link rel='stylesheet' href='/static/css/main.css'> <link rel='shortcut icon' href='/static/img/favicon.ico' type='image/x-icon'> <link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Ubuntu+Mono:400,700'> </head> <body> <header> <h1><a href='/'>Snippetbox</a></h1> </header> {{template "nav" .}} <main> <!-- Display the flash message if one exists --> {{with .Flash}} <div class='flash'>{{.}}</div> {{end}} {{template "main" .}} </main> <footer> Powered by <a href='https://golang.org/'>Go</a> in {{.CurrentYear}} </footer> <script src='/static/js/main.js' type='text/javascript'></script> </body> </html> {{end}}
Remember, the {{with .Flash}}
block will only be executed if the value of .Flash
is not the empty string. So, if there’s no "flash"
key in the current user’s session, the result is that the chunk of new markup simply won’t be displayed.
Once that’s done, save all your files and restart the application. Try adding a new snippet like so…

And after redirection you should see the flash message now being displayed:

If you try refreshing the page, you can confirm that the flash message is no longer shown — it was a one-off message for the current user immediately after they created the snippet.

Auto-displaying flash messages
A little improvement we can make (which will save us some work later in the project) is to automate the display of flash messages, so that any message is automatically included the next time any page is rendered.
We can do this by adding any flash message to the template data via the newTemplateData()
helper method that we made earlier, like so:
package main ... func (app *application) newTemplateData(r *http.Request) templateData { return templateData{ CurrentYear: time.Now().Year(), // Add the flash message to the template data, if one exists. Flash: app.sessionManager.PopString(r.Context(), "flash"), } } ...
Making that change means that we no longer need to check for the flash message within the snippetView
handler, and the code can be reverted to look like this:
package main ... func (app *application) snippetView(w http.ResponseWriter, r *http.Request) { id, err := strconv.Atoi(r.PathValue("id")) if err != nil || id < 1 { http.NotFound(w, r) return } snippet, err := app.snippets.Get(id) if err != nil { if errors.Is(err, models.ErrNoRecord) { http.NotFound(w, r) } else { app.serverError(w, r, err) } return } data := app.newTemplateData(r) data.Snippet = snippet app.render(w, r, http.StatusOK, "view.tmpl", data) } ...
Feel free to try running the application again and creating another snippet. You should find that the flash message functionality still works as expected.
Additional information
Behind the scenes of session management
I’d like to take a moment to unpack some of the ‘magic’ behind session management and explain how it works behind the scenes.
If you like, open up the developer tools in your web browser and take a look at the cookie data for one of the pages. You should see a cookie named session
in the request data, similar to this:

This is the session cookie, and it will be sent back to the Snippetbox application with every request that your browser makes.
The session cookie contains the session token — also sometimes known as the session ID. The session token is a high-entropy random string, which in my case is the value y9y1-mXyQUoAM6V5s9lXNjbZ_vXSGkO7jy-KL-di7A4
(yours will be different).
It’s important to emphasize that the session token is just a random string. In itself, it doesn’t carry or convey any session data (like the flash message that we set in this chapter).
Next, you might like to open up a terminal to MySQL and run a SELECT
query against the sessions
table to lookup the session token that you see in your browser. Like so:
mysql> SELECT * FROM sessions WHERE token = 'y9y1-mXyQUoAM6V5s9lXNjbZ_vXSGkO7jy-KL-di7A4'; +---------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------+ | token | data | expiry | +---------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------+ | y9y1-mXyQUoAM6V5s9lXNjbZ_vXSGkO7jy-KL-di7A4 | 0x26FF81030102FF820001020108446561646C696E6501FF8400010656616C75657301FF8600000010FF830501010454696D6501FF8400000027FF85040101176D61705B737472696E675D696E74657266616365207B7D01FF8600010C0110000016FF82010F010000000ED9F4496109B650EBFFFF010000 | 2024-03-18 11:29:23.179505 | +---------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------+ 1 row in set (0.00 sec)
This should return one record. The data
value here is the thing that actually contains the user’s session data. Specifically, what we’re looking at is a MySQL BLOB
(binary large object) containing a gob-encoded representation of the session data.
Each and every time we make a change to our session data, this data
value will be updated to reflect the changes.
Lastly, the final column in the database is the expiry
time, after which the session will no longer be considered valid.
So, what happens in our application is that the LoadAndSave()
middleware checks each incoming request for a session cookie. If a session cookie is present, it reads the session token from the cookie and retrieves the corresponding session data from the database (while also checking that the session hasn’t expired). It then adds the session data to the request context so it can be used in your handlers.
Any changes that you make to the session data in your handlers are updated in the request context, and then the LoadAndSave()
middleware updates the database with any changes to the session data before it returns.