skip to content

Idioms and API Design

Go leaves most design decisions to convention rather than to the compiler, so an interview asks you to defend the name, the constructor and the exported surface you chose, and the ones you did not.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

explore

questions

87 · 5 sections

Go has no `extends` keyword - how do you reuse another type's behaviour inside a struct?

level: juniorimportance: must knowfreq 78%
basics
~20 s

Go gives three choices: embed the type anonymously so its methods are promoted onto yours, hold it in a named field and forward only the calls you want, or depend on an interface it already satisfies.

open as a page

Why does os.Open return (*os.File, error) instead of throwing an exception when the file is missing?

level: juniorimportance: must knowfreq 88%
basics
~20 s

In Go a failure is an ordinary return value of the predeclared error interface type. os.Open hands back a file and an error together, so the caller checks it on the spot and every failure path stays visible.

open as a page

When a Go function returns a non-nil error, what may the caller assume about its other return values?

level: middleimportance: must knowfreq 68%
basics
~20 s

Nothing useful, unless the documentation says otherwise. By convention the other results are their zero values on failure, so a returned pointer is nil. Using one before returning is the classic nil dereference panic in Go code.

open as a page

In Go, why is `func Handle[T any](v T)` no more capable than `func Handle(v any)`?

level: middleimportance: must knowfreq 60%
basics
~20 s

A type parameter constrained by any permits no operations — not even ==. With T appearing in only one position it links nothing to nothing, so the generic form does exactly what the plain any parameter does, with more syntax.

open as a page

Why does Go make every numeric conversion explicit, and what does that still not catch?

level: middleimportance: must knowfreq 62%
basics
~20 s

Go performs no implicit numeric conversion: mixing an int with an int64, or an int with a time.Duration, is a compile error until you write the conversion. But the conversion you wrote can still truncate or lose precision silently.

open as a page

Why does Go write a multi-word identifier as maxRetryCount rather than max_retry_count?

level: juniorimportance: must knowfreq 72%
basics
~20 s

Go's convention is MixedCaps: each following word is capitalised and underscores are dropped. Case is already meaningful, since the first letter decides package visibility, so it doubles as the word separator. Underscores belong to file names.

open as a page

Why should a Go doc comment on an exported function begin with the function's name?

level: juniorimportance: must knowfreq 60%
basics
~20 s

Go's documentation tools lift the comment away from the declaration and show it on its own, so the sentence has to name its own subject. Starting with the identifier also makes the first sentence a self-contained one-line summary in package listings.

open as a page

In Go, why is the success path never put inside the `else` of an `if err != nil` check?

level: juniorimportance: must knowfreq 68%
basics
~20 s

Idiomatic Go handles the failure inside the if and returns, leaving the success path unindented below it. Because Go checks an error after nearly every call, putting each success in an else pushes the real work rightward.

open as a page

In Go, why is a type named http.HTTPClient worse than http.Client?

level: juniorimportance: must knowfreq 58%
basics
~20 s

Every reference outside the package already carries the package name, so http.HTTPClient says HTTP twice at the call site. Go names a type for how it reads after the qualifier, so inside package http the type is simply Client.

open as a page

Why is Go's handler method named ServeHTTP rather than ServeHttp, and how is an initialism cased in an unexported name?

level: middleimportance: must knowfreq 62%
basics
~10 s

Go keeps an initialism in one uniform case, all upper or all lower, never title case: ServeHTTP, URL, userID. In an unexported name a leading initialism goes fully lowercase, as in xmlHTTPClient.

open as a page

How should a Go constructor New(cfg Config) supply defaults for fields the caller left at zero?

level: juniorimportance: must knowfreq 62%
basics
~20 s

New takes the Config by value, replaces each field still at its zero value with a named package default, and builds the object from that copy. The caller's struct is untouched, and every caller gets identical defaults.

open as a page

Why do Go libraries write `New(addr string, opts ...Option)` instead of one parameter per setting?

level: juniorimportance: must knowfreq 62%
basics
~20 s

Because callers pass only the settings they care about. Option is a function type such as func(*Client); each With... helper returns one, and New starts from a struct holding the package defaults and applies the options to it.

open as a page

