Back Original

Go Naming Conventions: A Practical Guide

Choosing the right names in your codebase is an important (and sometimes difficult!) part of programming in Go. It's a small thing that makes a big difference — good names make your code clearer, more predictable, and easier to navigate; bad names do the opposite.

Go has fairly strong conventions — and a few hard rules — for naming things. In this post we're going to explain these rules and conventions, provide some practical tips, and demonstrate some examples of good and bad names in Go. If you're new to the language, all this information might feel like a lot to take in, but it'll quickly become second nature with a bit of practice 😊

Identifiers

Let's start with the hard rules for identifiers. By identifiers, I mean the names that you use for the variables, constants, types, functions, parameters, struct fields, methods and receivers in your code.

break        default      func         interface    select
case         defer        go           map          struct
chan         else         goto         package      switch
const        fallthrough  if           range        type
continue     for          import       return       var

So long as you stick to those three rules, any identifier name is technically valid and your code will compile all OK. But there are a bunch of other guidelines that it's good practice to follow:

Here are a few examples of good and bad identifier names:

Bad Reason Better
order.total := 99.99 Punctuation not allowed orderTotal := 99.99
const 3rdParty = "x" Cannot start with a digit const thirdParty = "x"
max_value := 10 Non-standard casing maxValue := 10
type HttpClient struct{} Inconsistent acronym casing type HTTPClient struct{}
func GetSessionId() ID should be all caps func GetSessionID()
résuméCount := 2 Non-ASCII letters resumeCount := 2
func clear() Clashes with builtin types or functions func clearQueue()
intCount := 42 Type included in name count := 42
type json struct{} Clashes with stdlib package names type payload struct{}

Exported and unexported identifiers

Identifiers in Go are case-sensitive. For example, the identifiers apiKey, apikey and APIKey are all different.

As you probably already know, when an identifier starts with a capital letter it is exported — that is, it's visible to code outside of the package it's declared in. This means that the casing of the first letter is significant. It impacts the behavior of your codebase. In turn, this means that you shouldn't start identifiers with a capital letter just because they look nice — you should only start them with a capital letter if you want them to be exported and accessible to code outside the package they are declared in.

As a tip, try to write packages using unexported identifiers by default. Only export them when you actually have a need to.

Typically, the less you export, the easier it is to refactor code within a package without affecting other parts of your codebase. There's a nice quote from The Pragmatic Programmer, which I'll adapt slightly for the Go nomenclature:

Write shy code - packages that don't reveal anything unnecessary to other packages and don't rely on other packages' implementations.

As a second tip, it's very rare for a main package to be imported by anything, so the identifiers in it should normally all be unexported and start with a lowercase letter. The most frequent exception to this is when you need to export a struct field so that it's visible to packages that use reflection to work, like encoding/json, encoding/gob or github.com/jmoiron/sqlx.

Identifier length and descriptiveness

In general, the further away that an identifier is used from where it is declared, the more descriptive the name should be.

If you have an identifier which is narrow in scope and only used close to where it is declared, it's generally OK to use a short and not-very-descriptive name. For example, if you're naming something that is only used in a small for loop, range block, or very short function, then using short or even single letter names is very common in Go.

But it you're naming something that has a larger scope, or is used far away from where it is declared, you should use a name that clearly describes what the thing represents.

Here is a nice example that Dave Cheney gave as part of his Practical Go presentation:

type Person struct {
    Name string
    Age  int
}

func AverageAge(people []Person) int {
    if len(people) == 0 {
        return 0
    }

    var count, sum int
    for _, p := range people {
        sum += p.Age
        count += 1
    }

    return sum / count
}

In this code, within the short range block we use the identifier p to represent a value in the people slice — the range block is so small and tight that using a single letter name is clear enough.

In contrast, the count and sum variables are declared, then used inside the range, then again in the return statement. Giving them more descriptive names makes it immediately clearer what the code is doing and what they represent, compared to single letter names like c and s. But these variables are only ever used inside the AverageAge function, so giving them even-more-descriptive names like peopleCount and agesSum would be unnecessarily verbose.

It's not an exact science, but when writing Go code you are encouraged to use the right length identifier — sometimes that might be long and descriptive, sometimes it might be short and terse.

Naming packages

The hard rules for package names are the same as for identifiers: they can contain unicode letters, numbers and underscores, must not begin with a number, and must not be a Go keyword. But in practice, the conventions for naming a package are much tighter. Conventionally:

Bad Reason Better
package 3rdparty Cannot start with a digit package thirdparty
package OrderManager Non-standard casing / separators package ordermanager
package o Too vague and not descriptive package orders
package ordermanagementsystem Too long / hard to type package orders
package url Clashes with stdlib package names package links
package _cache Ignored by Go tooling package cache
package internal Special directory names in Go package internalauth
package utils Catch-all names with unclear scope package validation

