i didn't switch to go because it was trending. i switched because i wanted to understand what was actually happening. here's what two months of building a real e-commerce backend taught me.
Suprim Khatri
Backend Developer · May 15, 2026
i came from django, then moved to next.js, got comfortable doing fullstack javascript. i could ship. i had the tools. and then i decided to throw all of that away and learn go.
not because someone told me to. not because it was on a job listing. just because i wanted to know what was underneath.
the first thing i built was auth — from scratch. no better auth, no passport, no magic. just me, bcrypt, jwt, and a lot of confusion about why my refresh tokens kept expiring wrong.
that confusion was the whole point.
in javascript you throw things and catch them somewhere else and hope for the best. in go, every function that can fail returns an error. you handle it right there, or you wrap it and send it up.
rows, err := pool.Query(ctx, query)
if err != nil {
return nil, fmt.Errorf("GetProducts: %w", err)
}at first it felt like boilerplate. by the third week it felt like clarity. you always know what can go wrong. nothing fails silently. and when something does blow up in production, your logs tell you exactly where.
i set up structured logging with slog early. best decision i made.
cartspace is a full e-commerce backend — products, variants, categories, auth, image uploads to cloudinary. built with gin, postgresql via pgx and sqlc, deployed on render inside a turborepo monorepo alongside the next.js frontend.
the architecture i landed on:
pgtype.UUID for ids — maps cleanly to null when zero-valuedthat last one sounds obvious but it took me embarrassingly long to get disciplined about it.
pointers. specifically, understanding when to use *T vs T and why rows.Scan needed &todo.ID instead of todo.ID.
the way it finally clicked: Scan needs to write into memory that already exists. a nil pointer is just an address to an empty lot — the postman shows up and there's no house. you need to give it a real struct first.
var product models.Product // allocate the struct
rows.Scan(&product.ID, &product.Name) // pass addresses so Scan can writeonce that clicked, everything else followed. receiver methods, passing structs to functions, why you'd return *Product from a repository but Product from a handler response.
i had written "tests" before. they were mostly console.log in disguise.
in cartspace, i learned go testing properly — repository interface mocking, httptest recorders, shared helpers_test.go per package, one test file per handler. the pattern is clean once you set it up:
func TestCreateCategory(t *testing.T) {
mockRepo := &MockCategoryRepo{}
handler := NewCategoryHandler(mockRepo)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
handler.CreateCategory(c)
assert.Equal(t, http.StatusCreated, w.Code)
}writing tests made me realize how much my earlier code was untestable by design — everything too coupled, too many side effects, no interfaces anywhere.
javascript has an ecosystem answer for everything. go makes you build it.
it took longer. i understand auth now in a way i never did using a library.
cartspace is deployed. the backend is live at api.cartspace.suprimkhatri.com.np. i navigated render's turborepo build quirks, got GIN_MODE=release set, figured out how to copy openapi.json at build time, and shipped it.
i'm documenting what i learn on codex — this site. i post go concepts on x every day, not for followers, just to stay consistent and force myself to actually understand what i'm writing about.
go hasn't made me faster yet. it's made me slower in the best possible way — slower to write code i don't understand, slower to reach for an abstraction i can't explain, slower to ship something i haven't thought through.
that's the trade. i'll take it.