1. Core Language (Recap)
Deepen core fluency: receivers, interfaces, slices/maps, generics, and type design.
Question: When should you use a pointer receiver versus a value receiver, and what are the trade-offs?
Answer: Use a pointer receiver (*T
) when a method needs to modify the receiver's state, or when the receiver is a large struct that would be expensive to copy. Use a value receiver (T
) when the method doesn't need to modify the receiver and the receiver is small. For consistency, if any method on a type has a pointer receiver, all methods on that type should have pointer receivers.
Explanation: Passing arguments by value is the default in Go. A value receiver operates on a copy of the original data, ensuring the original value is not mutated. A pointer receiver operates on a reference to the original data, allowing for mutation and avoiding the performance cost of copying large structs.
Question: How does implicit interface satisfaction work, and what design principles does it encourage?
Answer: A type satisfies an interface if it implements all the methods defined by that interface. There is no explicit implements
keyword. This is known as implicit satisfaction or structural typing.
Explanation: This design encourages small, focused interfaces and decoupling. Consumers of an API can define interfaces that describe only the behavior they need, even for types from external packages, without modifying the original source code. This aligns with the interface segregation principle.
Question: Explain the relationship between a slice's length, capacity, and its backing array.
Answer: A slice is a lightweight descriptor containing a pointer to an underlying array, a length (number of elements in the slice), and a capacity (number of elements in the array from the slice's start).
Explanation: Multiple slices can share the same backing array. Modifying elements in one slice can affect another if they share data. When you append
to a slice and its length exceeds its capacity, the runtime allocates a new, larger array and copies the old elements over. Understanding this is key to avoiding performance pitfalls and subtle bugs from unexpected data modification.
Question: What are the key characteristics of Go maps regarding iteration order and concurrency?
Answer: Map iteration order is intentionally randomized and should not be relied upon for deterministic behavior. The built-in map
type is not safe for concurrent access; concurrent reads and writes will cause a panic.
Explanation: To achieve stable iteration, you must extract the keys, sort them, and then iterate over the sorted keys. For concurrent use, maps must be protected with a synchronization primitive like sync.Mutex
or sync.RWMutex
, or by using the specialized sync.Map
type for highly concurrent, read‑mostly workloads.
Question: What is the execution order of
defer
, and are there performance costs to consider?
Answer: Deferred function calls are pushed onto a stack and executed in Last-In, First-Out (LIFO) order when the surrounding function returns. The arguments to a deferred call are evaluated immediately when the defer
statement is executed.
Explanation: There is a small overhead for each defer
call. While negligible in most cases, it can be measurable in "hot loops" (tight, performance-critical loops). In such scenarios, it's better to perform cleanup manually rather than using defer
in every iteration.
Question: What are the main features of generics in Go? Explain type parameters, constraints, and the
~
tilde.
Answer: Generics allow writing functions and data structures that work with a set of types. You define type parameters in square brackets ([T any]
). Constraints are interfaces that define the required methods or properties of a type parameter.
Explanation: The any
constraint allows any type. The comparable
constraint allows types that support ==
and !=
. A tilde (~
) in a constraint, like ~string
, means the type parameter can be the type itself (string
) or any type whose underlying type is string
(e.g., type MyString string
).
Question: What is a method set, and how does it affect interface satisfaction?
Answer: A type's method set determines which interfaces it implements. For a value of type T
, the method set includes methods with receiver (T)
; for a pointer *T
, it includes methods with receivers (T)
and (*T)
.
Explanation: If an interface requires a method with a pointer receiver, only *T
satisfies it. Conversely, if all methods have value receivers, both T
and *T
satisfy it. This impacts assignments like var i I = &T{}
vs var i I = T{}
.
Question: When do you use
make
vsnew
, and what do they return?
Answer: make
initializes built-in reference types (slice, map, channel) and returns a non-zero value of that type. new(T)
allocates zeroed storage for type T
and returns *T
.
Explanation: Use make
to create usable slices/maps/channels with length/capacity. Use new
rarely; a composite literal like &T{}
is clearer and allows field initialization.
Question: What are the nuances of
nil
slices and maps?
Answer: A nil
slice has len=0, cap=0
and can be append
ed to; a nil
map has len=0
but cannot be written to (assigning a key panics) until initialized with make(map[K]V)
.
Explanation: Prefer returning nil
slices to represent empty results (they marshal to null
in JSON) or return empty slices if you require []
in JSON (use make([]T,0)
). Always initialize maps before writes.
Question: What is the difference between a defined type and a type alias?
Answer: A defined type creates a new, distinct type: type ID string
. A type alias introduces an alternate name for the same type: type ID = string
.
Explanation: Defined types do not implicitly convert to/from their underlying type and can have their own methods, enforcing stronger type safety. Aliases are mostly for refactors and compatibility.
Question: What are build tags and how do
//go:build
constraints work?
Answer: Build tags select files/code for specific platforms or build modes. Prefer //go:build expr
at the top of files (legacy // +build
still supported). File suffixes like _linux.go
, _amd64.go
, _test.go
also affect selection.
Explanation: A file is built only when all constraints match. Example: //go:build linux && amd64
limits to Linux/amd64.
Question: How do
init()
functions run and what are caveats?
Answer: init()
runs once per file after package-level variables are initialized and before main
. Order across files is unspecified; across packages it follows import order.
Explanation: Keep init()
light and deterministic; avoid hidden work. Prefer explicit constructors/config.