DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Comprehensive Guide to Property-Based Testing in Go: Principles and Implementation
  • The Cypress Edge: Next-Level Testing Strategies for React Developers
  • Automated Kubernetes Testing With Terratest: A Step-by-Step Guide
  • From Bricks to Masterpieces: The Artistry of Building Quality in Agile

Trending

  • Stop Poisoning Your Models: How I Built a CV Dataset Quality Toolkit I Can Reuse Forever
  • When Angular APIs Return 200 but the Frontend Is Already Failing Users
  • How AI Coding Assistants Are Changing Developer Flow
  • Comparing Top Gen AI Frameworks for Java in 2026
  1. DZone
  2. Coding
  3. Languages
  4. 10 Go Best Practices Every Backend Developer Should Know

10 Go Best Practices Every Backend Developer Should Know

This article covers battle-tested Go best practices covering tooling, code organization, error handling for cleaner, production-ready code.

By 
Akshay Pratinav user avatar
Akshay Pratinav
·
Feb. 18, 26 · Analysis
Likes (1)
Comment
Save
Tweet
Share
2.1K Views

Join the DZone community and get the full member experience.

Join For Free

Go has become a cornerstone language for building scalable backend services, cloud-native applications, and DevOps tooling. While Go’s simplicity is one of its greatest strengths, writing production-ready Go code requires more than just knowing the syntax. This guide distills practical best practices that teams can adopt to improve code quality, consistency, and maintainability.

If you're new to Go, start with the official Go documentation and Effective Go. This article builds on those foundations with team-specific patterns that work well in enterprise environments.

1. Tooling: Your First Line of Defense

a. Formatting and Linting

Consistency is king in collaborative codebases. Go provides excellent built-in tooling:

  • gofmt: Always use gofmt to ensure consistent formatting across your codebase. This eliminates bikeshedding about code style.
  • golangci-lint: Use golangci-lint for comprehensive static analysis. Make sure your CI build fails on lint errors — this catches issues before they reach code review.

b. Mock Generation

Use go:generate with mockgen to facilitate mock generation. Your Makefile should include a target to generate and update mocks, making it easy for developers to keep mocks in sync with interfaces.

2. Code Organization: Structure for Clarity

a. Repository Structure

Follow the Standard Go Project Layout. This convention makes it easy for new team members to navigate your codebase and sets clear expectations about where different types of code live.

b. Import Statement Ordering

Import statements should be grouped and ordered consistently:

  1. Standard library
  2. Third-party libraries
  3. Organization/company libraries
  4. Current project packages

❌ Bad example:

Go
 
import (

    "context"

    "github.com/sirupsen/logrus"

    "github.company.com/org/repo/gapi"

    "github.company.com/org/repo/snow"

    "github.company.com/org/repo/internal/clients/slack"

    gdocs "google.golang.org/api/docs/v1"

    "time"

)


✅ Good example:

Go
 
import (

    "context"

    "time"

    "github.com/sirupsen/logrus"

    gdocs "google.golang.org/api/docs/v1"

    "github.company.com/org/repo/gapi"

    "github.company.com/org/repo/snow"

    "github.company.com/org/repo/internal/clients/slack"

)


c. File Layout Within a Package

After import statements, organize your Go code in this order:

  1. Constants and variables (const, var)
  2. Structs and interfaces
  3. Exported (public) methods
  4. Non-exported (private) methods
  5. Helper methods

This predictable structure makes code easier to navigate and review.

4. Naming: Keep It Simple and Non-Redundant

Avoid names that create redundancy. The package name already provides context.

❌ Bad:

Go
 
type DeploymentTransformerHandlerIntf interface {}

type DeploymentTransformerHandler struct {}


✅ Good:

Go
 
type DeploymentTransformerIntf interface {}

type DeploymentTransformer struct {}


The word Handler adds no value here; it’s just noise that makes the code harder to read.

5. Context Objects: Pass Them, Don’t Store Them

When a context object is needed, pass it through each function call rather than storing it in a struct. This pattern ensures proper context propagation and cancellation handling.

❌ Bad:

Go
 
type Client struct {

    ctx context.Context

}

func (c *Client) DoSomething(foo string) {

    // use c.ctx - Don't do this!

}


✅ Good:

Go
 
func (c *Client) DoSomething(ctx context.Context, foo string) {

    // use ctx

}


Rule of thumb: define your methods to accept a context.Context as the first parameter.

6. Method Signatures: Embrace Functional Options

Avoid methods with long parameter lists. Instead, leverage Go’s functional options pattern for flexible, readable configuration.

❌ Bad:

Go
 
svr := server.New("localhost", 8080, time.Minute, 120)


What do time.Minute and 120 mean? Without reading the function signature, it's impossible to tell.

✅ Good:

Go
 
svr := server.New(

    server.WithHost("localhost"),

    server.WithPort(8080),

    server.WithTimeout(time.Minute),

    server.WithMaxConn(120),

)


This pattern is self-documenting, flexible for future additions, and makes optional parameters trivial to implement.

7. Error Handling: Context Is Everything

a. Use Stack Traces

Use yerrors or an equivalent library to provide stack frames with your errors. Wrap errors with additional context as needed.

Why? Errors need enough context for engineers to debug. Explicitly specifying method names in error messages is fragile and has high maintenance cost. Stack traces solve this elegantly.

b. Security Considerations

Stack frames should be written to logs but never returned in API responses. Leaking implementation details to callers is a security risk.

c. Log Levels Matter

  • Error level: Reserve for internal server errors only
  • Warning level: Use for user or client errors

This distinction enables cleaner monitoring and alerting.