Naming files

In an ideal world, a .go filename should summarize what the file contains, be one word long, and all in lowercase. Some examples of good filenames from the standard library net/http package are cookie.go, server.go and status.go.

If you can't think of a good one-word name, and want to use two or more words, there is no clear convention for how those words should be separated. Even in the Go standard library itself there isn't consistency. Sometimes underscores are used to separate the words in filenames (like routing_index.go and routing_tree.go), and other times they are concatenated with nothing between them (like batchcursor.go, textreader.go and reverseproxy.go).

Because there isn't a strong convention around this, I recommend just picking one of these two approaches and sticking to it consistently within a codebase. Personally, I think it's better to concatenate words with nothing between them (like routingindex.go), and reserve the underscore character for only when you want to use a special filename suffix.

Talking of which, there are some filename prefixes and suffixes that have a special meaning in Go. You should avoid using these in your filenames unless you want to trigger the special behavior. Specifically:

Avoiding chatter

When you are naming exported functions, try to avoid repeating the name of the package they are declared in.

For example, if you have a package called customer, then function names like NewCustomer() or CustomerOrders() would be 'chattery' and unnecessarily repeat the word 'customer' when you call them from outside the package — like customer.NewCustomer() and customer.CustomerOrders(). Calling the functions New() and Orders() is sufficient and reads better at the call site — like customer.New() and customer.Orders().

The same advice also applies to exported types. For example, if you want to represent an address or phone number in a customer package, it's sufficient and less chattery to name the types Address and PhoneNumber rather than CustomerAddress and CustomerPhoneNumber.

Bad Reason Better
customer.NewCustomer() Chattery function call customer.New()
customer.CustomerAddress Chattery type reference customer.Address

Similar to function and type names, method names should ideally not 'chatter' too much when calling them. For example, if you are writing methods on a Token type, for example, it's probably OK to call a method Validate() rather than ValidateToken(), or IsExpired() rather than IsTokenExpired().

Method receivers

When you are creating methods, it is conventional for the method receiver to have a short name, normally between 1 and 3 characters long and often an abbreviation of the type that the method is implemented on. For example, if you are implementing a method on a Customer type, an idiomatic receiver name would be something like c or cus. Or if you were implementing a method on a HighScore type, a good receiver name would be hs.

The Go code review comments advise against using generic names like this, self or me for the receiver.

Also, you should be consistent with the receiver name. All methods on the same type should use the same receiver name — don't use c for one method and cus for another.

type Order struct {
    Items int
}

// Good: uses a short receiver 
func (o *Order) Validate() bool {
    return o.Items > 0
}

// Bad: uses a longer receiver name
func (order *Order) Validate() bool {
    return order.Items > 0
}

// Bad: uses a generic receiver name
func (self *Order) Validate() bool {
    return self.Items > 0
}

Getter and setter methods on structs

Typically, it is not necessary to create 'getter' and 'setter' methods on struct types in Go. Instead, you just access the struct field directly to read or change the data. The major exception to this is when you have a struct with an unexported field, but want to provide a way to get or set the field value from outside the package. To do this, you need to create exported 'getter' and 'setter' methods that read and write to the unexported field.

When doing this, it is conventional to prefix the setter method name with Set, but not prefix the getter method name with Get. Like so:

type Customer struct {
    address string
}

func (c *Customer) Address() string {
    return c.address
}

func (c *Customer) SetAddress(addr string)  {
    c.address = addr
}

Interfaces

By convention, interfaces that only contain one method should be named by the method name plus an '-er' suffix or similar. For example:

type Speaker interface {
    Speak() string
}

type Authorizer interface {
    Authorize(ctx context.Context, action string) error
}

type Authenticator interface {
    Authenticate(ctx context.Context) (User, error)
}

The Go standard library has quite a few examples of interfaces that follow this convention, such as io.Reader, io.Writer and fmt.Stringer (and also some, like http.Handler, which don't).

Also note that the guidance to avoid including the type in the name still applies to interfaces. Don't give your interfaces names like UserInterface or OrderInterface unless you really can't think of a decent alternative.

Breaking from conventions

There are rare occasions when breaking a convention can actually make your code clearer — and in my view, it can be OK to do that... especially if it's in a private codebase worked on by a small team.

For example, a couple of years ago I was working on a Go program that synchronizes data between some other external systems. In this project, I ended up breaking some of the Go naming conventions around casing and separators — instead using exactly the same identifiers that the external systems used. This actually made the intent of the program clearer, and more immediately obvious what was being synchronized with what.

But the vast majority of the time, you should endeavour to follow the naming rules and conventions we've discussed in this post. They exist for good reasons: they make your code more predictable and consistent, easier for other Gophers to quickly understand, and reduce the risk of certain bugs.