The capital letter is the door
You open a pull request. The tests pass. The logic works. The CI pipeline turns red, not because of a bug, but because of a name. A reviewer comments: "Why is this MyVariable? Why not myVariable? And why did you use Get?" You've spent years in Python or JavaScript where naming is mostly style. In Go, naming is the interface. The capitalization of the first letter isn't a style choice; it's the only mechanism for visibility. Get it wrong, and your code is invisible. Get it right, and the compiler enforces your design.
Go has no public, private, or protected keywords. There is only capitalization. If a name starts with an uppercase letter, it is exported. Any other package can import your package and use that name. If a name starts with a lowercase letter, it is unexported. Only code inside the same package can see it. This is a hard rule enforced by the compiler. It removes ambiguity. You never have to guess if a function is internal or external. You look at the first letter. That's it.
Capitalization is the only visibility mechanism. The compiler trusts the first letter.
Capitalization controls visibility
Think of capitalization as a badge. An uppercase first letter is a badge that says "I am part of the public API." A lowercase first letter means "I am an implementation detail." This applies to everything: functions, variables, types, constants, and struct fields.
The convention aside here is simple: gofmt handles indentation and spacing, but it does not fix names. The community relies on go vet and human review for naming quality, but the compiler strictly enforces the capitalization rule. You cannot argue with the compiler about visibility. If you lowercase a name, it is private. Period.
Minimal example
package main
// Calculate computes the sum of two integers.
// The capital C makes this function exported.
// Other packages can call main.Calculate.
func Calculate(a, b int) int {
return a + b
}
// helper performs a secret adjustment.
// The lowercase h makes this function unexported.
// Only code inside package main can call helper.
func helper(x int) int {
return x * 2
}
func main() {
// This works because main is in the same package.
result := Calculate(1, helper(2))
println(result)
}
How the compiler enforces boundaries
When the compiler processes a package, it builds a symbol table. Every identifier gets tagged based on its first character. Uppercase symbols go into the exported set. Lowercase symbols stay local. If you try to reference an unexported name from outside the package, the compiler stops immediately.
The compiler rejects the program with undefined: package.unexportedName. This is not a warning. The binary is not built. This prevents accidental leaks of internal state. It also forces you to think about boundaries. If you need to expose something, you must decide to capitalize it. There is no accidental exposure.
The compiler enforces boundaries. You cannot leak internals by accident.
Realistic package structure
Let's look at a realistic scenario. You are building a user service. You have a struct for user data and a function to fetch it. Notice how the struct fields and methods follow the capitalization rule.
package users
// User represents a registered account.
// The capital U makes this type exported.
// External packages can declare variables of type users.User.
type User struct {
// ID is the unique identifier for the user.
// Capital I exports this field.
ID int `json:"id"`
// name is the display name.
// Lowercase n keeps this field unexported.
// Only the users package can read or write name.
name string
}
// GetUser retrieves a user by ID.
// Capital G exports this function.
func GetUser(id int) (User, error) {
// Simulate fetching data.
return User{
ID: id,
name: "Alice",
}, nil
}
// validateName checks if the name is acceptable.
// Lowercase v keeps this internal.
func validateName(n string) bool {
return len(n) > 0
}
Structs carry data. Methods carry behavior. Export what the world needs.
Beyond capitalization: the naming ecosystem
The capitalization rule is simple, but Go naming conventions go deeper. The standard library sets the tone, and the community follows it closely. Deviating from these conventions makes code harder to read for Go developers.
CamelCase, not snake_case
Go uses camelCase for all identifiers. Variables are configPath, not config_path. Functions are CalculateTotal, not calculate_total. This applies to both exported and unexported names. The compiler allows snake_case, but the community rejects it. If you write my_variable, you are signaling that you do not know Go. Stick to camelCase.
Interface naming
Single-method interfaces end in er. If a type has a method Read, the interface is Reader. If it has Write, the interface is Writer. This pattern extends to custom types. A method Serve implies a Server interface. This makes code predictable. When you see io.Reader, you know it has a Read method. You don't need to look at the definition. This reduces cognitive load.
Receiver naming
Methods attach to types via receivers. The receiver name should be short, usually one or two letters. For User, use u. For Buffer, use b. Never use this or self. Those are keywords in other languages, but in Go they are just names, and the community considers them noise. Short names keep the method signature focused on the action.
// Name returns the display name of the user.
// The receiver is u, short for User.
func (u *User) Name() string {
return u.name
}
Context as the first parameter
Functions that do I/O or long work need a context.Context. It must be the first argument. The name should be ctx. If you see a function with ctx as the first arg, you know it respects cancellation. If you see c or context, you might be looking at legacy code. Stick to ctx.
// FetchData retrieves data from the network.
// ctx is the first parameter, following convention.
func FetchData(ctx context.Context, url string) ([]byte, error) {
// Implementation respects ctx.Done() for cancellation.
return nil, nil
}
Error handling is verbose by design
Errors are values. Return them. Check them. if err != nil is the standard pattern. It is verbose. That is a feature. It forces you to handle errors explicitly. You cannot accidentally swallow an error. The compiler ensures you use return values. If you ignore an error, the compiler rejects the code with err declared and not used. You must assign it to _ or handle it. This design prevents silent failures.
The underscore discards intentionally
The underscore _ is a special identifier. It discards a value. When a function returns two values and you only need one, use _ for the one you ignore. result, _ := DoSomething(). This tells the reader you considered the second value and chose to drop it. Do not use _ for errors unless you have a specific reason. Ignoring errors silently is a bug waiting to happen.
Don't pass pointers to strings
Strings are immutable. They are cheap to pass by value. Passing a pointer *string adds indirection without benefit. The only reason to use *string is if the value can be nil to represent "no value". Even then, consider a different design. Naming a variable *string suggests you are optimizing prematurely or confusing Go with C. Stick to string.
Goroutine leaks and channel naming
Goroutine leaks often happen when a goroutine waits on a channel that never closes. Naming channels helps. done channel signals completion. quit channel signals cancellation. If you name a channel ch, it's ambiguous. Good names prevent bugs. If you see select { case <-done: ... }, you know the goroutine exits when done closes. If you see case <-ch:, you might not know what triggers it. Name channels for their purpose.
Accept interfaces, return structs
This mantra affects naming. When you return a struct, you return a concrete type. The caller can access exported fields. When you accept an interface, you accept behavior. The naming of the interface should reflect the behavior. Reader vs User. User is a thing. Reader is a capability. Naming distinguishes data from behavior.
Names are documentation. Write them for humans, not machines.
Pitfalls and compiler feedback
The capitalization rule is simple, but naming conventions go deeper. Go has strong opinions about how names should read.
Getter and setter prefixes
In Java, you write getName(). In Go, you write Name(). The compiler doesn't care, but the community does. If you name a method GetName, you are fighting the language. The method name should match the field name. The compiler won't stop you from writing GetName, but go vet might complain, and reviewers will flag it.
Acronyms
URL vs Url. Go standard library uses URL. XML vs Xml. Use URL. If you mix Url and URL, your code looks inconsistent. The compiler allows both, but consistency matters.
Accessing unexported fields
If you try to access an unexported field from another package, the compiler rejects it with users.User.name undefined (cannot refer to unexported field or method). This is a hard stop. You cannot work around it. You must add a method or export the field.
Unused imports
Forget to use an imported package and the compiler rejects the build with imported and not used. Go requires every import to be used. This keeps packages clean.
The compiler catches unused variables. You must catch bad names.
Decision matrix
Use an exported name when the identifier is part of the public API that other packages must call or reference. Use an unexported name when the identifier is an implementation detail that should not be visible outside the package. Use a capitalized method name matching a field when you need to read the field from outside the package, avoiding Get prefixes. Use a lowercase helper function when the logic is only needed within the current package to keep the public surface area small. Use ctx as the name for the first context.Context parameter to signal cancellation support to readers. Use short receiver names like u for User to keep method signatures clean and consistent with standard library style. Use camelCase for all identifiers to align with the entire Go ecosystem. Use er suffixes for single-method interfaces to make behavior predictable. Use _ to discard values intentionally, but never discard errors without a reason.
Follow the conventions. They make Go code readable across millions of lines.