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.
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.
Raymond Ops
Linux ops automation, cloud-native, Kubernetes, SRE, DevOps, Python, Golang and related tech discussions.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.