It's Friday afternoon, 4:45 PM. My teammates are already discussing weekend plans in Slack. I'm finishing up a critical piece of our payment processing service. The code is done, tests pass, and I'm about to commit it before heading into the weekend. My finger hovers over the enter key for a moment...
But there's no hesitation, no worry. I commit the code, close my laptop, and join the weekend conversation without a second thought about my code failing in production.
This wasn't always the case. Ten years ago, I'd be checking my phone all weekend, worried about 3 AM calls from our on-call engineer.
What changed? Let me tell you my story of how I learned to write Go code I could trust completely.
The Cost of Uncertain Code
My journey with Go started in 2021 when I was working at a rapidly growing startup. We were moving from a monolithic Java application to microservices, and Go seemed like the perfect fit: fast, simple, and with built-in concurrency.
But my early Go code looked like this:
func ProcessPayment(paymentID string) error {
// Get payment details
payment, err := db.GetPayment(paymentID)
if err != nil {
return err // Which error? What happened?
}
// Process the payment
err = paymentGateway.Process(payment)
if err != nil {
return err // Again, what went wrong?
}
// Update payment status
err = db.UpdateStatus(paymentID, "processed")
if err != nil {
return err // Did the payment go through? Is it in a bad state?
}
return nil
}
Functionally, this code worked. But it haunted me at night because:
- Error handling was minimal and provided no context
- There was no logging for debugging
- No consideration for partial failures
- No validation of inputs
- No testing for edge cases
The result? Support tickets on Saturday mornings. Mysterious payment failures. Hours spent on debugging sessions. And the constant anxiety that something might be failing silently.
The Path to Trustworthy Code
Everything changed when I joined a team at WF. My mentor there had a simple philosophy: "Write every line of code as if you'll be on vacation when it runs in production."
This mindset shift transformed how I approached Go development. Here's what I learned, and what now lets me sleep well after committing code:
1. Errors Are Your Friends, Not Exceptions
Go's error handling is verbose but powerful when used correctly. The secret is to treat errors as valuable information carriers, not just failure signals:
func ProcessPayment(paymentID string) error {
// Get payment details
payment, err := db.GetPayment(paymentID)
if err != nil {
return fmt.Errorf("failed to retrieve payment %s: %w", paymentID, err)
}
// Validate before proceeding
if err := validatePayment(payment); err != nil {
return fmt.Errorf("payment validation failed for %s: %w", paymentID, err)
}
// Process the payment with context
err = paymentGateway.Process(payment)
if err != nil {
// Log additional details for debugging
log.WithFields(log.Fields{
"payment_id": paymentID,
"amount": payment.Amount,
"currency": payment.Currency,
}).Error("Payment processing failed")
return fmt.Errorf("gateway failed to process payment %s: %w", paymentID, err)
}
// Success path is clearly logged too
log.WithField("payment_id", paymentID).Info("Payment processed successfully")
return nil
}
The difference? When something goes wrong, I have rich context. The error messages tell a story, making debugging faster and easier.
2. Tests That Give You Confidence
My earlier self wrote tests that proved the code worked in the happy path. My current self writes tests that prove the code won't break in production:
func TestProcessPayment(t *testing.T) {
// Happy path test
t.Run("successful payment processing", func(t *testing.T) {
// Setup and assertions
})
// What happens when things go wrong
t.Run("database unavailable", func(t *testing.T) {
// Simulate DB failure
})
t.Run("payment gateway timeout", func(t *testing.T) {
// Simulate slow payment gateway
})
t.Run("invalid payment data", func(t *testing.T) {
// Test validation logic
})
t.Run("partial failure - payment processed but status update fails", func(t *testing.T) {
// Test recovery mechanisms
})
}
A comprehensive test suite is like insurance. It doesn't prevent all problems, but it significantly reduces the likelihood of common failures reaching production.
3. Graceful Degradation
One key insight: not all failures are equal. The best Go code doesn't just handle errors; it gracefully degrades functionality:
func GetUserRecommendations(userID string) ([]Recommendation, error) {
// Try to get personalized recommendations
recommendations, err := recommendationService.GetPersonalized(userID)
if err != nil {
// Log the error
log.WithError(err).Warn("Failed to get personalized recommendations, falling back to popular items")
// Fall back to popular recommendations
return recommendationService.GetPopular()
}
return recommendations, nil
}
This pattern means your code tries its best to provide value, even when some components fail.
4. Monitoring and Observability Built-In
Code I can trust doesn't just work well; it tells me how it's working:
func ProcessOrder(ctx context.Context, order Order) error {
// Start timing the operation
start := time.Now()
// Use defer to ensure metrics are always recorded
defer func() {
metrics.ObserveOrderProcessingTime(time.Since(start))
metrics.IncrementOrdersProcessed()
}()
// Trace this operation for distributed tracing
span, ctx := tracer.StartSpanFromContext(ctx, "process_order")
defer span.Finish()
// Add helpful information to the trace
span.SetTag("order_id", order.ID)
span.SetTag("customer_id", order.CustomerID)
// Process the order with the traced context
if err := orderProcessor.Process(ctx, order); err != nil {
// Record error in metrics and trace
metrics.IncrementOrderErrors()
span.SetTag("error", true)
span.LogKV("error.message", err.Error())
return fmt.Errorf("processing order %s failed: %w", order.ID, err)
}
return nil
}
When your code has built-in monitoring, you don't need to wonder if it's working correctly in production. You know.
Real-Life Example: The 3 AM Bug That Never Called
Last year, we deployed a new feature to our payment system right before a long holiday weekend. The old me would have been anxious the entire time, but I wasn't worried at all.
Sure enough, something unexpected happened: a third-party API we depended on changed their response format. But instead of a production outage, here's what happened:
- Our validation caught the changed format and returned a clear error
- The circuit breaker we implemented prevented cascading failures
- The system fell back to a secondary processing method
- Our monitoring alerted the on-call engineer with the exact issue
- Detailed logs showed exactly where and how the format had changed
The on-call engineer fixed it with a simple config change—no emergency, no all-hands debugging session.
The best part? I only found out about this when I read the incident report on Tuesday morning.
How to Write Go Code You Can Trust
After a decade of writing Go, here's my checklist for code I can trust enough to disconnect completely from work:
- Handle errors with context: Wrap errors, add information, make debugging easy
- Test failure modes: Don't just test success cases; test what happens when things fail
- Build in graceful degradation: Design systems that bend rather than break
- Make it observable: Logging, metrics, and tracing are not afterthoughts
- Validate early and thoroughly: Catch bad inputs before they cause damage
- Document assumptions: Clear documentation helps future you and your teammates
Conclusion
The difference between Go code that keeps you up at night and Go code you can trust isn't about clever algorithms or advanced features. It's about care, attention to detail, and a mindset that prepares for the unexpected.
When I interview Go developers now, I don't just look at how well they can write code that works. I look at how they handle the edge cases, how they think about failures, and whether their code would let them enjoy their weekends without worry.
Because in the end, the best code isn't just functionally correct—it's trustworthy enough that you can commit it on Friday afternoon and genuinely disconnect until Monday morning.
And for me, that peace of mind is worth every extra line of error handling, every additional test case, and every minute spent making my code more robust.
So next time you're writing Go code, ask yourself: "Would I sleep well tonight if this ran in production after I left?" If the answer isn't a confident "yes," you have more work to do.
Your future self—possibly on a beach somewhere without laptop access—will thank you.
Top comments (5)
How about your experience? Can you tell me more about it? 😉
Great post! Really enjoyed reading your insights. Keep up the amazing work—looking forward to your next post! 👏
Great article! Ensuring code reliability and anticipating potential issues are crucial. These best practices truly help developers sleep well after committing. 🚀
Loved this post
befect, good job