Introduction
"There are only two hard things in Computer Science: cache invalidation and naming things." - Phil Karlton
If you've been writing Go for a while, you've probably stared at your screen wondering: "What should I call this function?" or "Is data a good variable name here?" You're not alone. Good naming is one of the most underestimated skills in programming, yet it's what separates readable, maintainable code from cryptic puzzles.
In this comprehensive guide, we'll explore not just Go's naming conventions, but more importantly, the thought process behind creating meaningful names. By the end, you'll have a mental framework for naming that will make your Go code clearer and more professional.
Table of Contents
- Go File Naming Conventions
- Variable Naming in Go
- Function Naming Patterns
- The Art of Naming: Mental Process
- Real-World Naming Examples
- Common Naming Mistakes
- Go Commit Message Best Practices
- Practice Exercises
Go File Naming Conventions
Go has specific conventions that differ from other languages. Let's start with the fundamentals:
Basic File Naming
// ✅ Good - lowercase with underscores
user.go
user_service.go
payment_handler.go
database_connection.go
auth_middleware.go
// ❌ Avoid - camelCase or PascalCase in file names
userService.go
PaymentHandler.go
DatabaseConnection.go
Special File Types
// Test files - always end with _test.go
user_test.go
user_service_test.go
payment_handler_test.go
// Build constraint files
config_dev.go
config_prod.go
database_postgres.go
auth_linux.go
// Documentation and examples
doc.go // Package documentation
example_test.go // Example functions
Project Structure Example
project/
├── cmd/
│ └── myapp/
│ └── main.go
├── internal/
│ ├── auth/
│ │ ├── auth.go
│ │ ├── auth_test.go
│ │ └── middleware.go
│ └── user/
│ ├── user.go
│ ├── user_test.go
│ └── service.go
├── pkg/
│ └── utils/
│ ├── string_utils.go
│ └── time_utils.go
└── go.mod
Variable Naming in Go
Go's visibility rules through capitalization make variable naming unique. Let's break it down:
Export Visibility Rules
Exported (Public) Variables:
// Visible outside the package - start with uppercase
var DatabaseURL string
var MaxConnections int
var DefaultTimeout time.Duration
const APIVersion = "v1"
const MaxRetries = 3
Unexported (Private) Variables:
// Package-private - start with lowercase
var databaseURL string
var maxConnections int
var defaultTimeout time.Duration
const apiVersion = "v1"
const maxRetries = 3
Idiomatic Go: Short Names in Limited Scopes
One of Go's unique characteristics is embracing short variable names in limited scopes:
// ✅ Idiomatic Go - short names in limited scope
for i, v := range items {
fmt.Printf("Item %d: %v\n", i, v)
}
for k, v := range userMap {
fmt.Printf("User %s: %v\n", k, v)
}
// Common short names that Gophers expect
var (
i, j, k int // loop indices
n int // count/length
c chan string // channel
ctx context.Context
req *http.Request
resp *http.Response
err error
b []byte
s string
)
Boolean Variables
// ✅ Good - clear, positive language
var isActive bool
var hasPermission bool
var canEdit bool
var shouldValidate bool
// ❌ Avoid - unclear or negative
var active bool // unclear type
var notDisabled bool // double negative
var flag bool // too generic
Collections
// Slices and Arrays - use plural nouns
var users []User
var activeConnections []net.Conn
var errorMessages []string
// Maps - describe the relationship
var usersByID map[int]User
var settingsByKey map[string]string
var cacheEntries map[string]CacheEntry
// Channels - often end with 'Chan'
var userChan chan User
var errorChan chan error
var doneChan chan struct{}
Function Naming Patterns
CRUD Operations
// Create operations
func NewUser(name, email string) *User // Constructor
func CreateUser(user *User) error // Database create
func AddUser(user *User) error // Collection add
// Read operations
func GetUser(id int) (*User, error) // Single item
func FindUserByEmail(email string) (*User, error) // Search
func ListUsers() ([]*User, error) // Multiple items
// Update operations
func UpdateUser(user *User) error
func ModifyUserProfile(userID int, changes map[string]interface{}) error
// Delete operations
func DeleteUser(id int) error
func RemoveUser(id int) error
Boolean-Returning Functions
func IsValid(email string) bool
func HasPermission(user *User, resource string) bool
func CanAccess(user *User, resource string) bool
func ShouldRetry(err error) bool
func Exists(filename string) bool
Method Naming with Receivers
type User struct {
ID int
Name string
Email string
}
// Use consistent, short receiver names
func (u *User) GetFullName() string { ... }
func (u *User) SetEmail(email string) error { ... }
func (u *User) IsActive() bool { ... }
func (u *User) Save() error { ... }
func (u *User) Delete() error {... }
The Art of Naming: Mental Process
Here's where things get interesting. How do you actually come up with these names? It's not magic—there's a systematic thought process.
Step 1: Ask "What Does This Do?"
Before naming anything, clearly understand its purpose:
// Mental process: "What does this function do?"
// - Opens a file ✓
// - Reads its contents ✓
// - Returns the data as bytes ✓
// - Can return an error ✓
//
// Process: "Read" + "File" = ReadFile
func ReadFile(filename string) ([]byte, error) {
file, err := os.Open(filename) // What's this? → A file handle → "file"
if err != nil {
return nil, err // Error case → "err"
}
defer file.Close()
data, err := io.ReadAll(file) // What's this? → The actual data → "data"
return data, err
}
Step 2: Think in Terms of Actions and Objects
For Functions - Use Verbs:
// Mental process: Action + Object + Context
func GetUserByID(id int) (*User, error) // Get + User + By ID
func CalculateTotalPrice(items []Item) float64 // Calculate + Total + Price
func SendEmailNotification(user *User, msg string) error // Send + Email + Notification
For Variables - Use Nouns:
// Mental process: "What is this thing? What role does it play?"
// Opening a database connection
conn, err := sql.Open("postgres", dsn)
// Think: "What is this?" → A connection → "conn"
// Getting user data from API
userData, err := api.GetUser(userID)
// Think: "What is this?" → Data about a user → "userData"
// Creating HTTP server
server := &http.Server{Addr: ":8080", Handler: router}
// Think: "What is this?" → An HTTP server → "server"
Step 3: Follow the Data Flow
Variables often represent data at different stages of processing:
func ProcessOrder(orderData []byte) (*Order, error) {
// Raw input → "orderData" (bytes from HTTP request)
var orderRequest OrderRequest
err := json.Unmarshal(orderData, &orderRequest)
// Parsed input → "orderRequest" (structured request)
order, err := validateAndCreateOrder(orderRequest)
// Business object → "order" (the actual domain object)
savedOrder, err := repository.SaveOrder(order)
// Persisted result → "savedOrder" (order after database save)
return savedOrder, nil
}
Real-World Naming Examples
Let's walk through the thought process for common Go patterns:
Example 1: HTTP Handler
// What am I doing? Handling user login requests
func handleUserLogin(w http.ResponseWriter, r *http.Request) {
// What's this? → Login credentials from request
credentials, err := parseLoginRequest(r)
if err != nil {
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
// What's this? → The authenticated user
user, err := authenticateUser(credentials)
if err != nil {
http.Error(w, "Authentication failed", http.StatusUnauthorized)
return
}
// What's this? → JWT token for the session
token, err := generateAuthToken(user)
if err != nil {
http.Error(w, "Token generation failed", http.StatusInternalServerError)
return
}
// What's this? → Response data structure
response := LoginResponse{
Token: token,
User: user,
}
json.NewEncoder(w).Encode(response)
}
Example 2: Database Repository
// What am I doing? Finding users in the database
func FindUserByEmail(email string) (*User, error) {
// What's this? → SQL query string
query := "SELECT id, name, email FROM users WHERE email = $1"
// What's this? → Database row result
row := db.QueryRow(query, email)
// What's this? → The user object we're building
var user User
err := row.Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
if err == sql.ErrNoRows {
return nil, ErrUserNotFound
}
return nil, err
}
return &user, nil
}
Example 3: API Client
// What am I doing? Fetching user data from external API
func FetchUserFromAPI(userID int) (*User, error) {
// What's this? → The URL we're calling
url := fmt.Sprintf("https://api.example.com/users/%d", userID)
// What's this? → HTTP response
resp, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("API request failed: %w", err)
}
defer resp.Body.Close()
// What's this? → Raw response body bytes
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
// What's this? → The parsed user object
var user User
err = json.Unmarshal(body, &user)
if err != nil {
return nil, fmt.Errorf("failed to parse user data: %w", err)
}
return &user, nil
}
Common Naming Mistakes
Mistake 1: Too Generic
// ❌ Bad - What kind of data? What processing?
func ProcessData(data []byte) error
// ✅ Good - Specific about purpose
func ValidateUserProfileData(profileData []byte) error
Mistake 2: Inconsistent Patterns
// ❌ Bad - Different verbs for similar operations
func GetUser(id int) (*User, error)
func RetrieveOrder(id int) (*Order, error)
func FetchProduct(id int) (*Product, error)
// ✅ Good - Consistent pattern
func GetUser(id int) (*User, error)
func GetOrder(id int) (*Order, error)
func GetProduct(id int) (*Product, error)
Mistake 3: Technical Instead of Domain Focus
// ❌ Bad - Focuses on implementation
func ExecuteSQLQuery(sql string) (*sql.Rows, error)
// ✅ Good - Focuses on business purpose
func FindActiveUsers() ([]*User, error)
Mistake 4: Unclear Abbreviations
// ❌ Bad - Cryptic abbreviations
func ProcUsrReq(req *http.Request) error
// ✅ Good - Clear, full words
func ProcessUserRequest(req *http.Request) error
Go Commit Message Best Practices
Good naming extends to your commit messages. Here's how to write clear, professional commits for Go projects:
Structure
<type>(<scope>): <subject>
<body>
<footer>
Go-Specific Types and Scopes
// Types
feat // New feature
fix // Bug fix
refactor // Code refactoring
perf // Performance improvement
test // Adding tests
docs // Documentation
style // Formatting (gofmt, goimports)
chore // Dependencies, build changes
// Common Go scopes
api // API changes
auth // Authentication
db // Database
http // HTTP handlers
cmd // CLI changes
internal // Internal packages
pkg // Public packages
Go Commit Examples
# Feature addition
feat(auth): implement JWT token authentication
- Add JWT token generation and validation
- Create middleware for protected routes
- Update user service to handle token refresh
- Add unit tests for token operations
Closes #123
# Bug fix
fix(db): resolve connection pool exhaustion
The database connection pool was not being properly released
causing connection exhaustion under high load.
- Fix connection leak in user repository
- Add proper defer statements for cleanup
- Update connection pool configuration
Fixes #456
# Performance improvement
perf(search): optimize user search query
- Add database index on email field
- Implement query result caching
- Reduce N+1 query problem
Improves search performance by 60%
# Refactoring
refactor(user): extract validation logic to separate package
- Move user validation to internal/validator
- Create reusable validation rules
- Update user service to use new validator
- Maintain backward compatibility
No functional changes.
Practice Exercises
Let's put this knowledge to work! Try naming these scenarios:
Exercise 1: Authentication Function
// You're building a function that:
// - Takes username and password
// - Validates credentials against database
// - Returns JWT token if successful
// - Returns error if authentication fails
func _________(username, password string) (string, error) {
// Your implementation here
}
// Think about it:
// - What's the primary action? (Authenticate? Login? Validate?)
// - What's the main purpose? (Generate token? Verify user?)
// - What would be most clear to other developers?
// Possible answers:
// - AuthenticateUser
// - LoginUser
// - GenerateAuthToken
// - ValidateCredentials
Exercise 2: File Processing Variables
// You're converting CSV to JSON:
func ConvertCSVToJSON(csvPath, jsonPath string) error {
// What would you call these variables?
_______, err := os.ReadFile(csvPath)
// Options: csvData? rawData? fileContent? csvBytes?
_______ := csv.NewReader(bytes.NewReader(_____))
// Options: reader? csvReader? parser?
_______ := make([]map[string]string, 0)
// Options: records? rows? data? entries?
// ... processing logic
_______, err := json.Marshal(_______)
// Options: jsonData? jsonBytes? output?
return os.WriteFile(jsonPath, _______, 0644)
}
My suggested answers:
func ConvertCSVToJSON(csvPath, jsonPath string) error {
csvData, err := os.ReadFile(csvPath) // Raw CSV bytes
if err != nil {
return err
}
reader := csv.NewReader(bytes.NewReader(csvData)) // CSV parser
records := make([]map[string]string, 0) // Parsed CSV records
// ... processing logic to populate records
jsonData, err := json.Marshal(records) // JSON bytes
if err != nil {
return err
}
return os.WriteFile(jsonPath, jsonData, 0644)
}
The Naming Checklist
Before finalizing any name, ask yourself:
For Functions:
- ✅ Does the name clearly describe what the function does?
- ✅ Would a new team member understand the purpose immediately?
- ✅ Is it consistent with similar functions in the codebase?
- ✅ Does it use domain language that stakeholders understand?
For Variables:
- ✅ Does the name describe what the variable contains?
- ✅ Is the scope appropriate for the name length?
- ✅ Does it distinguish itself from similar variables?
- ✅ Would I understand this name in 6 months?
Key Takeaways
- Start with purpose: Always understand what you're naming before you name it
- Use domain language: Names should make sense to business stakeholders
- Be consistent: Similar operations should have similar naming patterns
- Context matters: Longer names for wider scopes, shorter for limited scopes
- Follow Go idioms: Embrace short names in limited scopes, use capitalization for visibility
- Iterate and improve: Don't be afraid to rename as your understanding evolves
Conclusion
Good naming isn't just about following conventions—it's about making your code tell a story. When you name things well, your code becomes self-documenting. Other developers (including future you) can read it like prose and understand not just what the code does, but why it exists.
The next time you're staring at your screen wondering what to call that function or variable, remember: good naming is thinking made visible. Take the time to understand the purpose, consider the context, and choose names that reveal your intent.
Your future self—and your teammates—will thank you.
What naming challenges do you face in your Go projects? Share your experiences in the comments below! 👇
Further Reading
Tags: #go #golang #coding #bestpractices #cleancode #naming #conventions
Top comments (0)