Yar Kravtsov's blog

Published at

Naming in Go: A Deep Dive into Function and Variable Conventions

Table of Contents

TL;DR

  • Go has specific naming conventions that enhance code readability and maintainability
  • Use PascalCase for exported identifiers, camelCase for unexported ones
  • Keep names short but descriptive, prioritizing clarity over brevity
  • Follow consistent patterns for different types (structs, methods, interfaces, etc.)
  • Handle acronyms in names according to Go’s specific conventions
  • Strive for self-documenting code through thoughtful naming

Introduction

As a Go developer, I’ve learned that one of the most underappreciated yet crucial aspects of writing clean, maintainable code is naming. It’s an art form that balances brevity with clarity, consistency with creativity. Today, we’re diving deep into the world of function and variable naming in Go. Why? Because great names can make your code sing, while poor ones can turn it into an unreadable mess.

I remember the first time I contributed to a large Go project. I thought I knew the language well, but my pull request came back with more comments about naming than actual logic! That experience taught me the importance of Go’s naming conventions, and I’m here to share those lessons with you.

The Go Way: Embracing Simplicity and Clarity

Before we dive into specific conventions, let’s talk about the philosophy behind Go’s approach to naming. Go, as a language, values simplicity and readability. This ethos extends to its naming conventions, which aim to make code self-documenting and easy to understand at a glance.

The Power of PascalCase and camelCase

In Go, we use two main casing styles:

  1. PascalCase: Used for exported (public) identifiers
  2. camelCase: Used for unexported (private) identifiers

This distinction is more than just a style choice – it’s baked into the language itself. Anything that starts with a capital letter is exported and visible outside the package, while lowercase-starting identifiers are package-private.

type User struct {  // Exported (public) type
    Name string     // Exported field
    age  int        // Unexported field
}

func NewUser(name string) *User {  // Exported function
    return &User{Name: name}
}

func (u *User) increaseAge() {  // Unexported method
    u.age++
}

This convention immediately tells other developers (and yourself) about the visibility and intended use of each identifier. It’s a small detail that makes a big difference in code readability.

Naming Conventions: A Closer Look

Let’s break down the naming conventions for different elements in Go code:

1. Structs: The Nouns of Your Code

Structs in Go are typically named using PascalCase nouns. They represent entities or concepts in your domain.

type Project struct {
    // fields
}

type WorkerPool struct {
    // fields
}

2. Methods: Actions for Your Types

Methods are functions associated with a type. In Go, we name them using PascalCase, starting with a verb that describes the action.

func (p *Project) Create() error {
    // implementation
}

func (wp *WorkerPool) Process(job Job) {
    // implementation
}

Notice how the method names are concise yet descriptive. They tell you exactly what the method does without being overly verbose.

3. Functions: Standalone Actions

Functions that aren’t methods follow a similar pattern to methods, using PascalCase. They often start with an action word or describe their purpose clearly.

func ParseConfig(filename string) (Config, error) {
    // implementation
}

func ValidateInput(input string) bool {
    // implementation
}

// For constructor-like functions, we often use "New" prefix
func NewHandler() *Handler {
    // implementation
}

Note how these function names clearly indicate their purpose, making the code more self-documenting.

4. Boolean Functions: Adjectives Without “Is”

When naming functions that return a boolean, Go convention suggests using adjectives without the “Is” prefix. This makes for more natural-sounding conditional statements.

func (j *Job) Completed() bool {
    // implementation
}

// Usage
if job.Completed() {
    // do something
}

Doesn’t that read more naturally than if job.IsCompleted()?

5. Variables: Short and Sweet

Go encourages short, clear variable names. This doesn’t mean using single-letter variables everywhere (except for very short-lived ones like loop indices), but rather finding a balance between brevity and clarity.

proj := &Project{}
repo := NewRepository()
ch := make(chan int)

These names are short enough to not clutter your code, but clear enough that you know what they represent.

