Middle Go preparation

Middle Go Backend Interview Questions

15 selected Middle Go interview questions for backend developers who need to explain practical trade-offs, concurrency, and service behavior.

Start a Middle Go AI InterviewNo credit card required. 1 free session available.
Technical interview practice in EnglishA mode where non-native speakers can practice passing technical interviews.

Type System

1Explain how nil works differently for pointers, slices, maps, channels, functions, and interfaces in Go.

In Go, nil is the zero value for pointers, slices, maps, channels, functions, and interfaces, but operations on those nil values differ by type. A nil pointer can be compared to nil, but dereferencing it panics. A nil slice has length and capacity 0 and can be ranged over and appended to. A nil map can be read from and ranged over, but assigning to it panics. Sending to or receiving from a nil channel blocks forever, and closing a nil channel panics. Calling a nil function panics. An interface is nil only when it has no dynamic type and no dynamic value; an interface holding a typed nil value, such as a nil pointer, is not itself nil.

Try answering this question with an AI coach

2What are comparable types in Go, and how do comparability rules affect map keys, equality, and generics constraints?

Comparable types in Go are types whose values can be compared with `==` and `!=`. Basic types, pointers, channels, interfaces, and structs/arrays whose fields or elements are comparable are comparable; slices, maps, and functions are not comparable except to `nil`. Map keys must be comparable. Equality follows the type's comparison rules, and interface comparison depends on the dynamic concrete values; if a compared interface contains an incomparable dynamic value, the comparison panics. In generics, the predeclared `comparable` constraint allows type parameters to be compared with `==`/`!=` and used as map keys.

Try answering this question with an AI coach

3How does Go represent bytes, runes, and UTF-8 encoded text, and why can len(s) differ from the number of user-visible characters?

In Go, `byte` is an alias for `uint8` and represents one raw byte, while `rune` is an alias for `int32` and represents a Unicode code point. A `string` is a read-only sequence of bytes, commonly UTF-8 encoded text but able to contain arbitrary bytes. `len(s)` returns the number of bytes, not runes or user-visible characters. Indexing a string returns a byte; ranging over a string decodes UTF-8 and yields byte indexes plus runes. `len(s)` can differ from the number of visible characters because UTF-8 may use multiple bytes per code point and because one user-visible character may be composed of multiple code points, such as combining marks or emoji sequences.

Try answering this question with an AI coach

4How do type aliases differ from defined types, and when would you use each?

A defined type, such as `type UserID int64`, creates a new distinct type with `int64` as its underlying type. It is not freely assignable to `int64` without conversion and can have its own methods. A type alias, such as `type UserID = int64`, is just another name for the same type, so type identity and assignability are preserved. Use defined types for domain modeling, type safety, and methods; use aliases mainly for refactoring, migration, or compatibility without introducing a new type.

Try answering this question with an AI coach

5How does Go treat floating-point special values and what equality pitfalls matter in backend systems?

Go `float32` and `float64` use IEEE-754-style behavior, including special values such as positive/negative infinity and NaN. For `float64`, helpers include `math.Inf`, `math.IsInf`, `math.NaN`, and `math.IsNaN`. NaN is not equal to anything, including itself, so `x == x` is false when `x` is NaN. Exact equality on computed floats is also risky because rounding and precision can make mathematically equal values differ; use domain-appropriate tolerances or avoid floats for exact business values like money. Floats are allowed as map keys, but NaN keys are problematic because map lookup depends on equality and NaN does not compare equal, even to itself.

Try answering this question with an AI coach

6Describe struct embedding in Go and how promoted fields and methods behave.

Struct embedding in Go means declaring a field by its type without an explicit field name, for example `type User struct { Person }`. The embedded value is still a real field, accessible as `u.Person`, but its exported/accessible fields and methods may be promoted so callers can write selectors like `u.Name` or `u.Greet()` as shorthand for going through the embedded field. Embedding is composition, not classical inheritance: the outer type is not automatically a subtype of the embedded type. If promoted selectors conflict, Go does not guess; ambiguous names must be qualified or are not selectable through the outer value.

Try answering this question with an AI coach

Data Structures

7Describe how slice reslicing and assignment can cause multiple slices to share the same underlying array, and what bugs this can create.

A slice value is a header pointing into an underlying array. Assigning a slice or passing it to a function copies only that header, not the elements. Reslicing creates another header pointing to a range of the same backing array. Therefore multiple slices can alias the same storage: changing an element through one slice can be visible through another, and appending to one slice can overwrite data visible to another if it still has spare capacity. Bugs include surprising mutations, corrupted results, retaining large backing arrays through small subslices, and data races when aliases are used concurrently. To avoid unintended sharing, make a defensive copy with copy or append([]T(nil), s...), or limit capacity with a full-slice expression before append.

Try answering this question with an AI coach

