How to Handle HTTP Cookies in Go

Web
Use the net/http package to read cookies from requests and write them to responses in Go.

The stateless problem

You build a shopping cart. The user adds a widget, refreshes the page, and the cart is empty. HTTP requests are stateless. The server sees every request as a fresh start. Cookies fix this by letting the browser store a small piece of data and send it back automatically. The server reads the cookie, remembers the widget, and the cart survives the refresh.

Cookies are just headers

A cookie is a key-value pair stored by the browser. The server sends a Set-Cookie header in the response. The browser saves the pair. On future requests to the matching domain and path, the browser includes a Cookie header with the data.

Go does not have a separate cookie package. The net/http package handles everything. You create an http.Cookie struct, set its fields, and use http.SetCookie to write the header. To read, you call r.Cookie(name) or r.Cookies().

Think of a cookie like a wristband at a club. The bouncer gives you a wristband with your name on it. You wear it all night. Every time you order a drink, the bartender checks the wristband to know who you are. The wristband travels with you; the bartender doesn't need to look you up in a database every time.

Minimal example

Here's the simplest pattern: check for a cookie, set it if missing, update it if present.

package main

import (
	"fmt"
	"net/http"
)

// handleCookies increments a visit counter stored in a cookie.
func handleCookies(w http.ResponseWriter, r *http.Request) {
	// r.Cookie returns the cookie or an error if missing.
	c, err := r.Cookie("visit_count")
	count := 0
	if err == nil {
		// Cookie exists. Parse the value back to an integer.
		fmt.Sscanf(c.Value, "%d", &count)
	}

	count++

	// SetCookie writes the Set-Cookie header to the response.
	// Path "/" ensures the browser sends this cookie to every route.
	http.SetCookie(w, &http.Cookie{
		Name:  "visit_count",
		Value: fmt.Sprintf("%d", count),
		Path:  "/",
	})

	fmt.Fprintf(w, "Visit: %d", count)
}

func main() {
	http.HandleFunc("/", handleCookies)
	http.ListenAndServe(":8080", nil)
}

How the lifecycle works

When a request arrives, r.Cookie("name") parses the Cookie header and searches for the matching name. It returns an *http.Cookie and an error. If the error is http.ErrNoCookie, the browser didn't send that cookie. This is not a failure; it just means the cookie is absent. Handle it gracefully.

http.SetCookie writes the Set-Cookie header to the response. The browser receives the response, stores the cookie, and includes it in future requests. The Path field controls scoping. If Path is /app, the browser only sends the cookie to URLs starting with /app. If Path is /, the cookie goes to every request on the domain.

The Domain field controls subdomain sharing. If Domain is empty, the cookie is tied to the exact host. If Domain is .example.com, the cookie is shared across www.example.com, api.example.com, and example.com. Be careful with broad domains. Subdomains should only share cookies when necessary.

The community accepts the verbose error check if err != nil because it makes the unhappy path visible. In the minimal example, we check err == nil to proceed only when the cookie exists. Both styles are idiomatic. Trust the boilerplate. It prevents silent bugs.

Realistic example with security

Real applications need security flags. Browsers and attackers respect these flags. Here's how to set a session cookie that expires and protects against common attacks.

// setSessionCookie writes a session cookie with security best practices.
func setSessionCookie(w http.ResponseWriter, sessionID string) {
	// Expires tells the browser when to discard the cookie.
	// Without this, the cookie lives until the browser closes.
	expiry := time.Now().Add(24 * time.Hour)

	cookie := &http.Cookie{
		Name:     "session_id",
		Value:    sessionID,
		Path:     "/",
		Expires:  expiry,
		// HttpOnly prevents JavaScript from reading the cookie.
		// This mitigates XSS attacks stealing the session.
		HttpOnly: true,
		// Secure ensures the cookie is only sent over HTTPS.
		// This prevents network sniffing.
		Secure: true,
		// SameSite restricts cross-site request sending.
		// Strict blocks all cross-site requests.
		SameSite: http.SameSiteStrictMode,
	}

	http.SetCookie(w, cookie)
}

HttpOnly stops JavaScript from accessing the cookie via document.cookie. If an attacker injects a script, they can't steal the session ID. Secure forces the browser to send the cookie only over HTTPS. This prevents interception on public Wi-Fi. SameSite controls cross-site behavior. Strict blocks all cross-site requests. Lax allows top-level navigation like clicking a link. Most login cookies use Lax so users can click links from email or search results.

Security flags are not optional in production. Set them from day one.

Pitfalls and errors

Cookies have size limits. Browsers enforce a limit of roughly 4KB per cookie. Do not store JSON blobs or large payloads. Store a token or ID, and look up the data on the server. If you try to set a cookie that exceeds the limit, the browser may ignore it or truncate it. The server won't know.

r.Cookie returns http.ErrNoCookie when the cookie is missing. Do not treat this as a fatal error. Log it if needed, but proceed. If you forget to check the error and access c.Value on a nil cookie, the program panics with a nil pointer dereference at runtime. Always check the error or use r.Cookies() to iterate safely.

Localhost development can be tricky with Secure cookies. Browsers treat localhost as a secure context for cookies in some cases, but not all. If your cookie vanishes on localhost, check the browser's cookie settings or temporarily disable Secure for development. Never ship without Secure.

The compiler rejects the program with undefined: http if you forget to import net/http. If you try to access a field on http.Cookie that doesn't exist, you get a standard undefined field error. Go's type system catches most mistakes early.

When iterating over all cookies, use for _, c := range r.Cookies(). The underscore discards the index. This signals to readers that you only care about the cookie values. Use _ intentionally to show you considered the discarded value.

Decision matrix

Use r.Cookie(name) when you need a specific cookie and want a clear error if it is missing. Use r.Cookies() when you need to inspect all cookies sent by the browser, such as during debugging or migration. Use http.SetCookie when you need to create or update a cookie in the response. Use a signed or encrypted cookie library when you need to store data the client cannot tamper with, rather than rolling your own crypto. Use a session store on the server when the data is large or sensitive, and only keep a session ID in the cookie. Use MaxAge: -1 when you want to delete a cookie immediately by setting an expired date.

Cookies are headers. Treat them as headers. Keep them small, secure, and scoped.

Where to go next