HTTP is stateless. Every request your server receives is independent - it has no memory of previous requests from the same client. Sessions solve this by storing user-specific data on the server side and associating it with a client through a cookie. If you are also working with databases in Go, you might find my post on transactions in Postgres with Golang useful as a companion read.

In this article, we will implement session management in Go using Redis as the session store and the gorilla/sessions package for handling session logic.

Why Redis for Sessions?

You could store sessions in memory, in files, or in a database. Redis is the best choice for most production applications because:

  • Speed - Redis operates in memory, so reads and writes are extremely fast (sub-millisecond)
  • TTL support - Redis can automatically expire keys, which maps perfectly to session expiry
  • Scalability - When you run multiple instances of your app behind a load balancer, all instances can share the same Redis store
  • Persistence - Redis can persist data to disk, so sessions survive server restarts

If you store sessions in application memory, they vanish when the process restarts and cannot be shared across multiple server instances. Redis eliminates both problems.

Setting Up the Project

First, install the required packages:

go get github.com/gorilla/sessions
go get github.com/gomodule/redigo/redis
go get github.com/boj/redistore

The redistore package provides a Redis-backed session store that implements the gorilla/sessions interface. This means you get the clean API of gorilla/sessions with Redis as the backend.

Initializing the Session Store

package main

import (
    "log"
    "net/http"
    "github.com/boj/redistore"
)

var store *redistore.RediStore

func init() {
    var err error
    // NewRediStore(size, network, address, password, keyPairs)
    store, err = redistore.NewRediStore(
        10,           // max idle connections
        "tcp",        // network
        "localhost:6379", // address
        "",           // password (empty if none)
        []byte("secret-key-change-this"),
    )
    if err != nil {
        log.Fatal("Failed to create Redis store: ", err)
    }

    // Configure session options
    store.Options.MaxAge = 86400 * 7 // 7 days
    store.Options.HttpOnly = true
    store.Options.Secure = true      // set to false for local dev
}

The MaxAge option controls how long the session lives in seconds. HttpOnly prevents JavaScript from accessing the session cookie, which protects against XSS attacks. Secure ensures the cookie is only sent over HTTPS.

Creating and Reading Sessions

Here is a login handler that creates a session, and a profile handler that reads from it:

func loginHandler(w http.ResponseWriter, r *http.Request) {
    // Authenticate user (simplified)
    username := r.FormValue("username")
    password := r.FormValue("password")

    if !authenticate(username, password) {
        http.Error(w, "Invalid credentials", http.StatusUnauthorized)
        return
    }

    // Get a session (creates new if doesn't exist)
    session, err := store.Get(r, "session-name")
    if err != nil {
        http.Error(w, "Server error", http.StatusInternalServerError)
        return
    }

    // Set session values
    session.Values["authenticated"] = true
    session.Values["username"] = username

    // Save the session
    err = session.Save(r, w)
    if err != nil {
        http.Error(w, "Failed to save session", http.StatusInternalServerError)
        return
    }

    http.Redirect(w, r, "/profile", http.StatusSeeOther)
}

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

    auth, ok := session.Values["authenticated"].(bool)
    if !ok || !auth {
        http.Error(w, "Unauthorized", http.StatusUnauthorized)
        return
    }

    username := session.Values["username"].(string)
    w.Write([]byte("Welcome, " + username))
}

Building a Session Middleware

Instead of checking session state in every handler, extract it into a middleware:

func requireAuth(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        session, err := store.Get(r, "session-name")
        if err != nil {
            http.Error(w, "Server error", http.StatusInternalServerError)
            return
        }

        auth, ok := session.Values["authenticated"].(bool)
        if !ok || !auth {
            http.Redirect(w, r, "/login", http.StatusSeeOther)
            return
        }

        next(w, r)
    }
}

Now you can protect any route by wrapping the handler:

http.HandleFunc("/profile", requireAuth(profileHandler))
http.HandleFunc("/settings", requireAuth(settingsHandler))
http.HandleFunc("/login", loginHandler) // public

Destroying Sessions (Logout)

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

    // MaxAge < 0 means delete the cookie immediately
    session.Options.MaxAge = -1
    session.Save(r, w)

    http.Redirect(w, r, "/login", http.StatusSeeOther)
}

Key Takeaways

  • Always use HttpOnly and Secure flags on session cookies
  • Redis gives you automatic expiry, shared state across instances, and fast lookups
  • Use middleware to avoid repeating auth checks in every handler
  • Change your secret key in production and rotate it periodically
  • Remember to call session.Save() after modifying session values - forgetting this is a common bug

Sessions are a foundational piece of most web applications. Getting them right from the start saves you from security headaches later. If you are passing session data through your Go handlers, understanding how to marshal structs in Golang will help you serialize session payloads cleanly.