8Explain slice growth during append at a conceptual level and the performance implications of repeated reallocation.

When append adds elements to a slice, it writes into the existing backing array if the slice has enough capacity. If capacity is insufficient, Go allocates a larger backing array, copies the existing elements, writes the new elements, and returns a slice header pointing to the new storage. The exact growth policy is implementation-dependent, but conceptually capacity grows enough to make repeated append amortized efficient. Repeated reallocations still cost CPU for copying, create allocations, increase GC pressure, and may break sharing with old slice aliases. If you know the expected size, preallocate with make([]T, 0, n) when building by append or make([]T, n) when filling by index to reduce reallocations.

Try answering this question with an AI coach

Packages

9What are init-time registration patterns in Go, and what risks come with blank-import side effects and global registries?

An init-time registration pattern is where a package registers an implementation with a shared registry from an `init` function. A blank import such as `_ "example.com/driver"` is often used to import a package only for its side effects, causing its `init` function to run even though no exported names are referenced. This is common for driver, codec, plugin, metrics, or serializer extension points. The risks are hidden dependencies and startup side effects, global mutable state, duplicate or order-sensitive registration, harder test isolation, and less explicit dependency wiring. It should be used deliberately, documented clearly, and often mitigated with explicit registration, idempotent/concurrency-safe registries, or injectable/resettable registries for tests.

Try answering this question with an AI coach

Error Handling

10How do defer, panic, and named return values interact when implementing cleanup that may modify returned errors?

Deferred functions run after return values have been assigned but before the function returns to its caller. Because of that, a deferred closure can read or modify named return values such as a named `err`. This is commonly used to add cleanup errors from `Close`, `Commit`, or similar operations to the error being returned, ideally preserving the primary error rather than overwriting it. During panic unwinding, deferred functions still run; a deferred function may recover and set a named return value, but that should be limited to intentional panic boundaries. Be careful not to shadow a named return variable such as `err`, because the defer may then observe or modify a different variable than intended.

Try answering this question with an AI coach

11How do errors.Is, errors.As, and %w work in Go error wrapping chains?

`fmt.Errorf` with `%w` creates a new error that wraps an underlying error while adding context. Wrappers expose underlying errors through `Unwrap`, forming a chain or tree that the standard library can inspect. `errors.Is(err, target)` checks whether `err` or anything it wraps matches a target error. `errors.As(err, &target)` checks whether `err` or anything it wraps is assignable to the target type and stores the matched value in the provided pointer.

Try answering this question with an AI coach

12What are sentinel errors, and what are the tradeoffs versus custom typed errors or richer domain error models?

A sentinel error is a named error value, often a package-level variable such as `var ErrNotFound = errors.New("not found")`, used to represent a specific condition that callers can test, typically with `errors.Is` when wrapping is possible. Sentinels are simple and useful for broad stable categories, but exported sentinels become part of the API and can couple callers to particular values. Custom typed errors can carry structured fields and be found with `errors.As`. Richer domain error models classify failures by kind/code and may include safe messages or metadata, which is useful when callers need stable behavior beyond one fixed error value.

Try answering this question with an AI coach

13How should a Go backend map internal errors to useful client responses while still giving operators diagnosable signals?

A Go backend should translate internal errors at an application or transport boundary into stable, client-safe categories and responses. Those categories should map to appropriate HTTP status codes or equivalent transport statuses, with safe messages and machine-readable codes rather than raw internal errors. Operators should still get diagnostic information through structured logs, traces, metrics, correlation/request IDs, and preserved underlying causes. Logging is usually best done once at a boundary that has request context, to avoid both missing failures and noisy duplicate logs.

Try answering this question with an AI coach

14How do you implement custom error types correctly in Go, and how does errors.Join affect inspection and cleanup-path error handling?

A custom Go error type satisfies `error` by implementing `Error() string`. It can also carry structured fields and implement `Unwrap() error` to expose an underlying cause. Pointer versus value receivers matter: a pointer receiver means only `*T` satisfies `error`, while a value receiver usually means both `T` and `*T` do, which affects copying and the type callers should use with `errors.As`. `errors.Join` combines multiple errors into one error; `errors.Is` and `errors.As` inspect the joined children. This is useful when both a primary operation error and a cleanup/deferred error should be returned without losing either failure.

Try answering this question with an AI coach

Concurrency

15How does select work with channels, including ready-case choice, default cases, and cancellable operations?

select waits on multiple channel operations and runs one case whose send or receive can proceed. If no channel case is ready, it blocks unless there is a default case; default runs immediately only when no channel operation can proceed, which is useful for non-blocking send/receive attempts. If multiple cases are ready, Go chooses one pseudo-randomly rather than by source order. Cancellable channel operations commonly add a case receiving from ctx.Done() so the goroutine can stop waiting when the context is cancelled or times out.

Try answering this question with an AI coach