In Go, what does it mean for a type to have a useful zero value, and why do package authors design for one?

level: juniorimportance: must knowfreq 70%
basics
~20 s

A type has a useful zero value when var x T is ready to use with no constructor call. bytes.Buffer, sync.Mutex and http.Client all work this way, so callers can declare or embed them and start calling methods immediately.

open as a page

In a Go Config struct, how do you tell a field the caller never set from one deliberately set to zero?

level: middleimportance: must knowfreq 55%
basics
~10 s

Make the field a pointer, such as *int or *time.Duration. A nil pointer means the caller said nothing, so the constructor's default applies; a non-nil pointer to zero means they really asked for zero.

open as a page

One Config struct feeds both NewWorker and NewPublisher — where should defaulting and validation live?

level: middleimportance: should knowfreq 42%
basics
~20 s

Put both on the Config itself: a withDefaults method returning a filled copy, and a validate method returning an error. Every constructor calls them in that order, once, so validation judges the values that will actually be used.

open as a page

Why does a Go package usually export a blocking function instead of starting a goroutine for the caller?

level: juniorimportance: must knowfreq 55%
basics
~20 s

A caller can always add concurrency to a blocking function by starting it with go, but cannot remove concurrency the package started. Staying synchronous leaves the goroutine's lifetime, its errors and how many run in the caller's hands.

open as a page

In Go, what makes an identifier visible outside its package, and what does an unexported struct field mean for importing packages?

level: juniorimportance: must knowfreq 85%
basics
~20 s

Case decides it. An identifier whose name begins with an upper-case letter is exported and visible to packages that import it; a lower-case one is package-private. Importing packages cannot read, set, or even name an unexported struct field.

open as a page

Where do ctx context.Context and the error result belong in an exported Go function's signature?

level: juniorimportance: must knowfreq 74%
basics
~20 s

A context.Context parameter goes first and is named ctx; the error result goes last. Everything else sits between them, with any variadic parameter at the end, because that is the shape the standard library and every reviewer expect.

open as a page

How does a Go service struct get its collaborators when main wires the program by hand?

level: juniorimportance: must knowfreq 68%
basics
~20 s

Through its constructor's parameters. main creates each collaborator first and passes them to a function such as NewThumbnailer, which stores them in unexported struct fields, so the type never reaches outside itself for a dependency.

open as a page

Why does adding a method to an exported Go interface break importers, when adding one to a struct does not?

level: middleimportance: must knowfreq 55%
basics
~20 s

An exported Go interface is satisfied implicitly, so any type with the right method set implements it. Adding a method enlarges that set, and every outside implementer stops compiling. A method on a struct affects only that type.

open as a page

In Go, why do reviewers reject an exported interface declared beside its only implementation?

level: juniorimportance: must knowfreq 72%
basics
~20 s

Go checks interface satisfaction implicitly, so whoever needs the abstraction can declare it later. An interface written beside its only implementation adds indirection now, hides the concrete type's other methods and fields, and buys nothing.

open as a page

Why does Go compile a call whose returned error you never check?

level: juniorimportance: must knowfreq 72%
basics
~20 s

Go's compiler rejects unused variables and unused imports, but not unused return values. A call like f.Close() is a complete statement on its own, so nothing forces you to look at the error it returns.

open as a page

What is wrong with a package-level `var apiURL = os.Getenv("API_URL")` in a Go package?

level: juniorimportance: must knowfreq 68%
basics
~20 s

It runs at package initialization, before main, so nothing can supply or check the value: the package cannot report a missing setting as an error, and a test can only change it by writing to a global.

open as a page

Why is `defer f.Close()` on a file you just wrote an error-swallowing bug?

level: middleimportance: must knowfreq 58%
basics
~20 s

A deferred call's return value is thrown away, so defer f.Close() discards the error reporting a failed write. Closing the file also does not flush a bufio.Writer above it, so buffered bytes are never written and the run still reports success.

open as a page

What does an exported Go function lose by taking a parameter of type any?

level: middleimportance: should knowfreq 45%
basics
~20 s

An any parameter gives up the compile-time contract. The signature no longer says what is accepted, so the rule lives in a doc comment and a runtime type switch, and a caller's wrong type fails in production.

open as a page