Don’t Log at Every Level

With proper error wrapping and stack frames, you don’t need to log errors at each level of code execution. Propagate the error upward and log it once at the top level.

❌ Bad:

Go
 
docSvc, err := docs.NewService(context, option.WithCredentialsJSON([]byte(gSASecret)))

if err != nil {

    log.Errorf("ERROR :: Google Docs :: NewGoogleApiHandler :: Unable to create Google Docs service :: %v", err)

    return nil, err

}

docsHandler, err := gdocs.NewGoogleDocsHandler(docSvc, mc, l)

if err != nil {

    log.Errorf("Error :: Google Docs :: NewGoogleApiHandler :: Unable to create Google Docs handler :: %v", err)

    return nil, err

}


✅ Good:

Go
 
docSvc, err := docs.NewService(context, option.WithCredentialsJSON([]byte(gSASecret)))

if err != nil {

    return nil, yerrors.Errorf("failed to create google docs service: %w", err)

}

docsHandler, err := gdocs.NewGoogleDocsHandler(docSvc, mc, l)

if err != nil {

    return nil, yerrors.Errorf("failed to create google docs handler: %w", err)

}


The second approach shown is cleaner, more consistent, and the stack trace provides all the context you need.

8. Logging: Structure Over Strings

Use Logrus for logging. Enable JSON format for non-local environments to make logs machine-parseable.

a. Use Structured Logging

❌ Bad:

Go
 
h.Logger.Infof("Got Deployment Number: %+v", deployment)

h.Logger.Errorf("Failed to write AI-generated summary. Error from WriteToRCADocument: %s", err)


✅ Good:

Go
 
log.WithFields(log.Fields{

    "event": event,

    "topic": topic,

    "key":   key,

}).Infof("Processing new event")


Structured logging makes it easy to search, filter, and aggregate logs in production.

b. Handling Known Errors Gracefully

When you’ve identified an error that can be safely ignored, it’s acceptable to log at debug level and continue.

Go
 
user, err := client.AddUser(context, userID)

if err != nil {

    if err.Error() == "user_exists" {

        log.WithError(err).Debug("user exists, continuing")

        continue

    }

    return nil, yerrors.Errorf("unable to add user: %s, %w", userID, err)

}


c. Request-Scoped Logging with Transaction IDs

For low-level code, expose a customizable LoggerFn to enable request-scoped logging with transaction IDs.

Go
 
package health

type Handler struct {

    LoggerFn func(ctx context.Context) *logrus.Entry

}

type HandlerIntf interface {

    CheckHealth(ctx context.Context)

}

func (h *Handler) CheckHealth(ctx context.Context) {

    logger := h.LoggerFn(ctx)

    logger.Info("log something here")

}


Callers can then inject their own logger that extracts the transaction ID from context:

Go
 
healthHandler := health.Handler{

    LoggerFn: middleware.RequestIDLogger,

}


This pattern enables consistent tracing across your entire request lifecycle.

9. HTTP: Prefer Context Timeouts Over Client Timeouts

Use context-based timeouts instead of HTTP client timeouts. This provides more fine-grained control and integrates better with Go’s cancellation model.

Go
 
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)

defer cancel()

req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)

resp, err := client.Do(req)


0. Unit Testing: Table-Driven Tests for the Win

a. Use Testify

Use Testify for easy assertions:

  • assert.EqualValues: For maps, slices, or similar objects where ordering shouldn’t matter
  • assert.EqualError: For simple error comparisons

b. Table-Driven Test Structure

Table-driven tests are the preferred pattern in Go. Structure your tests as follows:

  1. Test cases definition: Define all test cases with their inputs, expected outputs, and mocks
  2. Test driver code: Loop through and execute each test case

Key principles:

  • Test cases should be self-contained, including mocks and expectations
  • The test driver should only call the code under test and perform assertions — no custom logic
  • When using gomock, avoid Any() for parameters, return values, and call counts

Example structure:

Go
 
func TestMyFunction(t *testing.T) {

    tests := []struct {

        name     string

        input    string

        expected string

        wantErr  bool

    }{

        {

            name:     "valid input",

            input:    "hello",

            expected: "HELLO",

            wantErr:  false,

        },

        {

            name:     "empty input",

            input:    "",

            expected: "",

            wantErr:  true,

        },

    }

    for _, tt := range tests {

        t.Run(tt.name, func(t *testing.T) {

            result, err := MyFunction(tt.input)

            if tt.wantErr {

                assert.Error(t, err)

                return

            }

            assert.NoError(t, err)

            assert.Equal(t, tt.expected, result)

        })

    }

}


Conclusion

Writing production-ready Go code isn’t just about making things work — it’s about making them maintainable, debuggable, and consistent. By adopting these practices, you’ll:

  • Reduce cognitive load through consistent formatting and organization
  • Improve debuggability with proper error handling and structured logging
  • Enable better testing through table-driven tests and proper mocking
  • Create self-documenting code with functional options and clear naming

These aren’t arbitrary rules. Each practice solves a real problem teams encounter at scale. Start with the practices that address your biggest pain points, and gradually adopt the rest as your team matures.

Have questions or additional best practices to share? Drop a comment below!

dev Go (programming language) Testing

Opinions expressed by DZone contributors are their own.

Related

  • Comprehensive Guide to Property-Based Testing in Go: Principles and Implementation
  • The Cypress Edge: Next-Level Testing Strategies for React Developers
  • Automated Kubernetes Testing With Terratest: A Step-by-Step Guide
  • From Bricks to Masterpieces: The Artistry of Building Quality in Agile

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook