DEV Community

Cover image for Go Naming: Why Getters Drop the Get Prefix (and Other Idioms)
Gabriel Anhaia
Gabriel Anhaia

Posted on

Go Naming: Why Getters Drop the Get Prefix (and Other Idioms)


You write a Go struct with a private field and a method to read it.
Muscle memory from Java, C#, or PHP takes over, and you type
GetName(). The code compiles. Tests pass. Then a reviewer leaves a
comment that just says "drop the Get" with no explanation, and you're
left wondering whether that's a real rule or one person's taste.

It's a real rule. It's written into the language's own style guide,
the standard library follows it everywhere, and several linters will
flag the code that breaks it. Go naming isn't a matter of opinion the
way it is in some languages. A handful of conventions are baked into
the tooling, and once you know them, half the reviewer nitpicks
disappear.

Getters drop the Get

The convention comes straight from the Effective Go
document. If you have an unexported field owner, the accessor is
named Owner, not GetName or GetOwner. The mutator, if you need
one, keeps the Set prefix.

type File struct {
    owner string
}

func (f *File) Owner() string   { return f.owner }
func (f *File) SetOwner(o string) { f.owner = o }
Enter fullscreen mode Exit fullscreen mode

The reasoning is about how the call reads. person.Name() says the
same thing as GetName() with less noise, and the parentheses already
tell you it's a method call. Get adds a word that carries no
information in a language where field access and method calls look
different anyway.

The Set prefix stays because there's no other clean way to signal
mutation. Name() reads, SetName(x) writes. The asymmetry is
deliberate.

This shows up all over the standard library. bytes.Buffer has
Len(), not GetLen(). sync.Once has no getters to get wrong, but
http.Request exposes fields and methods that never carry a Get.
time.Time has Hour(), Minute(), Second(). The pattern is
consistent enough that GetHour() would look foreign in any Go file.

MixedCaps, not underscores

Go uses MixedCaps or mixedCaps for multi-word names. Not
snake_case, not SCREAMING_SNAKE for constants. The first letter's
case controls export: capital means exported, lowercase means package
private. That's the whole rule, and it collapses two decisions
(visibility and word separation) that other languages keep apart.

const maxRetries = 3          // unexported constant
const MaxConnections = 100    // exported constant

var userCount int             // unexported
type HTTPServer struct{}      // exported
Enter fullscreen mode Exit fullscreen mode

Constants are the place people slip, because the C and Java habit is
MAX_RETRIES. In Go that's wrong twice: wrong word separator and, if
you meant it to be package-visible, the all-caps doesn't make it any
more exported than MaxRetries would. Case alone decides export, so
SCREAMING_SNAKE gains you nothing and costs you the idiom.

revive and staticcheck both carry checks for this. The
var-naming rule in revive flags underscores in names and
all-caps constants, and it's on in most golangci-lint presets.

Initialisms stay all one case

Here's the one that trips up almost everyone. When a name contains an
initialism like URL, ID, HTTP, or API, that initialism keeps a single
case throughout. It's URL, not Url. It's userID, not userId.
It's ServeHTTP, not ServeHttp.

type Client struct {
    baseURL string
    apiKey  string
    id      string
}

func (c *Client) UserID() string    { return c.id }
func parseHTMLResponse(r *http.Response) {}
Enter fullscreen mode Exit fullscreen mode

The rule lives in the Go team's code review comments:
"Words in names that are initialisms or acronyms have a consistent
case." So an exported name is ServeHTTP (all caps) and an
unexported one is xmlHTTPRequest if it starts lowercase, but the
HTTP part stays uppercase either way.

When two initialisms collide, you get names that look strange but are
correct: xmlHTTPRequest, parseURLID. Ugly, but consistent, and
the linter wants it that way.

staticcheck ships this as check ST1003, and revive's
var-naming enforces a list of known initialisms. If you write
userId, staticcheck will tell you to make it userID. This is
the single most common naming lint I see fire on code coming from
other languages.

The table below is worth memorizing:

