GoExpress

why i switched from express to go (and why it was hard)

i knew express. i was comfortable. so why did i throw myself into go, a language that made me question everything i thought i knew about backend dev? honest story.

SK

Suprim Khatri

FullStack Developer · March 15, 2026

4 min read

the comfort zone problem

i had a good thing going with express. knew how to spin up a rest api, wire middleware, connect a db, handle auth. felt natural.

so when i kept feeling this pull toward go, i ignored it for a while. told myself — backend is backend, learn the concepts in one language and transfer them. that logic made me choose express over go when i was starting out. honestly, right call at the time.

but the itch never went away.


why go?

couldn't give you a perfectly rational answer. i just wanted to. sometimes that's enough.

what i knew going in:

  • compiled, fast
  • used heavily at google, uber, cloudflare, docker
  • strong stdlib, less magic

what i didn't know: it would completely change how i think about code.


the first week was humbling

coming from js, go felt like someone took away all my toys.

no npm install something-that-does-everything. no magic abstractions. no try/catch. just you, the language, and a compiler that refuses to let anything slide.

first thing that broke my brain: pointers.

in js, you never think about memory. the runtime handles it. in go, you're suddenly writing *pgxpool.Pool and &todo.ID everywhere and asking yourself — wait, what is actually happening here?

copy
var todo models.Todo         // build the struct in memory
rows.Scan(&todo.ID)          // pass the address so Scan can write into it
todos = append(todos, &todo) // store the address, not a copy

took me an embarrassing amount of time to understand why you'd do var todo models.Todo instead of var todo *models.Todo.

the answer: Scan needs real memory to write into. a pointer to nothing is just an address to an empty lot — the postman shows up and there's no house.

once that clicked, something shifted.


what go taught me that js never did

js hides memory from you. that's a feature — it makes the language approachable. but it also means you can spend years writing js without ever understanding what's actually happening when you pass a value around.

go makes it explicit. every * or & is a deliberate decision about memory. verbose at first. then muscle memory. then you start appreciating it.

same thing happened with error handling. no try/catch in go — every function returns an error you have to deal with manually:

copy
rows, err := pool.Query(ctx, query)
if err != nil {
    return nil, fmt.Errorf("GetAllTodos: %w", err)
}

felt like noise at first. then i realized — this is actually great. every error is explicit. nothing fails silently. you know exactly what can go wrong and where.

compare that to js where a promise can reject and you don't even know unless you remember to .catch() it.


the ecosystem gap is real

won't sugarcoat this. the js ecosystem is incredible.

better auth, drizzle, zod, resend, vercel ai sdk — everything is one npm install away and works like magic.

in go, you wire things up yourself. no better auth equivalent. no drizzle. validation is manual — switch cases on struct tags or a library like go-playground/validator.

copy
type CreateTodoRequest struct {
    Title       string `json:"title" validate:"required,min=1,max=255"`
    Description string `json:"description" validate:"omitempty,max=1000"`
}

for a while this felt like a downgrade. then i realized it was the opposite.

when you use magic libraries, you understand the what but not the why. when you build auth yourself in go — generating jwt tokens, setting httponly cookies, handling refresh logic, writing email verification flows — you understand auth at a level that using better auth never gave me.

copy
// generating a jwt in go, no magic
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
    "sub": userID,
    "exp": time.Now().Add(15 * time.Minute).Unix(),
})
 
signed, err := token.SignedString([]byte(os.Getenv("JWT_SECRET")))
if err != nil {
    return "", fmt.Errorf("signToken: %w", err)
}

the js ecosystem's strength is also its weakness: it abstracts so much that you can ship without understanding.


where i am now

still learning. still getting confused. still occasionally wondering why i didn't just stick with express.

but then something clicks — a pointer makes sense, an error handling pattern feels right, raw sql stops feeling scary — and i remember why i started.

go is making me a better developer. not because it's better than js, but because it forces me to think.

and thinking, it turns out, is the whole point.


if you're a js dev curious about go: do it. give yourself a few weeks of genuine discomfort. the other side is worth it.

GoExpressBackendJavaScriptLearning