Backend Development 14 min read

10 Essential Go Practices to Write Flexible, Maintainable Code

This article presents ten practical Go programming techniques—from using a single GOPATH and wrapping for‑select loops to defining custom types, improving enum handling with iota, and encapsulating repetitive logic with context helpers—aimed at building flexible, readable, and easily maintainable applications.

Raymond Ops
Raymond Ops
Raymond Ops
10 Essential Go Practices to Write Flexible, Maintainable Code

Ten useful Go techniques compiled from years of experience, focusing on flexibility, readability, and maintainability for long‑lived applications.

1. Use a single GOPATH

Multiple GOPATHs cause versioning side‑effects and reduce flexibility; stick to one GOPATH unless a project is extremely large and critical.

2. Wrap for‑select in a function

Encapsulate a for‑select loop in its own function to simplify early exits and improve readability.

<code>func main() {
L:
    for {
        select {
        case <-time.After(time.Second):
            fmt.Println("hello")
        default:
            break L
        }
    }
    fmt.Println("ending")
}
</code>

Or move the loop into a helper function:

<code>func main() {
    foo()
    fmt.Println("ending")
}

func foo() {
    for {
        select {
        case <-time.After(time.Second):
            fmt.Println("hello")
        default:
            return
        }
    }
}
</code>

3. Initialise structs with field names (tag syntax)

Using named fields prevents compilation errors when the struct definition changes.

<code>type T struct {
    Foo string
    Bar int
    Qux string
}

func main() {
    t := T{Foo: "example", Qux: 123}
    fmt.Printf("t %+v\n", t)
}
</code>

4. Split struct initialisation over multiple lines

When a struct has several fields, write each field on its own line for better readability and easier modification.

<code>T{
    Foo: "example",
    Bar: someLongVariable,
    Qux: anotherLongVariable,
    B:   forgetToAddThisToo,
}
</code>

5. Add a String() method for iota‑based integer constants

Define a String() method for enum types so printed values are meaningful.

<code>type State int

const (
    Running State = iota
    Stopped
    Rebooting
    Terminated
)

func (s State) String() string {
    switch s {
    case Running:
        return "Running"
    case Stopped:
        return "Stopped"
    case Rebooting:
        return "Rebooting"
    case Terminated:
        return "Terminated"
    default:
        return "Unknown"
    }
}
</code>

6. Make iota start at 1 to avoid zero‑value confusion

Start enum values at 1 so the zero value can represent an undefined state.

<code>const (
    Unknown State = iota
    Running
    Stopped
    Rebooting
    Terminated
)
</code>

7. Return the result of another function directly

If a wrapper does nothing but call another function, return that call directly.

<code>func bar() (string, error) {
    return foo()
}
</code>

8. Define slices and maps as custom types

Creating a named type for a slice or map makes future extensions easier.

<code>type Server struct { Name string }

type Servers []Server

func ListServers() Servers {
    return []Server{{Name: "Server1"}, {Name: "Server2"}, {Name: "Foo1"}, {Name: "Foo2"}}
}

func (s Servers) Filter(name string) Servers {
    var filtered Servers
    for _, srv := range s {
        if strings.Contains(srv.Name, name) {
            filtered = append(filtered, srv)
        }
    }
    return filtered
}
</code>

9. Create a withContext helper for repeated setup

Encapsulate common boiler‑plate such as locking or acquiring a DB connection in a higher‑order function.

<code>func withLockContext(fn func()) {
    mu.Lock()
    defer mu.Unlock()
    fn()
}

func foo() {
    withLockContext(func() {
        // foo work
    })
}

func withDBContext(fn func(db *DB) error) error {
    dbConn := NewDB()
    return fn(dbConn)
}
</code>

10. Add setters/getters for map access and hide implementation behind an interface

Wrap map operations in methods protected by a mutex, and expose only an interface to callers.

<code>type Storage interface {
    Put(key, value string)
    Delete(key string)
    Get(key string) string
}

type memStore struct {
    m  map[string]string
    mu sync.Mutex
}

func (s *memStore) Put(key, value string) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.m[key] = value
}

func (s *memStore) Delete(key string) {
    s.mu.Lock()
    defer s.mu.Unlock()
    delete(s.m, key)
}

func (s *memStore) Get(key string) string {
    s.mu.Lock()
    defer s.mu.Unlock()
    return s.m[key]
}
</code>

These practices improve code elasticity, simplify maintenance, and reduce bugs in Go projects.

concurrencyGobest practicescode organizationGOPATHiotastruct initialization
Raymond Ops
Written by

Raymond Ops

Linux ops automation, cloud-native, Kubernetes, SRE, DevOps, Python, Golang and related tech discussions.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.