Skip to content
gohacks.dev
Basics

Go Naming Conventions That the Whole Ecosystem Follows

A lookup table of Go naming rules for packages, files, types, constants, acronyms, getters, receivers, errors and tests, with the standard library as proof.

5 min read
Summarize in AI
Flat illustration of an editor window full of code.

This is the reference to keep open while you write Go. None of these rules is checked by the compiler, except that a leading capital exports a name, and all of them are followed by the standard library and by nearly every library you will import. Following them is how your code reads like everyone else's. The rules are listed first, then the cases people get wrong, and the program at the end uses each one. The reasoning for packages is in package naming, and the wider start is the Go basics path.

Tested with Go 1.25.6, macOS on Apple Silicon (arm64)

The table

ThingRuleExample
PackageShort, lowercase, no underscores, singularbytes, user, httputil
Source fileLowercase, underscores allowed, words by purposeuser_store.go, server.go
Test file_test.go suffixuser_store_test.go
Platform file_GOOS, _GOARCH or both before .gosock_linux.go, asm_arm64.go
Exported nameStarts with an uppercase letter, MixedCapsParseConfig, MaxRetries
Unexported nameStarts with a lowercase letter, mixedCapsparseConfig, maxRetries
AcronymsOne case throughoutID, URL, HTTP; userID, ServeHTTP, xmlParser
ConstantsMixedCaps, not ALL_CAPSDefaultTimeout, maxSize
GetterThe field name, no Getu.Owner()
SetterSet plus the field nameu.SetOwner(x)
One-method interfaceMethod name plus erReader, Writer, Stringer
ReceiverOne or two letters from the type, same in every methodfunc (s *Server), func (c *Cache)
Local variableShort in a short scope, longer in a long onei, n, buf; pendingRequests
Error variableErr prefixErrNotFound, io.EOF is the old exception
Error typeError suffix*PathError, ParseError
Error stringLowercase, no final punctuation"open config: file not found"
Test functionTestName, BenchmarkName, ExampleName, FuzzNameTestParseConfig
Type parameterShort uppercase, T, K, Vfunc Map[T, U any](...)
Build taggo:build line, lowercase//go:build linux && amd64

The ones people get wrong

Acronyms

userId and ServeHttp look natural to people who learned in other languages. In Go an initialism stays in one case: all capitals when exported or at the start of a word, all lowercase when the word begins an unexported name. So userID, ID, ServeHTTP, httpClient, parseURL, HTMLEscape. The standard library has a handful of old exceptions, and the rule for new code is firm. Linters such as revive and staticcheck flag the wrong form.

Getters

Fields that need an accessor use the field's own name for the getter. owner the field and Owner() the method, with SetOwner() for the setter if one exists. A GetOwner method adds a word that carries no information, and it is the habit most often carried over from Java. The Get prefix is fine where it means "go and fetch", as in an HTTP Get or a cache Get(key), because the operation is not a field read.

Receivers

A receiver name is a short abbreviation of the type, the same in every method of the type: s for Server, c for Cache, rw for ReadWriter. Not self, not this, and not different letters in different methods. It stays short because the receiver appears in nearly every line of a method body, and a long name would swamp the logic.

Underscores

Do not use them in Go identifiers, with the exception of test function names that include a subtest style like TestParse_empty, which some teams allow, and generated code. Constants are MaxRetries and not MAX_RETRIES. File names may contain underscores, and a file name suffix of _test, _linux and so on has meaning to the toolchain.

Variable name length

The rule is proportional: the bigger the scope, the longer the name. A loop index in a three-line loop is i. A parameter of a short function is n or s. A package-level variable is a full word. A variable that lives across fifty lines deserves a name that reminds the reader what it is. The idioms that every Go reader knows are err, ok, ctx, i, j, n, b for bytes or a benchmark, t for a test, r for a reader, w for a writer, buf, s for a string, m for a map.

Errors

Sentinel values are var ErrNotFound = errors.New("not found"). Custom error types end in Error, and the pointer to a struct, *PathError, is the usual shape. The string of an error is lowercase and unpunctuated, because it is meant to be joined to others in a chain, "open /etc/app.toml: permission denied", and a capital letter or full stop in the middle of a chain reads wrong. Error message style in Go goes through the cases.

A program that follows them

This program uses most of the rows above, so you can read the rules back against real code:

main.goGo
package main

import (
	"errors"
	"fmt"
)

const DefaultLimit = 3

var ErrLimitReached = errors.New("limit reached")

type Account struct {
	owner  string
	userID int
	items  []string
}

func NewAccount(owner string, userID int) *Account {
	return &Account{owner: owner, userID: userID}
}

func (a *Account) Owner() string     { return a.owner }
func (a *Account) SetOwner(o string) { a.owner = o }
func (a *Account) UserID() int       { return a.userID }

func (a *Account) Add(item string) error {
	if len(a.items) >= DefaultLimit {
		return fmt.Errorf("add %q: %w", item, ErrLimitReached)
	}
	a.items = append(a.items, item)
	return nil
}

type Describer interface {
	Describe() string
}

func (a *Account) Describe() string {
	return fmt.Sprintf("%s (id %d) holds %d items", a.owner, a.userID, len(a.items))
}

func main() {
	a := NewAccount("ada", 7)
	for _, it := range []string{"a", "b", "c", "d"} {
		if err := a.Add(it); err != nil {
			fmt.Println(err, errors.Is(err, ErrLimitReached))
		}
	}
	var d Describer = a
	fmt.Println(d.Describe(), a.Owner())
}
Text
add "d": limit reached true
ada (id 7) holds 3 items ada

Read it with the table beside it: the exported constant and error, userID and UserID with the acronym in one case, getters with no Get, the setter with Set, a receiver letter that is the same in every method, a one-method interface named Describer after its method, and an error string that is lowercase and punctuation-free so that the wrapped message reads as one line.

What is not on this list, and why

Naming in tests has more room: table-driven test case names are free-form strings, and helper functions in _test.go files follow ordinary rules. Generated code, such as protocol buffer output, follows its generator's conventions and is exempt from your linter. Names inside a package that mirrors an external schema, a JSON field user_id or a column created_at, belong in struct tags, not in the Go identifier: the field is UserID and the tag is json:"user_id". And there are conventions about names that are really about design, such as avoiding stutter or generic names, which the package naming article and Effective Go treat in depth. The authoritative short list is the Go Code Review Comments, and gofmt will fix your layout but not your names, so run a linter for those.

Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.