How to Name Things in Go

Conventions and Best Practices

Use TitleCase for exported identifiers and lowercase for unexported ones to control visibility in Go.

Naming things in Go

You open a Go file and the first thing that hits you is the silence. No public. No private. No class. No interface keyword on the struct. Just type Buffer struct. And then the methods. func (b *Buffer) Read(...). It looks like C, but the naming tells a different story. The capitalization is doing heavy lifting. The receiver name b is a whisper of convention. If you come from Java or C#, your muscle memory screams for getters and setters. Go laughs at getters. Naming in Go is not just style. It shapes your architecture. The name itself declares the API boundary.

Capitalization controls visibility

Go uses capitalization to control visibility. Uppercase is exported. Lowercase is unexported. That is the rule. The compiler enforces it. There are no keywords. This means the name itself declares the API boundary. If you name a field data, only this package touches it. If you name it Data, the world sees it. This forces a discipline. You cannot accidentally expose internals. You have to choose.

Here is the core rule in action: capitalization controls visibility, and receiver names match the type.

package main

// Buffer is exported because it starts with a capital letter.
// Any package that imports this package can create a Buffer.
type Buffer struct {
	// data is unexported. Only code inside this package can read or write it.
	data []byte
}

// Write appends bytes to the buffer.
// The receiver name b is short and matches the type Buffer.
func (b *Buffer) Write(p []byte) int {
	b.data = append(b.data, p...)
	return len(p)
}

// reset clears the buffer.
// It is unexported because it starts with a lowercase letter.
func (b *Buffer) reset() {
	b.data = b.data[:0]
}

The compiler scans the first character. Buffer is exported. reset is not. Try to call reset from another package and the compiler rejects it with buf.reset undefined (type Buffer has no field or method reset). The error is precise. It points to the boundary.

The receiver name b is a convention. The compiler does not care. You could write func (self *Buffer) Read(...). The code compiles. The code review fails. The community expects one or two letters. b for Buffer. s for Server. r for Reader. This keeps the method body clean. You do not want self.buf cluttering every line. b.buf is faster to read. The receiver name is a promise to the reader, not the compiler.

Capitalization is the boundary. Respect it.

Realistic patterns

Naming extends beyond structs and methods. Error variables, context parameters, and discarded values follow strict conventions. These patterns reduce cognitive load. When you see ctx, you know it is a context. When you see err, you know it is an error. Deviation causes friction.

Here is a service struct showing error naming, context handling, and the underscore discard.

package service

import (
	"errors"
)

// ErrNotFound indicates the requested resource does not exist.
// Export error variables so callers can compare them with errors.Is.
var ErrNotFound = errors.New("resource not found")

// Service coordinates requests against the storage backend.
type Service struct {
	store *Store
}

// NewService creates a Service instance.
// Constructor functions often start with New and return a pointer.
func NewService(store *Store) *Service {
	return &Service{store: store}
}

The error variable ErrNotFound starts with Err. This is the standard prefix for exported error values. Callers check for specific errors using errors.Is(err, service.ErrNotFound). If the error is only used internally, name it errNotFound and keep it unexported.

Constructor functions follow a pattern. NewService creates a Service. It returns a pointer because the struct likely holds state. If the struct is small and immutable, returning a value is acceptable. The name New signals allocation and initialization.

Here is a method demonstrating context and the underscore.

// Fetch retrieves data respecting cancellation.
// Context is always the first parameter, named ctx.
func (s *Service) Fetch(ctx context.Context, id string) (*Result, error) {
	// Check for cancellation before doing work.
	if err := ctx.Err(); err != nil {
		return nil, err
	}

	res, err := s.store.Find(id)
	if err != nil {
		// Propagate errors explicitly.
		// The compiler forces you to handle or return every error.
		return nil, err
	}

	return res, nil
}

// parseID extracts the ID from a raw string.
// Using the underscore discards the second return value intentionally.
func parseID(raw string) string {
	id, _ := splitRaw(raw)
	return id
}

The parameter ctx is always the first argument. This is a hard convention. Functions that take a context must respect cancellation and deadlines. The variable name is always ctx. Never context or c. The community expects ctx.

The underscore _ discards a value. id, _ := splitRaw(raw) says "I considered the second return value and chose to drop it". Use this sparingly with errors. Dropping an error with _ hides a potential failure. The compiler allows it, but linters often warn. If you drop an error, you must have a reason. Usually, you do not.

Context is plumbing. Name it ctx and pass it first.

Pitfalls and anti-patterns

Naming mistakes in Go rarely cause compiler errors. They cause code review pain. The compiler allows bad names. The community does not.

The biggest trap is writing getters. If you have a struct User with a field Name, you access u.Name. You do not write a method GetName(). Go prefers direct access. If you write GetName(), you are adding indirection for no reason. If you need logic, name the method by the result. FullName() combines first and last. Age() calculates from birthdate. But GetName() just returns the field. Delete it. The compiler will not stop you, but go vet might warn, and your peers will ask why.

Boolean methods drop Is. isValid becomes Valid. HasPermission becomes Can or Has. u.Valid() is clearer than u.IsValid(). The return type is bool. The name implies the check.

Package names are lowercase. No underscores. path/filepath. The import path can be long. The package name is short. This reduces noise. filepath.Join is better than file_path.Join. If you name your package my_awesome_service, every call becomes my_awesome_service.DoWork. That is painful. Pick a short name. service. store. db. The package name should match the directory name. gofmt does not enforce package names, but the tooling ecosystem expects this alignment.

Receiver names like this or self are forbidden by convention. The compiler accepts them. The code review rejects them. Use one or two letters matching the type. b for Buffer. s for Service. r for Reader. If the type name is long, use the first letter. c for Connection. m for Manager.

Variable names depend on scope. In a loop, i is perfect. In a function spanning 50 lines, index is better. Go encourages short names for short scopes. err is always err. ctx is always ctx. t is always *testing.T. These are universal. Deviating causes friction.

If you try to access an unexported field from another package, the compiler rejects it with cannot refer to unexported field or method name. If you try to call a method that does not exist, you get undefined: GetName. If you import a package and do not use it, the compiler rejects the file with imported and not used. Go is strict about unused imports. This keeps dependencies clean.

Do not write getters. Access the field.

Decision matrix

Naming in Go is a series of choices. Use the right pattern for the situation.

Use TitleCase when the identifier must be visible to other packages.

Use lowercase when the identifier is an implementation detail of the current package.

Use a short receiver name matching the type when defining methods.

Use ctx as the variable name for context.Context parameters.

Use err for error variables.

Use NewType for constructor functions that return a pointer.

Use plain field access when no logic is needed; skip getter methods entirely.

Use package names that are lowercase, singular, and concise.

Use the er suffix for single-method interfaces like Reader or Writer.

Use Err prefix for exported error variables.

Use the underscore to discard values you have considered and intentionally dropped.

Naming is a contract. Keep it simple.

Where to go next