DEV Community

Trung Duong
Trung Duong

Posted on

10 5 5 4 5

😴 Go Code You Can Trust: Sleep Well After You Commit

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.

Golang sleep
Image source: Medium

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
}
Enter fullscreen mode Exit fullscreen mode

Functionally, this code worked. But it haunted me at night because:

  1. Error handling was minimal and provided no context
  2. There was no logging for debugging
  3. No consideration for partial failures
  4. No validation of inputs
  5. 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
}
Enter fullscreen mode Exit fullscreen mode

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
    })
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Our validation caught the changed format and returned a clear error
  2. The circuit breaker we implemented prevented cascading failures
  3. The system fell back to a secondary processing method
  4. Our monitoring alerted the on-call engineer with the exact issue
  5. 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:

  1. Handle errors with context: Wrap errors, add information, make debugging easy
  2. Test failure modes: Don't just test success cases; test what happens when things fail
  3. Build in graceful degradation: Design systems that bend rather than break
  4. Make it observable: Logging, metrics, and tracing are not afterthoughts
  5. Validate early and thoroughly: Catch bad inputs before they cause damage
  6. 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.

Heroku

Built for developers, by developers.

Whether you're building a simple prototype or a business-critical product, Heroku's fully-managed platform gives you the simplest path to delivering apps quickly — using the tools and languages you already love!

Learn More

Top comments (5)

Collapse
 
trungdlp profile image
Trung Duong

How about your experience? Can you tell me more about it? 😉

Collapse
 
tuna99 profile image
Anh Tu Nguyen

Great post! Really enjoyed reading your insights. Keep up the amazing work—looking forward to your next post! 👏

Collapse
 
devflex-pro profile image
Pavel Sanikovich

Great article! Ensuring code reliability and anticipating potential issues are crucial. These best practices truly help developers sleep well after committing. 🚀

Collapse
 
sabbits profile image
Sab_bitS

Loved this post

Collapse
 
sn_ovn_1ada704c07240 profile image
Sơn Đào Văn

befect, good job

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

DEV is better (more customized, reading settings like dark mode etc) when you're signed in!

Okay