Go Naming Conventions
Every language has a grammar, but Go also has a strong, mostly-unwritten style guide for naming things — and unlike many languages, some of that style is not just convention but is baked directly into the compiler. Whether an identifier starts with an uppercase or lowercase letter determines whether it is visible outside its package at all. Learning Go’s naming conventions well means learning both the compiler-enforced rules and the community idioms that make Go code instantly recognizable and easy to read across any codebase.
Overview: How Naming Works in Go
Go has no public, private, or protected keywords. Instead, visibility is determined entirely by the first letter (technically, the first Unicode code point) of an identifier’s name:
- An identifier that starts with an uppercase letter (
Balance,NewAccount,MaxRetries) is exported — visible to, and usable by, code in other packages that import this one. - An identifier that starts with a lowercase letter (
balance,newAccount,maxRetries) is unexported — visible only within the package that declares it.
This applies to everything: package-level variables, constants, functions, types, and struct fields. It is checked by the compiler, not just a linter — trying to reference an unexported identifier from another package is a compile error, full stop. This is why Go naming conventions matter more than in most languages: capitalization is not a style nicety, it is the access-control mechanism.
On top of that hard rule, Go’s community has converged on a consistent style, largely inherited from the standard library and formalized in documents like Go’s own code review comments:
- MixedCaps, not snake_case. Go uses
totalPriceandTotalPrice, nevertotal_priceorTOTAL_PRICE. This applies to variables, functions, types, and even constants — Go does not use SCREAMING_SNAKE_CASE for constants the way C or Python often do. - Short names for short scopes. A loop index is almost always called
i, an error almost always callederr, a boolean check almost always calledok. The shorter a variable’s lifetime and scope, the shorter its name should be — a variable used on the next two lines does not need a paragraph-long name. Package-level exported names, by contrast, should be more descriptive since they are read far from their declaration. - Package names are short, lowercase, and unadorned. No underscores, no MixedCaps, no
UtilsorCommongrab-bags. Standard library examples:strings,bytes,time,net/http. A package name becomes a prefix every caller types (strings.Split), so it should read naturally in that position. - Avoid stuttering. Because callers always prefix an exported identifier with the package name, you should not repeat the package name inside the identifier. The standard library’s
bytespackage exportsBuffer, notBytesBuffer— callers writebytes.Buffer, andbytes.BytesBufferwould be redundant. - Getters skip the "Get" prefix. A method that returns a struct field’s value is usually just named after the field: a field
balancegets an accessor method namedBalance(), notGetBalance(). Setters, when needed, do use aSetprefix (SetBalance), since there’s no shorter idiomatic alternative. - Single-method interfaces end in "-er". An interface with one method named
Readis calledReader; one methodWriteis calledWriter; one methodStringis calledStringer. This is why the standard library reads so consistently:io.Reader,io.Writer,io.Closer,fmt.Stringer. - Acronyms and initialisms keep consistent case. Write
ID,URL,HTTP,API— neverId,Url,Http. A method that serves HTTP requests isServeHTTP, notServeHttp. The rule is simple: an initialism is either all-uppercase or all-lowercase depending on whether the whole identifier is exported or not (userIDunexported,UserIDexported) — it is never partially capitalized. - Receiver names are short and consistent. A method receiver is typically a one- or two-letter abbreviation of the type name, reused identically across every method of that type —
func (a *Account) Deposit(...),func (a *Account) Balance() ..., notthis,self, or a different letter per method.
Syntax
There’s no special syntax for naming — these are conventions applied to ordinary Go declarations. The table below summarizes the rules for each kind of identifier.
| Identifier kind | Convention | Example |
|---|---|---|
| Exported (public) name | Starts uppercase, MixedCaps | MaxRetries, NewAccount |
| Unexported (private) name | Starts lowercase, mixedCaps | maxRetries, newAccount |
| Package name | Short, lowercase, no underscores | strings, http, json |
| Local variable, loop index | Very short, scope-proportional | i, err, ok, buf |
| Constant | MixedCaps, never SCREAMING_SNAKE | MaxConnections |
| Interface with one method | Method name + "-er" | Reader, Validator |
| Getter method | Field name, no "Get" prefix | Balance() not GetBalance() |
| Method receiver | 1-2 letter type abbreviation, consistent | (a *Account) |
| Acronym/initialism | Uniform case, never mixed | UserID, ServeHTTP |
Examples
The following examples show these conventions in real, compilable code.
Example 1: MixedCaps for variables, constants, and functions
package main
import "fmt"
const MaxRetries = 3
func calculateTotalPrice(basePrice float64, taxRate float64) float64 {
return basePrice + (basePrice * taxRate)
}
func main() {
itemPrice := 19.99
taxRate := 0.08
totalPrice := calculateTotalPrice(itemPrice, taxRate)
fmt.Printf("Total price: $%.2f\n", totalPrice)
fmt.Println("Max retries allowed:", MaxRetries)
}
Output:
Total price: $21.59
Max retries allowed: 3
Every identifier here follows Go’s default casing: the exported constant MaxRetries starts uppercase, the unexported function calculateTotalPrice and the local variables itemPrice, taxRate, and totalPrice all start lowercase and use MixedCaps rather than underscores. None of this affects how the program runs — it’s pure readability — but it means any Go developer can predict at a glance which names are meant to be used from outside this file’s package.
Example 2: Getter methods without a "Get" prefix
package main
import "fmt"
type Account struct {
Owner string
balance float64
}
func NewAccount(owner string, balance float64) *Account {
return &Account{Owner: owner, balance: balance}
}
func (a *Account) Balance() float64 {
return a.balance
}
func (a *Account) Deposit(amount float64) {
a.balance += amount
}
func main() {
acct := NewAccount("Priya", 100.0)
acct.Deposit(50.0)
fmt.Printf("%s's balance: $%.2f\n", acct.Owner, acct.Balance())
}
Output:
Priya's balance: $150.00
Account mixes an exported field, Owner, with an unexported field, balance — a common pattern when a struct wants some data freely readable/writable by callers and other data only mutable through controlled methods like Deposit. Note the accessor is named Balance(), matching the field name, rather than GetBalance(). Also note the constructor is named NewAccount, following the standard library convention that a package’s primary constructor for a type T is named NewT.
Example 3: Interface naming and acronym casing
package main
import "fmt"
type Validator interface {
Valid() bool
}
type UserRecord struct {
ID int
URL string
Email string
}
func (u UserRecord) Valid() bool {
return u.ID > 0 && u.Email != ""
}
func checkRecord(v Validator) {
if v.Valid() {
fmt.Println("record is valid")
} else {
fmt.Println("record is invalid")
}
}
func main() {
u := UserRecord{ID: 42, URL: "https://example.com", Email: "user@example.com"}
checkRecord(u)
}
Output:
record is valid
The single-method interface Validator is named after its one method, Valid, plus "-er" — mirroring io.Reader and fmt.Stringer from the standard library. UserRecord satisfies Validator implicitly: there is no implements keyword anywhere, the compiler simply notices that UserRecord has a method matching Valid() bool and allows it to be passed anywhere a Validator is expected. Notice also ID and URL: both are initialisms written fully uppercase, exactly as the standard library writes fields like ID on many types — never Id or Url.
How It Works Step by Step
Unlike most of Go’s style conventions, export visibility is not just a suggestion — it’s enforced mechanically:
- When you run
go build, the compiler processes each package and records, in its export data, every top-level identifier that starts with an uppercase letter: functions, types, variables, constants, and struct fields. - When another package imports this one, the compiler only allows references to those recorded exported names. Writing
otherpkg.someHelperwheresomeHelperis lowercase inotherpkgis a compile-time error — not a runtime failure and not something a linter has to catch after the fact. - Struct fields follow the same rule independently per field, which is why it’s common to see structs (like
Accountin Example 2) that mix exported and unexported fields — each field’s own capitalization controls its own visibility, regardless of the struct type’s visibility. - Everything else in this lesson — MixedCaps instead of snake_case, short local names, the "-er" interface suffix, avoiding "Get" prefixes — is convention only. The compiler accepts
total_priceorGetBalancejust fine. These rules exist purely so that any Go codebase reads consistently, and tools likegolint,staticcheck, andgo vetflag violations as style warnings, not compiler errors.
Common Mistakes
Mistake 1: Using snake_case or SCREAMING_SNAKE_CASE
Developers coming from Python, Ruby, or C often default to underscores. It compiles fine in Go, but it stands out immediately as non-idiomatic and every style checker will flag it.
// Wrong: not idiomatic Go
var user_name string
var Max_Retries = 3
func get_user_name() string {
return user_name
}
// Right: MixedCaps
var userName string
var MaxRetries = 3
func userNameValue() string {
return userName
}
Use MixedCaps everywhere — for locals, exported names, and even constants, where other languages would reach for all-caps.
Mistake 2: Stuttering package and type names
Because every exported identifier is always accessed with its package name as a prefix, repeating that package name inside the identifier just adds noise.
// Wrong: package "user", but every name repeats "User"
package user
type UserAccount struct {
UserName string
}
func NewUserAccount() *UserAccount {
return &UserAccount{}
}
// Callers must write user.UserAccount and user.NewUserAccount.
// Right: let the package name do the work
package user
type Account struct {
Name string
}
func New() *Account {
return &Account{}
}
// Callers write user.Account and user.New -- no repetition.
This is exactly why the standard library has http.Client, not http.HTTPClient, and json.Decoder, not json.JSONDecoder.
Mistake 3: "Get" prefixes and SCREAMING_SNAKE constants
// Wrong: Get prefix and all-caps constant
package main
const MAX_CONNECTIONS = 100
type Server struct {
port int
}
func (s *Server) GetPort() int {
return s.port
}
// Right: idiomatic Go
package main
const MaxConnections = 100
type Server struct {
port int
}
func (s *Server) Port() int {
return s.port
}
Both wrong versions compile without error — the compiler doesn’t care — but code reviewers and the go vet/staticcheck tooling most Go teams run will flag them, and readers coming from the standard library will trip over the inconsistency.
Best Practices
- Default to MixedCaps for every identifier; reserve underscores for filenames like
_test.go, not Go code itself. - Let scope size drive name length:
i,err, andokfor a few lines; full descriptive names for anything exported and read far from its declaration. - Keep package names short, lowercase, singular, and free of underscores — think
time, nottime_utilsorTimeUtils. - Never repeat the package name inside an exported identifier that already lives in that package (
user.Account, notuser.UserAccount). - Name getters after the field they expose (
Balance()), and reserve aSetprefix for setters (SetBalance). - Name single-method interfaces after the method plus "-er" (
Reader,Closer,Validator), matching the standard library’s pattern. - Keep acronyms uniformly cased — all-uppercase when exported (
ID,URL,HTTP), all-lowercase when unexported (id,url) — never mixed likeId. - Reuse the same short receiver name across every method of a type, and keep it to one or two letters.
- Run
go vetand a linter such asstaticcheckin CI — they catch naming violations the compiler happily ignores.
Practice Exercises
- Exercise 1: Rewrite this declaration in idiomatic Go style:
var user_email_address stringandfunc Get_User_Email() string. What should each be renamed to? - Exercise 2: You’re designing a package called
inventorythat exports a type for tracking stock. Name the type and its constructor function so that a caller writesinventory.New()and gets back an*inventory.Item— avoid stuttering. Then add a getter for an unexportedquantityfield, named correctly. - Exercise 3: Define an interface for a type with a single method
Close() error, following the standard library’s "-er" naming pattern. Then write a struct with a fieldapiURL string(unexported) and a fieldUserID int(exported), checking that both use correct, uniform acronym casing for "API" and "ID" respectively.
Summary
- Capitalization of the first letter is compiler-enforced: uppercase exports an identifier outside its package, lowercase keeps it private — there is no
public/privatekeyword. - Go uses MixedCaps everywhere, never snake_case or SCREAMING_SNAKE_CASE, even for constants.
- Package names are short, lowercase, and free of underscores; avoid repeating the package name inside identifiers it exports (no stuttering).
- Getters drop the "Get" prefix (
Balance(), notGetBalance()); setters keepSet(SetBalance). - Single-method interfaces are named after their method plus "-er" (
Reader,Validator) and are satisfied implicitly — noimplementskeyword needed. - Acronyms and initialisms keep one consistent case throughout an identifier:
ID,URL,HTTPwhen exported;id,url,httpwhen not. - Method receivers use a short, consistent one- or two-letter abbreviation of the type name across every method.