You might type Go wants
Url, url mid-name URL, URL
Id, userId ID, userID
Http, Https HTTP, HTTPS
Api API
Json JSON
Uuid UUID

Receiver names: short and consistent

Go method receivers get their own convention, and it runs opposite to
almost every other naming rule you know. Receivers should be short,
usually one or two letters, and derived from the type name. Not this,
not self, not a descriptive word.

func (c *Client) Do(r *Request) {}
func (b *Buffer) Write(p []byte) {}
func (s *Server) Start() {}
Enter fullscreen mode Exit fullscreen mode

Client becomes c. Buffer becomes b. The receiver is not the
place for a long name because it appears on every method of the type,
and the reader already knows what type they're looking at from the
signature. A one-letter receiver reads as pure plumbing, which is
exactly what it is.

Two sub-rules travel with this. First, be consistent across all
methods of a type. If one method uses c *Client, every method uses
c *Client. Mixing c and cl and client across a type's methods
is a lint finding under revive's receiver-naming check.

Second, don't name a receiver this or self. Go isn't trying to
mimic object-oriented syntax from other languages, and those names
signal that the author is thinking in a different language's model.

// avoid
func (this *Client) Do(r *Request) {}
func (self *Server) Start() {}
Enter fullscreen mode Exit fullscreen mode

The code review comments
page is explicit: "Don't name result parameters just to avoid
declaring a var... The name of a receiver should be consistent across
a type's methods." this and self are called out by name as things
to avoid.

Package names shape everything above them

One more that changes how the rest reads: package names are short,
lowercase, single words, no underscores, no mixedCaps. And the
package name is part of every identifier you export from it, so you
name to avoid stutter.

package http

// good: http.Server, http.Client
// bad:  http.HTTPServer, http.HTTPClient
Enter fullscreen mode Exit fullscreen mode

A type named Server in package http is referenced as
http.Server. If you named it HTTPServer, callers would write
http.HTTPServer, and the HTTP repeats. The convention is to let
the package qualify the name. bytes.Buffer, not bytes.ByteBuffer.
bufio.Reader, not bufio.BufReader.

revive has a package-comments check and a stutter-detecting
check. There's no single staticcheck code for package stutter, so
revive is what flags it here. The practical upshot: pick a good
package name first,
then name the types inside it as if the package name is already
attached.

Wiring the linters up

None of this is enforced by go build or go vet by default. The
compiler does not care whether you write Url or URL. The rules
live in linters, and the two that matter are staticcheck (via the
ST1xxx style checks) and revive (the modern replacement for the
now-archived golint).

The easiest path is golangci-lint, which bundles both:

# .golangci.yml
linters:
  enable:
    - staticcheck
    - revive
Enter fullscreen mode Exit fullscreen mode

Then run it:

golangci-lint run ./...
Enter fullscreen mode Exit fullscreen mode

staticcheck alone catches the initialism and Get-prefix issues
out of the box. revive adds the receiver-consistency and
underscore checks. Turn them on once at the repo level and the naming
review comments stop being something a human has to remember.

Why any of this matters

Go's naming rules are narrow on purpose. There aren't many of them,
and the ones that exist are mechanical enough that a linter can enforce
them. That's the point. In languages with looser conventions, naming
becomes a per-team debate that never fully resolves. Go moved that
debate into tooling, so a file written by a stranger reads like a file
written by you.

Drop the Get. Keep URL all caps. Give receivers one letter and
keep them consistent. Let the package name do half the work. Wire up
staticcheck and revive so you stop thinking about it. The whole
system is designed so that idiomatic Go looks the same no matter who
typed it, and these conventions are most of how it gets there.


Naming is the surface layer of a deeper habit: writing Go that reads
the way the standard library reads. The Complete Guide to Go
Programming
goes through the language's conventions alongside the
runtime and stdlib internals that explain why they exist, and
Hexagonal Architecture in Go is where naming meets structure —
keeping these idioms consistent across the ports and adapters of a
real service, so the boundary code reads as cleanly as the core.

Thinking in Go — the 2-book series on Go programming and hexagonal architecture

Top comments (0)