September 18, 2026
What actually changed after six months of writing Go

I wrote a post a couple of months ago about moving from PHP and Laravel into Go, mostly first impressions: explicit error handling, no framework doing the wiring for you, goroutines being a normal tool instead of an escape hatch. Enough time has passed that some of those first impressions have either hardened into habits or turned out to be wrong. Here is the update.
The error handling verbosity never really went away
I said explicit error checks were the thing I’d miss most going back to PHP. That part held up, I do miss it. What I did not expect is that the verbosity itself never gets shorter, you just stop noticing it. Every call site still gets an if err != nil block, and six months in I am still typing that exact string multiple times a day. The difference is it no longer feels like busywork, it feels like the shape the code is supposed to have.
What did change is how I use the wrapping. Early on I was returning errors mostly unwrapped, or wrapping them with a vague message. Now every wrap includes exactly what was being attempted and the identifier involved:
func (s *Service) chargeInvoice(ctx context.Context, invoiceID string) error {
inv, err := s.repo.FindInvoice(ctx, invoiceID)
if err != nil {
return fmt.Errorf("charge invoice %s: find invoice: %w", invoiceID, err)
}
if err := s.gateway.Charge(ctx, inv.Amount, inv.CustomerID); err != nil {
return fmt.Errorf("charge invoice %s: gateway charge for customer %s: %w", invoiceID, inv.CustomerID, err)
}
return nil
}
The payoff shows up in production logs, not in the code review. When something fails three layers down, the top-level log line already reads like a sentence describing exactly what happened and to which record, no need to go add debug logging and redeploy just to find out which invoice choked. That is the opposite of how I debugged things in Laravel, where the stack trace told you the line but rarely the business context around it.
Concurrency stopped feeling like a special occasion
In the original post I described fanning out work with goroutines and a wait group as something that felt unusual coming from PHP-FPM’s one-request-one-process model. It does not feel unusual anymore. At this point, if I am writing a handler that needs two independent pieces of data, reaching for a goroutine per piece is as automatic as writing a for loop. The mental shift was less about learning the syntax and more about learning to spot which parts of a request are actually independent, and that instinct now applies even when I am not writing Go.
What still trips me up
Project structure. Laravel hands you a folder layout and you argue with it at most. Go hands you nothing, and six months in I am still occasionally restructuring a package because I put something in the wrong place three months ago and it is now imported from five other packages. The compiler catches a lot of mistakes for me, but it has no opinion on where a type should live, and that is still a decision I get wrong more often than I’d like.
The surprise
The genuinely surprising part, in a good way, is how much the compiler and the static type system catch before anything runs. In PHP a typo in an array key or a wrong argument order shows up at runtime, sometimes in production if the code path is rare enough. In Go, most of those same mistakes are a build failure. I underestimated how much mental energy I used to spend in PHP just being careful, energy that Go’s compiler now spends for me.
Would I make the same move again? Yes, without much hesitation. If I were starting over on day one, the one thing I would tell myself is to stop trying to make Go feel like Laravel. The lack of a framework is not a gap to fill with the closest equivalent, it is the point, and fighting that for the first month cost me more time than actually learning the language did.