Types and Interfaces
13. Why can an error be non-nil when it holds a nil pointer?
An interface value is a pair: a dynamic type and a value. It is nil only when both are nil. Returning a typed nil pointer through an error produces an interface whose type is set, so err != nil is true.
type MyErr struct{}
func (*MyErr) Error() string { return "boom" }
func find() error {
var p *MyErr // nil pointer
return p // non-nil error: type is *MyErr, value is nil
}
fmt.Println(find() == nil) // false
The fix is to return a literal nil on the success path. This question separates candidates who have debugged real Go code from those who have only read about interfaces.
14. When do you use a pointer receiver instead of a value receiver?
Use a pointer receiver when the method modifies the receiver, when the struct is large enough that copying it matters, or when the type contains a sync.Mutex or another value that must not be copied.
Otherwise a value receiver is fine, and it is common for small immutable types such as time.Time.
The follow-up is method sets. The method set of *T includes methods with both receiver kinds; the method set of T includes only value receivers.
So if Save has a pointer receiver, a T value does not satisfy an interface that requires Save, and the compiler says so. Mixing receiver kinds on one type is legal but the Go code review guidance recommends picking one.
15. What does append do to the slice you passed in?
A slice is a pointer to an array plus a length and a capacity. If the capacity has room, append writes into the same array and the new slice shares memory with the old one.
If not, it allocates a new, larger array and copies, and the two slices become independent.
a := make([]int, 3, 4)
b := append(a, 4) // fits in capacity: shares a's array
b[0] = 99
fmt.Println(a[0]) // 99
c := append(b, 5) // over capacity: new array
c[0] = 7
fmt.Println(b[0]) // still 99
This is why functions that append to a caller's slice return the result, and why taking a sub-slice of a large buffer keeps the whole buffer in memory. slices.Clone or a full slice expression s[lo:hi:hi] breaks the sharing.
16. What should a candidate know about maps in Go?
Maps are not safe for concurrent writes, and iteration order is unspecified and deliberately randomized between runs, so code must not depend on it.
Reading a missing key returns the zero value; the two-value form v, ok := m[k] tells absence from a stored zero.
Since Go 1.24 the built-in map uses a Swiss table implementation, part of a runtime change the release notes put at 2 to 3% lower CPU overhead on average.
For concurrent access, a mutex around a plain map is the default. sync.Map is tuned for two cases named in its documentation: keys written once and read many times, and goroutines working on disjoint sets of keys.
17. How do you wrap and inspect errors?
Wrap with fmt.Errorf("load config: %w", err) to add context while keeping the original error in the chain. Then check the chain with errors.Is for a sentinel value such as fs.ErrNotExist, or extract a typed error.
Go 1.20 added errors.Join and multiple %w verbs in one Errorf; Go 1.26 added the generic errors.AsType:
_, err := os.Open("/does/not/exist")
err = fmt.Errorf("load config: %w", err)
if errors.Is(err, fs.ErrNotExist) { /* true */ }
if pe, ok := errors.AsType[*fs.PathError](err); ok {
fmt.Println(pe.Op) // "open"
}
AsType returns the typed value directly, avoids the pointer-to-a-variable pattern of errors.As, and is faster. A senior answer also covers when not to wrap: exposing a dependency's error type through %w makes it part of your package's API.
18. When is panic and recover the right tool?
Rarely.
Errors are returned as values; panic is for programmer errors and states that should be impossible, such as an index out of range or a failed invariant during initialization. recover only works inside a deferred function in the same goroutine that panicked.
That last point matters in servers. net/http recovers a panic in a handler and logs it, so one bad request does not crash the process, but a panic in a goroutine the handler started is not covered and takes the whole program down.
Code that starts goroutines from library code should recover and convert panics to errors at that boundary.
19. When should you use generics, and when not?
Use type parameters when the same logic applies to many element types and the alternative is any plus type assertions: containers, and functions over slices and maps like those in the slices and maps packages.
Constraints are interfaces; ~int matches any type whose underlying type is int, and comparable covers map keys.
func Map[T, U any](s []T, f func(T) U) []U {
out := make([]U, 0, len(s))
for _, v := range s {
out = append(out, f(v))
}
return out
}
Do not reach for generics when an ordinary interface describes behavior: a function that calls Read should take an io.Reader, not a type parameter constrained by it.
20. Is struct embedding inheritance?
No. Embedding a type promotes its fields and methods to the outer struct, which gives reuse, but there is no virtual dispatch: a promoted method's receiver is still the embedded value, and it cannot call an overriding method on the outer type.
The outer type can define a method with the same name, which shadows the promoted one.
Embedding an interface in a struct is a common testing trick: the struct satisfies the interface and you implement only the methods the test needs.
Calling any other method panics with a nil dereference, so it suits fakes, not production code.