6. Constants: Clarity Trumps Brevity

For constants, we use PascalCase for exported constants and camelCase for unexported ones. Unlike variables, constant names can be a bit longer to provide more context.

const (
    StatusPending   = "pending"
    MaxRetries      = 3
    taskTypeMerge   = "merge"
)

7. Interfaces: Actions with “-er” or occasionally “-or”

Interfaces in Go often describe behavior, so we name them with PascalCase, typically ending with “-er” for actions, though occasionally “-or” is used.

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

type Stringer interface {
    String() string
}

This naming convention makes it immediately clear that we’re dealing with an interface that defines a set of actions. While “-er” is more common, you might occasionally see “-or” in some standard library interfaces (like io.Reader or io.Writer).

The Art of Self-Documenting Code

Now that we’ve covered the basics, let’s talk about the real art of naming: making your code self-documenting. This means choosing names that explain what a function does or what a variable represents without needing additional comments.

Consider this example:

func (s *Service) ProcessItems(items []Item) error {
    for _, item := range items {
        if err := s.validateAndUpdateItem(item); err != nil {
            return err
        }
    }
    return nil
}

func (s *Service) validateAndUpdateItem(item Item) error {
    if !item.Valid() {
        return ErrInvalidItem
    }
    return s.repo.Update(item)
}

Without any comments, you can understand what this code does. The function names clearly describe their purpose, making the code easy to read and maintain.

Avoiding Common Pitfalls

In my journey as a Go developer, I’ve seen (and, admittedly, made) some common naming mistakes. Let’s look at a few:

  1. Redundancy: Avoid repeating the package name in your identifiers. For example, in a project package, project.CreateProject() is redundant. Just project.Create() is clearer.

  2. Acronym Casing: Go has specific conventions for handling acronyms in names. When an acronym is part of a name, it should be uniformly upper case or lower case, depending on whether the name starts with the acronym and whether it’s exported.

    • If the name begins with the acronym and is exported, use all caps for the acronym.
    • If the name doesn’t begin with the acronym or is unexported, use all lowercase for the acronym.

    Here are some examples:

    // Correct usage
    var userID string
    var httpSrv *http.Server
    
    func ServeHTTP(w http.ResponseWriter, r *http.Request) {}
    func newHTTPClient() *http.Client {}
    
    type XMLEncoder struct{}
    type xmlEncoder struct{}
    
    // Incorrect usage
    var userId string
    var HttpSrv *http.Server
    
    func ServeHttp(w http.ResponseWriter, r *http.Request) {}
    func newHttpClient() *http.Client {}
    
    type XmlEncoder struct{}
    type XMLencoder struct{}
    
  3. Overuse of Single-Letter Variables: While i for a loop index is fine, avoid using single letters for important variables.

    // Avoid
    u, err := GetUser(id)
    
    // Better
    user, err := GetUser(id)
    
  4. Inconsistent Naming Across Similar Concepts: Maintain consistency in your naming. If you have CreateUser, UpdateUser, and DeleteUser, don’t suddenly switch to RemoveCustomer.

Conclusion: The Impact of Good Naming

As we’ve explored, naming in Go is more than just following a set of rules. It’s about creating code that’s easy to read, understand, and maintain. Good naming can:

  • Reduce the need for comments
  • Make code reviews smoother
  • Decrease onboarding time for new team members
  • Improve overall code quality and maintainability

Remember, you’re not just writing code for the compiler – you’re writing it for other developers (including future you). Taking the time to choose good names is an investment that pays off in the long run.

So, the next time you’re about to name a function, variable, or type in Go, pause for a moment. Ask yourself: “Does this name clearly convey its purpose? Is it consistent with Go conventions and the rest of my codebase?” Your future self (and your teammates) will thank you.

  • Yar Kravtsov
    Name
    Yar Kravtsov
    About
    Senior Software Engineer
    Twitter
    @yarlson