Last Updated: May 22, 2026
Go has strong opinions about how code should look and behave. Those opinions are collected in the official "Effective Go" document, and they're what makes a random Go file from any team feel familiar. This chapter surveys the conventions that matter most: naming, formatting, comments, control flow, error handling, interfaces, composition, zero values, and concurrency. Later chapters in this section go deeper on each topic.
Most languages have a style guide that's a suggestion. Go's conventions are closer to laws. The compiler rejects unused imports and unused local variables. gofmt rewrites your code into a single canonical layout. The standard library models naming, error handling, and interface design, and every popular Go project follows the same patterns.
The payoff is that Go code reads the same way no matter who wrote it. You can drop into an unfamiliar codebase and the function signatures, the error checks, and the package names all look like what you'd write yourself. That uniformity is worth more than any individual rule.
This chapter doesn't repeat the language spec. It's a tour of the idioms that separate Go code from "code that happens to compile as Go".
Names in Go are short, descriptive, and tied to scope. The wider the scope, the more meaningful the name has to be. A loop counter named i is fine. A package-level variable named i is not.
Package names are short, lowercase, and a single word. No underscores, no mixedCaps, no plurals.
Avoid grab-bag names like util, common, helpers, or misc. They don't tell the reader anything, and they encourage code to drift into them. If you find yourself reaching for util, the code probably belongs in a more specific package, or as a method on an existing type.
The package name is part of the public API. Callers write cart.Add, not cartpkg.Add. So pick a name that reads well at the call site. The standard library is full of good examples: bytes.Buffer, time.Now, http.Get. The package name and the exported identifier together form a phrase.
Go doesn't follow the Java convention of prefixing accessors with Get. If a struct has an owner field and you want to expose it, the getter is called Owner, not GetOwner.
Order.Owner() reads as a noun, like a field access with a function call. Order.GetOwner() reads like Java code that wandered into the wrong file.
A one-method interface is named for the method, with -er appended. Read becomes Reader. Write becomes Writer. Close becomes Closer. The standard library is full of these: io.Reader, io.Writer, fmt.Stringer, sort.Interface.
When the suffix doesn't fit (because the verb already ends in -er, or because the interface has multiple methods), pick a noun that describes what the interface represents. http.Handler, sort.Interface, and error are all examples.
Go uses MixedCaps for exported names and mixedCaps for unexported ones. Underscores in identifiers are reserved for generated code and a small number of special cases.
The case of the first letter decides whether a name is exported. TotalPrice is visible from other packages. totalPrice is not. There's no public or private keyword in Go, and there doesn't need to be.
Short scope, short name. Long scope, descriptive name.
p works inside the loop because the scope is tiny. A package-level variable that lives for the program's lifetime needs a name that survives without context: defaultCustomerTaxRate, not r.
gofmt is the official formatter, and it's the end of the discussion. Every Go file goes through it. Editors run it on save. CI rejects code that hasn't been formatted.
What gofmt decides:
func main() { is one line, not two.a + b, not a+b. Composite literals like Product{Name: "Pen", Price: 1.49} get one space after the colon.Run gofmt -w. (or use goimports, which also adjusts the import block) and stop thinking about formatting. The minutes you save not arguing about tabs versus spaces add up over a career.
gofmt runs in milliseconds even on large files. There's no reason to skip it. Hook it into your editor save action.
Go has two comment styles: // line comment and /* block comment */. Use the line style for almost everything.
A doc comment is a comment that sits directly above a top-level declaration (a function, type, variable, or constant) with no blank line between the comment and the declaration. The go doc command and pkg.go.dev pull these out as documentation.
A few rules that show up everywhere:
// Add inserts..., not // Adds a product.... This convention makes generated docs read consistently.// SetName sets the name. adds nothing. Either describe the constraints and edge cases, or skip the comment.For exported identifiers, doc comments are basically required. For unexported helpers, they're optional but useful when the function isn't self-explanatory.
Go's grammar uses semicolons as statement terminators, but you almost never type them. The lexer inserts a semicolon at the end of any line that ends with an identifier, a literal, certain keywords (break, continue, fallthrough, return), or a closing bracket. This is why the opening brace of a function has to stay on the same line as the signature.
You won't write semicolons by hand outside of for loops, where the three-clause form uses them explicitly: for i := 0; i < n; i++ {... }. The lesson is small but important: Go's whitespace matters, and the rules aren't arbitrary.
Go's control structures look familiar but have a few twists that make idiomatic code shorter than the equivalent C or Java.
if can declare a variable in its header. The variable lives only for the duration of the if (and any attached else).
This pattern is the standard way to call a function that returns a value and an error: scope the variables to the conditional, handle the error, and move on. Variables that only exist for one check don't bleed into the surrounding code.
When an if ends with return, drop the else. It saves a level of indentation and reads more naturally.
Both compile. The first version is what is in the standard library and in most Go code. Guard clauses with early returns keep the happy path on the left margin.
Go has one loop keyword. It covers C-style three-clause loops, while-style loops, infinite loops, and range loops over collections.
One keyword, four shapes. The grammar is regular, and you don't need to remember which keyword pairs with which loop style.
When an operation can fail or return a "not present" answer, Go uses a second boolean return. The two main places this appears are map lookups and type assertions.
Using prices["eraser"] without the ok returns the zero value (0.0 for float64) when the key is missing, which is often indistinguishable from a real zero. The comma-ok form tells the difference.
When a value's type can be one of several things (most often through an interface), use a type switch. It's switch with the type assertion baked in.
Inside each case, x has the case's static type. No additional assertion needed. Type switches show up in formatters, serializers, and anywhere an interface{} has to be unpacked.
Errors in Go are values. They're returned from functions like any other value, and they're checked explicitly. There's no exception system, no try/catch. The pattern is the foundation of Go's error model.
Three rules drive idiomatic error handling.
Return errors, don't panic. panic is for genuinely unrecoverable situations: programmer bugs, corrupted invariants, impossible states. Network failures, invalid input, missing files, none of those are panic-worthy. Return an error and let the caller decide.
Wrap context as you go. When you receive an error from a deeper call and want to add information, use fmt.Errorf with the %w verb. This wraps the original error so callers can still inspect it.
%w preserves the underlying error, so errors.Is(err, ErrOutOfStock) returns true even though the message has been wrapped. If you don't need the wrap relationship, use %v or %s instead. The choice is intentional: %w is "I want callers to be able to detect this specific error", %v is "I want a readable message".
Sentinel errors and `errors.Is`. A sentinel error is a package-level variable that callers compare against. Standard library examples include io.EOF, sql.ErrNoRows, and os.ErrNotExist. Define your own with errors.New and document them.
For richer error data (like a status code or a failed field name), define an error type with a Error() string method and use errors.As to extract the concrete type.
fmt.Errorf with %w allocates a small wrapper. In hot paths that run millions of times per second, that adds up. For typical request handling, the cost is invisible and the debugging value is huge.
Go interfaces are satisfied implicitly. A type doesn't declare which interfaces it implements; the compiler checks at the use site. If a type has the right methods, it satisfies the interface.
EmailNotifier doesn't say "I implement Notifier" anywhere. It just has a Notify method with the right signature, and that's enough. This decoupling is what makes Go interfaces composable. You can define interfaces in the package that consumes them, even for types you don't own.
Two rules of thumb separate good Go interfaces from cluttered ones.
Keep interfaces small. The smaller the interface, the easier it is to satisfy and the more flexible the code becomes. io.Reader is one method. io.Writer is one method. fmt.Stringer is one method. The standard library's most-used interfaces all have one or two methods. A ten-method interface is usually a sign that the abstraction is wrong.
Accept interfaces, return structs. Functions take interface parameters when they want flexibility about what's passed in. Functions return concrete types so callers get all the methods and don't have to type-assert.
There are exceptions. If your factory function might one day return different concrete types, an interface return makes sense. But the default is concrete, and you only widen to an interface when you have a real reason.
Go has no classes and no inheritance. Instead, types compose through embedding: a struct can embed another struct (or an interface) by listing its type without a field name, and the embedded type's methods become callable on the outer struct.
Embedding gives method promotion without inheritance's coupling. Customer isn't a subclass of Address; it just contains one and exposes its methods directly. If you want to override, define the method on the outer type and Go uses that one instead.
Embedding interfaces is equally common. The standard library's io.ReadWriter is literally:
This is the whole definition. Anything that satisfies both Reader and Writer satisfies ReadWriter automatically. Composition over inheritance, in 3 lines.
Avoid embedding for "code reuse" the way inheritance is used in other languages. Embed when the outer type genuinely "has-a" or "is-a" the inner type in a way that makes the inner type's methods part of the outer API.
Every Go type has a zero value, and the language initializes variables to it. int starts at 0. string starts at "". Pointers, slices, maps, channels, interfaces, and function values all start at nil. Structs start at a struct of zero values.
The idiom Go pushes hard is design types so the zero value is useful. A sync.Mutex is usable as soon as you declare it; no constructor needed. A bytes.Buffer writes to an empty buffer without setup. A nil slice can be appended to.
When you design your own types, ask whether var x MyType should work. If the answer is yes, the type is more pleasant to use. When it can't (because there's required configuration), provide a constructor named NewMyType and document that callers must use it.
Map and channel types are exceptions. A nil map can be read (returns zero values) but writing to it panics. A nil channel blocks forever on send and receive. For those, the constructor (or make) is required for writes.
A useful zero value means no allocation up front. Compare var buf bytes.Buffer (zero allocations) with buf := &bytes.Buffer{} or buf := new(bytes.Buffer) (one allocation each). For high-frequency code, the difference adds up.
Concurrency is a chapter unto itself, and the Concurrency section of this course covers goroutines, channels, the GMP scheduler, and synchronization in detail. The idioms below are the ones is referenced everywhere.
Share by communicating. The Go proverb is "Do not communicate by sharing memory; instead, share memory by communicating." Translated: prefer passing data through channels over locking shared state. Channels make ownership explicit; locks make it implicit and error-prone.
The order data passes through the orders channel. The goroutine owns each order while processing it. No mutex, no shared state.
Don't expose channels in APIs unnecessarily. A channel in a function signature commits you to a concurrency model the caller might not want. If you're writing a function that returns a list of results, return a slice or a range-over-func iterator, not a channel. Reserve channels for genuinely concurrent producer/consumer patterns where the laziness or back-pressure matters.
Each goroutine has an owner. Whoever starts a goroutine is responsible for making sure it stops. Goroutines that hang because their channel is never closed, or block on a send no one will receive, are leaks. Use context.Context for cancellation in any goroutine that might outlive its caller.
The diagram shows two paths a worker goroutine can take: it either watches ctx.Done() and exits when the caller cancels, or it ignores the context and leaks until the program exits. The discipline is to always go through the green path.
A short example that exercises most of what this chapter covered. A cart package exposes a Cart type with a useful zero value, returns errors instead of panicking, accepts a small interface for pricing, and uses idiomatic naming throughout.
Every convention from this chapter shows up: short package-style naming, no Get prefix on accessors, a small interface named after its method, errors returned instead of panicked, sentinel error checked with errors.Is, a useful zero value for Cart, and a concrete return type from Total. None of it is clever. That's the point.