Idioms & Best Practices

intermediate go testing idioms

This is the capstone note — a grab-bag of idiomatic Go. None of it is hard, but knowing these proverbs by name is the difference between “I write Go” and “I write idiomatic Go” in an interview.

In simple language: Go has a strong culture of “the right way to do things,” and a lot of it is captured in short proverbs.

Accept interfaces, return structs

When writing a function, take the most general type we can (an interface), but return a concrete type (a struct).

Why? Accepting an interface lets callers pass anything that fits — easy to test, easy to swap. Returning a concrete struct gives callers the full, specific type with all its methods, instead of a stripped-down interface.

// Good: accepts any io.Reader, returns a concrete *Parser
func NewParser(r io.Reader) *Parser {
	return &Parser{src: r}
}

The standard library does this everywhere — io.Reader in, concrete types out.

Keep interfaces small

The best Go interfaces are tiny — one or two methods. io.Reader is one method (Read). Small interfaces are easy to implement, easy to mock, and compose well.

A related proverb: “The bigger the interface, the weaker the abstraction.” If our interface has ten methods, it’s not really an abstraction, it’s a wishlist. Define interfaces where they’re used (the consumer), not where the type is defined.

Errors are values — handle them, don’t panic

Go has no exceptions. Functions return an error as their last value, and we check it right there. This is verbose but explicit — we always know exactly where things can fail.

f, err := os.Open("config.json")
if err != nil {
	return fmt.Errorf("opening config: %w", err) // wrap with context, return up
}
defer f.Close()

We wrap errors with %w to add context as they bubble up, and the caller can unwrap them with errors.Is / errors.As. The proverb: “Don’t just check errors, handle them gracefully.”

panic is reserved for truly unrecoverable situations (programmer bugs, impossible states) — not for normal control flow. In simple language: a missing file is an error; indexing past the end of a slice is a panic. Don’t panic for things we expect to happen.

Make the zero value useful

When we design a struct, try to make it work straight out of the box with no constructor — its zero value (all fields at their defaults) should be usable.

The classic examples: sync.Mutex is ready to lock the moment it’s declared, and bytes.Buffer is ready to write to. No New() needed.

var mu sync.Mutex   // zero value is an unlocked, ready-to-use mutex
mu.Lock()           // works immediately, no initialization

If users have to call Init() before our type does anything, we’ve made their life harder. Aim for “declare and go.”

Prefer composition over inheritance

Go has no class inheritance. Instead we embed types — drop one struct inside another and its methods get promoted up. We build big things out of small things rather than extending a base class.

type Logger struct{ prefix string }
func (l Logger) Log(msg string) { /* ... */ }

type Server struct {
	Logger // embedded — Server now has Log() for free
	port int
}

Server gets Log() automatically. This is “composition,” and it’s the only inheritance-like mechanism Go gives us — by design.

A little copying beats a little dependency

A famous Go proverb. If pulling in a whole package just to use one tiny helper, it’s often cleaner to copy that handful of lines into our code. Fewer dependencies means less to break, less to audit, less version hell.

It’s not about being lazy — it’s about not coupling our whole project to a library for the sake of ten lines.

Naming: short, no stutter

  • Short names for short scopes — i, r, buf are fine inside a small function.
  • Exported names start with a capital letter; unexported with lowercase. That’s the entire visibility system.
  • No stutter — it’s http.Server, never http.HTTPServer, because callers already wrote http.. Same idea: user.New() over user.NewUser().

And finally — gofmt everything

There’s one canonical format and gofmt enforces it, so we never argue about braces or spaces. “Gofmt’s style is no one’s favorite, yet gofmt is everyone’s favorite.” Just run it and move on.

Interview soundbite

Idiomatic Go in a nutshell: accept interfaces and return structs, keep interfaces tiny (the bigger the interface, the weaker the abstraction), and define them at the consumer. Errors are values we handle and wrap with %wpanic is only for unrecoverable bugs, not control flow. Design types so the zero value is useful, prefer composition via embedding over inheritance, remember “a little copying is better than a little dependency,” name things short with no stutter, and gofmt everything. It’s all in Effective Go.