Clean Code with Go — Part 2: Naming — Code That Speaks for Itself
Good names are the best documentation. Learn Go naming conventions and how to write self-documenting code.
Picture this: you’ve just joined a new team and you’re asked to investigate a bug. You open the file, and this is what greets you:
func proc(d *D, tmp int) (interface{}, error) {
x := getStuff(d.id)
if x == nil || tmp > x.v {
return nil, fmt.Errorf("err")
}
data2 := h(x)
return data2, nil
}
Two hours later — after jumping between four other files, pinging two teammates, and brewing a second cup of coffee — you finally figure out that this 20-line function is processing a payment validation. Two hours just to read the code. Not to fix the bug. Just to read it.
This isn’t a hypothetical. It happens every day on teams that underestimate the power of good naming. And here’s the good news: it’s entirely preventable.
A good name is the best documentation — and it never goes out of date.
Concept Map: Naming in Go
mindmap
root((Naming))
Variables
Descriptive and contextual
Avoid ambiguous abbreviations
Length proportional to scope
Functions
Verb + noun pattern
Single purpose, single name
Boolean: isX / hasX
Struct & Interface
Use nouns
Interface with -er suffix
Package
Lowercase, no underscores
Short but meaningful
Go Naming Conventions
Go has a simple but powerful philosophy around naming. Here are the essential things you need to know:
1. Exported vs Unexported
In Go, the first letter determines visibility. User is accessible from outside the package; user is internal only.
type User struct { ... } // ✅ Exported — accessible by other packages
type internalCache struct { ... } // ✅ Unexported — internal to this package only
2. Acronyms Are Written Consistently
For acronyms like URL, ID, HTTP — Go convention uses all caps (not Url, Id, Http).
// ❌ BAD:
func getUserUrl(userId int) string { ... }
// ✅ GOOD:
func getUserURL(userID int) string { ... }
3. Interfaces Use the -er Suffix
Go interfaces are ideally named after the behavior they describe, using an -er suffix.
type Reader interface { Read(p []byte) (n int, err error) }
type Storer interface { Store(key string, value any) error }
type PaymentProcessor interface { ProcessPayment(amount float64) error }
4. Name Length Should Match Scope
A variable that lives for two lines can be short. A variable that lives across a whole function or struct should be descriptive.
// ✅ GOOD: `i` is fine for a loop counter
for i := 0; i < len(users); i++ { ... }
// ✅ GOOD: a longer name for a wider scope
cachedUser, err := userCache.GetByID(userID)
❌ Wrong Implementation
Here’s an example of code with terrible naming — exactly what you’d encounter in the story above:
// ❌ BAD: Meaningless names throughout
type D struct { // What is D? Data? Domain? Doctor?
id int
v float64 // v = value? version? volume?
ts int64 // ts = timestamp? test score?
}
// proc = process what, exactly?
func proc(d *D, tmp int) (interface{}, error) {
x := getStuff(d.id) // getStuff gets what "stuff"?
if x == nil || tmp > x.v {
return nil, fmt.Errorf("err") // what error?
}
data2 := h(x) // h() = handler? hash? helper?
return data2, nil
}
Why this is wrong:
D,x,tmp,data2give the reader zero contextproc(),h(),getStuff()force every reader to guess their intentfmt.Errorf("err")is a completely useless error message- One ambiguous name is annoying — an entire codebase of them is a nightmare
✅ Correct Implementation
Now let’s rewrite the same logic with meaningful names:
// ✅ GOOD: Clear names that speak for themselves
type Payment struct {
id int
amount float64
expiresAt int64
}
// processPayment validates and processes a user's payment.
// Returns an error if the payment is not found or has expired.
func processPayment(payment *Payment, currentUnixTime int64) (*PaymentResult, error) {
existingPayment, err := getPaymentByID(payment.id)
if err != nil {
return nil, fmt.Errorf("payment not found: %w", err)
}
isExpired := currentUnixTime > existingPayment.expiresAt
if isExpired {
return nil, fmt.Errorf("payment %d has expired", payment.id)
}
result := buildPaymentResult(existingPayment)
return result, nil
}
Why this works:
Payment,expiresAt,currentUnixTime— you understand them instantly, no extra docs neededprocessPayment(),getPaymentByID(),buildPaymentResult()— clear verb + noun pairsisExpired— a boolean with the conventionalisprefix- Informative error messages with
%wfor proper error wrapping
Real-World Use Case: User Authentication Service
Here’s how proper naming looks applied across a real-world auth service:
package auth
import (
"context"
"fmt"
"time"
)
// UserAuthenticator defines the contract for user authentication.
type UserAuthenticator interface {
Authenticate(ctx context.Context, credentials Credentials) (*AuthToken, error)
RevokeToken(ctx context.Context, tokenID string) error
}
// Credentials holds the login information provided by the user.
type Credentials struct {
Email string
Password string
}
// AuthToken represents a token issued after successful authentication.
type AuthToken struct {
TokenID string
UserID int
ExpiresAt time.Time
IssuedAt time.Time
}
// authService is the internal implementation of UserAuthenticator.
type authService struct {
userRepo UserRepository
tokenRepo TokenRepository
hasher PasswordHasher
}
// Authenticate verifies the credentials and returns a token if valid.
func (s *authService) Authenticate(ctx context.Context, creds Credentials) (*AuthToken, error) {
foundUser, err := s.userRepo.FindByEmail(ctx, creds.Email)
if err != nil {
return nil, fmt.Errorf("user lookup failed: %w", err)
}
isPasswordValid := s.hasher.Compare(creds.Password, foundUser.HashedPassword)
if !isPasswordValid {
return nil, ErrInvalidCredentials
}
token, err := s.tokenRepo.Create(ctx, foundUser.ID)
if err != nil {
return nil, fmt.Errorf("token creation failed: %w", err)
}
return token, nil
}
Notice how readable this is. Without a single explanatory comment, you know exactly what every line is doing and why.
📋 Summary: 5 Naming Rules Every Go Developer Should Know
1. Name things by intent, not implementation
calculateTotalPrice()beatscalc()ordoThing()every time2. Follow Go’s acronym convention
userID,httpClient,getURL()— notuserId,httpClient,getUrl()3. Name interfaces with the
-ersuffixReader,Storer,PaymentProcessor,UserAuthenticator4. Prefix booleans with
is,has, orcanisExpired,hasPermission,canRetry5. Name length should be proportional to scope A loop counter can be
i. A struct field must beexpiresAt, neverea
🎯 Your Challenge
Open the last PR or commit you worked on. Find 3 variables or functions whose names could be improved, and rename them using the rules above.
Then show it to a teammate — without any explanation. Do they immediately understand what the code does?
If yes, you’ve written code that speaks for itself. 🎉
🇮🇩 Versi Indonesia | 🇬🇧 English version