go
64 lines · 9 steps
A cookie-backed signup wizard in Gin
A multi-step signup form keeps its whole state in one signed, encrypted cookie instead of server-side sessions.
Explained by
highlit
1package signup
2
3var wizardCodec = securecookie.New(
4 []byte(os.Getenv("WIZARD_HASH_KEY")),
5 []byte(os.Getenv("WIZARD_BLOCK_KEY")),
6)
7
8type WizardState struct {
9 Step int `json:"step"`
10 Email string `json:"email"`
11 Company string `json:"company"`
12 Plan string `json:"plan"`
13}
14
15func loadState(c *gin.Context) WizardState {
16 var st WizardState
17 if raw, err := c.Cookie("signup_wizard"); err == nil {
18 _ = wizardCodec.Decode("signup_wizard", raw, &st)
19 }
20 return st
21}
22
23func saveState(c *gin.Context, st WizardState) error {
24 encoded, err := wizardCodec.Encode("signup_wizard", st)
25 if err != nil {
26 return err
27 }
28 c.SetCookie("signup_wizard", encoded, 1800, "/signup", "", true, true)
29 return nil
30}
31
32func Step(c *gin.Context) {
33 st := loadState(c)
34
35 if c.Request.Method == http.MethodPost {
36 switch st.Step {
37 case 0:
38 st.Email = c.PostForm("email")
39 case 1:
40 st.Company = c.PostForm("company")
41 case 2:
42 st.Plan = c.PostForm("plan")
43 }
44 st.Step++
45
46 if st.Step >= 3 {
47 accounts.Create(st.Email, st.Company, st.Plan)
48 c.SetCookie("signup_wizard", "", -1, "/signup", "", true, true)
49 c.Redirect(http.StatusSeeOther, "/signup/done")
50 return
51 }
52
53 if err := saveState(c, st); err != nil {
54 c.AbortWithStatus(http.StatusInternalServerError)
55 return
56 }
57 c.Redirect(http.StatusSeeOther, "/signup")
58 return
59 }
60
61 c.HTML(http.StatusOK, fmt.Sprintf("signup/step%d.tmpl", st.Step), gin.H{
62 "state": st,
63 })
64}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Encrypted, signed cookies let you carry multi-step form state without any server-side session store.
- 2Advancing a step counter and redirecting after each POST implements the classic Post/Redirect/Get pattern.
- 3Clearing the cookie with a negative max-age is how you finalize and tear down transient wizard state.
Related explainers
go
package streaming import ( "bufio"
Streaming NDJSON logs over HTTP in Go
http-streaming
channels
select
Advanced
10 steps
java
@Component @Converter public class EncryptedStringConverter implements AttributeConverter<String, String> {
Transparent column encryption in Spring & JPA
encryption
aes-gcm
jpa-converter
Advanced
10 steps
go
package api import ( "crypto/sha256"
ETag conditional requests in Gin
http-caching
etag
conditional-requests
Intermediate
6 steps
go
func (w *Watcher) resetDebounce(d time.Duration) { if !w.timer.Stop() { select { case <-w.timer.C:
Debouncing a stream of events in Go
debounce
timers
channels
Advanced
7 steps
go
package logging import ( "context"
Deduplicating log attributes in Go's slog
decorator-pattern
structured-logging
immutability
Intermediate
8 steps
ruby
class ApplicationController < ActionController::Base EXPERIMENTS = { checkout_button_color: %w[control blue green], onboarding_flow: %w[control streamlined]
How A/B test cohorts are assigned in Rails
a-b-testing
cookies
hashing
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/a-cookie-backed-signup-wizard-in-gin-explained-go-c5c2/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.