Idiomatic Go is remarkably uniform because the community agreed on conventions and let tools enforce them. This guide covers the ones that matter most day to day, so your code reads like everyone else’s.
Let gofmt decide formatting
Formatting is not a matter of taste in Go. gofmt (run via go fmt) rewrites
your code into the one canonical style: tabs for indentation, aligned struct
fields, sorted imports. Configure your editor to run it on save and never argue
about braces or spacing again.
Naming: MixedCaps and short names
Go uses MixedCaps or mixedCaps, never snake_case, for multi-word names.
const MaxRetries = 3
type HTTPClient struct {
baseURL string
timeout int
}
Prefer short names, especially for short-lived locals: i for a loop index,
r for a reader, buf for a buffer. Longer names earn their length only when
the scope is large. Keep initialisms all-caps: HTTPClient and userID, not
HttpClient or userId.
Exported vs unexported
Visibility is controlled by the first letter, not a keyword. A capitalised identifier is exported (visible to other packages); a lowercase one is private to its package.
// Message is exported: other packages can call it.
func Message(name string) string {
return "Hello, " + name
}
// normalize is unexported: usable only inside this package.
func normalize(name string) string {
return name
}
Export the smallest surface that is useful. Everything public is a promise you have to keep compatible.
Doc comments
A doc comment is a regular comment directly above a declaration, and by
convention it begins with the name being documented. go doc and pkg.go.dev
turn these into documentation automatically.
// Message returns a greeting for name. It never returns an empty string.
func Message(name string) string {
return "Hello, " + name
}
Error handling style
Return errors as the last value, check them immediately, and add context when
you pass them up with %w so callers can still inspect the original.
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read config %s: %w", path, err)
}
Do not ignore errors with _ unless you truly mean to, and handle the error
where you have the context to do something useful with it.
Accept interfaces, return structs
A widely used guideline: functions should accept interfaces (so callers can pass any implementation) but return concrete types (so callers get the full, discoverable API).
// Accepts any io.Reader; returns a concrete *Scanner.
func NewScanner(r io.Reader) *Scanner {
return &Scanner{source: r}
}
This keeps your inputs flexible and your outputs honest. Define interfaces where they are consumed, not where the concrete type is defined.
Package naming
Package names are short, lowercase, single words, and describe what they
provide, not what they contain: time, http, greeting. Avoid utils or
common grab-bags, and avoid stutter — greeting.Message, not
greeting.GreetingMessage.
Summary
- Let
gofmtown formatting; enforce it in CI. - Use MixedCaps and short names; keep initialisms all-caps.
- Control visibility with capitalisation, document exported names, and add
context to errors with
%w.
Put these into practice with a clean Go project structure, and see the full path in the Go developer roadmap.