When building web applications with standard HTML forms, user experience often hinges on two crucial details: preventing duplicate form submissions and providing clear, immediate feedback.
If you render a response directly from an HTML POST request, your users run into a common annoyance: refreshing the page triggers a scary browser warning asking to re-submit form data. Even worse, accidental re-submissions can duplicate database entries or charge a customer twice.
The solution is a classic web architecture pattern known as Post/Redirect/Get (PRG) paired with Flash Messages. In this guide, we'll explore how the PRG pattern works, why Flash messages are essential, and how to implement a clean, thread-safe Flash message system from scratch in idiomatic Go (Golang).
What is the Post/Redirect/Get (PRG) Pattern?
The Post/Redirect/Get (PRG) pattern is a web development design strategy that prevents duplicate form submissions and keeps your browser history clean.
Instead of rendering HTML directly in response to a POST request, the server returns an HTTP redirect response (usually 303 See Other or 302 Found). The browser then follows this redirect by sending a GET request to display the target page.
+---------+ +--------+
| Browser | | Server |
+---------+ +--------+
| |
|--- 1. POST /login ---->| (Processes data, creates Flash message)
| |
|<-- 2. 303 Redirect ----| (Redirects to /dashboard)
| |
|--- 3. GET /dashboard ->| (Reads & clears Flash message)
| |
|<-- 4. 200 OK (HTML) ---| (Renders page with feedback)
v v
Why PRG is Essential:
- Prevents Duplicate Submissions: Hitting
F5or Refresh only repeats the finalGETrequest, not the state-changingPOST. - Bookmarking & Sharing: Users can safely bookmark or share the resulting page URL without re-triggering server logic.
- Clean Browser History: The back button behaves predictably without warning prompts.
The Missing Link: What is a Flash Message?
While the PRG pattern solves the double-submission issue, it introduces a new challenge: HTTP is stateless.
When the server redirects the browser from a POST route to a GET route, any context about the operation (such as "Account created successfully" or "Invalid password") is lost.
A Flash Message is a temporary notification stored in session memory or an encrypted cookie. It follows a read-once lifecycle:
- Created during the
POSThandler execution. - Stored temporarily (in a cookie or server session).
- Retrieved during the subsequent
GETrequest. - Deleted immediately after being rendered to the user.
Implementing Flash Messages in Go
Let's build a working Go application that implements the PRG pattern using encrypted browser cookies for Flash messages. We'll use the popular gorilla/sessions package to handle secure cookie storage.
Prerequisites
Initialize a new Go module and install the Gorilla Sessions package:
go mod init go-flash-demo
go get github.com/gorilla/sessions
Step 1: Complete Go Server Implementation
Create a file named main.go:
package main
import (
"html/template"
"log"
"net/http"
"os"
"github.com/gorilla/sessions"
)
// Initialize the cookie store with a secret key for encryption.
// In production, load this key from a secure environment variable.
var store = sessions.NewCookieStore([]byte(getEnv("SESSION_KEY", "super-secret-key-32-bytes-long!")))
const flashSessionName = "flash-session"
func main() {
http.HandleFunc("/", handleHome)
http.HandleFunc("/submit", handleSubmit)
http.HandleFunc("/dashboard", handleDashboard)
log.Println("Server starting on http://localhost:8080...")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatalf("Server failed: %v", err)
}
}
// Helper function to set a flash message in the cookie session.
func setFlash(w http.ResponseWriter, r *http.Request, name string, value string) {
session, _ := store.Get(r, flashSessionName)
session.AddFlash(value, name)
if err := session.Save(r, w); err != nil {
http.Error(w, "Failed to save session", http.StatusInternalServerError)
}
}
// Helper function to retrieve and clear flash messages.
func getFlash(w http.ResponseWriter, r *http.Request, name string) []string {
session, _ := store.Get(r, flashSessionName)
flashes := session.Flashes(name)
// Saving the session after reading flashes clears them automatically.
if err := session.Save(r, w); err != nil {
log.Printf("Failed to clear flash session: %v", err)
}
var result []string
for _, f := range flashes {
if str, ok := f.(string); ok {
result = append(result, str)
}
}
return result
}
// 1. GET / - Renders the initial form
func handleHome(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
renderTemplate(w, "templates/index.html", nil)
}
// 2. POST /submit - Processes form data and executes Redirect
func handleSubmit(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
username := r.FormValue("username")
// Basic validation
if username == "" {
setFlash(w, r, "error", "Username cannot be empty!")
http.Redirect(w, r, "/", http.StatusSeeOther) // Redirect back to home
return
}
// Business logic execution (e.g., database save)
log.Printf("Successfully registered user: %s", username)
// Set success notification and redirect using PRG pattern
setFlash(w, r, "success", "Welcome aboard, "+username+"! Your account is active.")
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
}
// 3. GET /dashboard - Renders page with flash messages
func handleDashboard(w http.ResponseWriter, r *http.Request) {
successMessages := getFlash(w, r, "success")
data := map[string]interface{}{
"Successes": successMessages,
}
renderTemplate(w, "templates/dashboard.html", data)
}
func renderTemplate(w http.ResponseWriter, tmplFile string, data interface{}) {
tmpl, err := template.ParseFiles(tmplFile)
if err != nil {
http.Error(w, "Template parsing error: "+err.Error(), http.StatusInternalServerError)
return
}
if err := tmpl.Execute(w, tmpl); err != nil {
http.Error(w, "Template execution error: "+err.Error(), http.StatusInternalServerError)
}
}
func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}
Step 2: Creating the HTML Templates
Create a folder named templates and add the following two template files:
templates/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>PRG Pattern in Go</title>
<style>
body { font-family: sans-serif; max-width: 500px; margin: 50px auto; padding: 20px; }
.alert-error { background-color: #f8d7da; color: #721c24; padding: 10px; margin-bottom: 15px; border-radius: 4px; }
.form-group { margin-bottom: 15px; }
input[type="text"] { width: 100%; padding: 8px; box-sizing: border-box; }
button { padding: 10px 15px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
</style>
</head>
<body>
<h2>User Registration</h2>
<form action="/submit" method="POST">
<div class="form-group">
<label for="username">Username:</label>
<input type="text" id="username" name="username" placeholder="Enter name">
</div>
<button type="submit">Register</button>
</form>
</body>
</html>
templates/dashboard.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Dashboard</title>
<style>
body { font-family: sans-serif; max-width: 500px; margin: 50px auto; padding: 20px; }
.alert-success { background-color: #d4edda; color: #155724; padding: 10px; margin-bottom: 15px; border-radius: 4px; }
</style>
</head>
<body>
{{range .Successes}}
<div class="alert-success">{{.}}</div>
{{end}}
<h1>Dashboard</h1>
<p>Welcome to your application dashboard!</p>
<a href="/">Back to Form</a>
</body>
</html>
Key Best Practices for Flash Messages in Go
- Use HTTP
303 See Otherfor Redirects: When redirecting after aPOSTrequest, preferhttp.StatusSeeOther(303) overhttp.StatusFound(302). HTTP 303 explicitly guarantees that the browser will issue aGETrequest to the target URL regardless of the original method. - Secure Your Cookie Key: Flash cookies must be signed (and ideally encrypted) to prevent client-side tampering. Never hardcode session keys in production source code; always pull them from environment variables or secret vaults.
- Keep Messages Small: Cookies have a browser limit of approximately 4KB. Store short string messages or status codes in flash notifications rather than large payload objects.
- Category-Based Flashes:
Organize flash messages by types (e.g.,
"success","error","warning"). Gorilla'ssession.AddFlash(value, key)lets you scope messages logically for styled rendering in HTML templates.
Conclusion
Combining the Post/Redirect/Get (PRG) pattern with Flash Messages provides a robust foundation for handling web forms. It prevents accidental double-submits, protects server state integrity, and provides users with immediate, intuitive visual feedback.
With Go's standard library http package and lightweight session managers like gorilla/sessions, implementing clean and secure Flash messages takes only a few lines of idiomatic code.