Go interviewer course · Lesson 0001

Your outcome today

See the model.
Then run the interview.

In about 25 minutes, learn the reasoning behind five struct/interface questions and five concurrency questions. During the interview, reveal the answer only after the candidate commits to a prediction.

Level · intermediate Prep · 25 minutes Interview · 45–60 minutes Score · 20 points

Fast route

One pass before the call

8 minLearn the two visual models
12 minPredict each snippet, then reveal
4 minRead the scoring anchors and probes
1 minOpen the print sheet
The interview habit to use:

Ask “What happens, and why?” before asking “How would you fix it?” Prediction exposes the candidate’s model; the fix exposes engineering judgment.

Part 1 · Visual foundation

Two maps explain all ten questions

Don’t memorize ten isolated answers. Use one map for type behavior and one for coordination.

Map A — concrete value → method set → interface

Counter
data value
method set
Value()
contracts it
satisfies
*Counter
pointer value
method set
Value() + Add()
more contracts
possible

T has methods declared on T. *T has methods declared on both T and *T. An addressable value may get call-site shorthand, but that does not change its method set.

Authority: Go specification — method sets

Map B — an interface value is a pair

Use this conceptual pair to reason about typed nils.

Dynamic typenil
Dynamic valuenil

This interface is nil.

Dynamic type*AppError
Dynamic valuenil

This interface is not nil. It contains a concrete type even though that type’s value is a nil pointer.

Authority: Go FAQ — nil error values

Map C — give each coordination tool one job

goroutineRuns work independently. Starting it does not wait for it or propagate cancellation.
channelMoves values or signals events. A matching operation can establish ordering.
mutexProtects a shared invariant while several goroutines access the same state.
WaitGroupJoins a known set of tasks. It communicates completion, not results or cancellation.
contextPropagates cancellation and deadlines across an operation’s call tree.

Visibility needs an ordering path

write shared data
send / close channel
matching receive
read shared data
write protected data
Unlock
later Lock
read protected data

No synchronization arrow means no visibility guarantee. “It probably finishes first” is not a synchronization mechanism.

Authority: The Go Memory Model and package sync

Decision shortcut

Transfer work or ownership → channel. Guard shared state → mutex. Wait for completion → WaitGroup. Stop an operation tree → context. These tools are complementary.

Part 2 · Retrieval practice

The ten interview questions

Say your own answer aloud before opening each reveal. A four-minute timer is included because concise reasoning is part of the signal.

04:00

Five on structs & interfaces

01

Pointer receiver vs method set

Which three lines compile? Explain why the first call and the interface assignment follow different rules.

type Counter struct{ n int }

func (Counter) Value() int { return 0 }
func (*Counter) Add()      {}

type Incrementer interface{ Add() }

var c Counter
c.Add()                    // A
var i Incrementer = c      // B
var j Incrementer = &c     // C
Reveal model answer

A and C compile; B does not. Because c is addressable, c.Add() is shorthand for (&c).Add(). But interface satisfaction uses method sets: Counter has only Value; *Counter has Value and Add. Assignment does not auto-take the address.

Strong signalSeparates call-site addressability from compile-time interface satisfaction.
Risk signalSays “pointer methods work on both” without explaining why B fails.

Probe: Why would a mutating method normally use *Counter? What if the receiver contains a mutex?

Spec: method sets · Effective Go: pointers vs values

Score this answer
02

Struct copy is not deep copy

After these mutations, what does a contain? How would you make b independent?

type Batch struct {
    IDs  []int
    Meta map[string]string
}

a := Batch{
    IDs:  []int{1, 2},
    Meta: map[string]string{"owner": "ana"},
}
b := a
b.IDs[0] = 9
b.Meta["owner"] = "bob"
Reveal model answer

a.IDs is [9 2] and a.Meta["owner"] is "bob". Assignment copies the struct fields, but a slice field is a descriptor referring to an underlying array and a map refers to runtime-managed map data. Copy each referenced structure explicitly when independent ownership is required.

b := a
b.IDs = append([]int(nil), a.IDs...)
b.Meta = make(map[string]string, len(a.Meta))
for k, v := range a.Meta { b.Meta[k] = v }
Strong signalDistinguishes copying a slice header from copying its backing array.
Risk signalClaims structs are references, or that every struct assignment is a deep copy.

Probe: If b.IDs = append(b.IDs, 3), must a observe it? Answer: it depends on capacity and allocation; element aliasing and slice length are separate issues.

Spec: struct types · Effective Go: slices

Score this answer
03

Embedding and ambiguity

Does C satisfy Pinger? Is struct embedding inheritance?

type A struct{}
func (A) Ping() {}

type B struct{}
func (B) Ping() {}

type C struct { A; B }
type Pinger interface{ Ping() }

var _ Pinger = C{}
Reveal model answer

It does not compile. Both embedded fields promote a Ping at the same depth, so C.Ping is ambiguous and C has no unique promoted method satisfying Pinger. Calls can be qualified as c.A.Ping(), or C can declare its own Ping.

Embedding is composition with field/method promotion, not subtype inheritance. A promoted call still uses the embedded field as receiver; there is no superclass-style dynamic dispatch.

Strong signalUses “promotion,” “ambiguity,” and “composition” precisely.
Risk signalCalls C a child class or expects automatic method overriding.

Probe: How would func (C) Ping() {} change the result? It resolves the selector and satisfies the interface.

Spec: embedded fields and promoted methods · Effective Go: embedding

Score this answer
04

The typed-nil interface

What does the comparison print, and what exactly is stored in the returned interface?

type AppError struct{}
func (*AppError) Error() string { return "failed" }

func load() error {
    var e *AppError
    return e
}

fmt.Println(load() == nil)
Reveal model answer

It prints false. The returned error holds dynamic type *AppError and dynamic value nil. An interface compares equal to nil only when neither a dynamic type nor value is present. Return an explicit nil on the success path.

Strong signalExplains both halves of the interface value and gives the correct API fix.
Risk signalSays “nil pointers are never nil in Go” or reaches first for reflection.

Probe: Can a method be invoked through this interface? Yes; it receives a nil pointer. Whether it panics depends on the method body.

Go FAQ: why a nil error may not equal nil

Score this answer
05

Design the smallest boundary

An encoder only calls Write. Should its parameter be *bytes.Buffer, a custom Write/Flush/Close interface, or io.Writer? Why?

func Encode(dst ???, record Record) error
Reveal model answer

Use io.Writer if writing bytes is the whole requirement. It is the smallest established contract, accepts buffers, files, network connections, hashes, and test doubles, and avoids coupling to one implementation. Do not require Flush or Close unless this function uses or owns those lifecycle operations.

Good Go interfaces usually describe behavior needed by the consumer. Define a new interface near the consuming code only when no existing contract expresses the need.

Strong signalConnects interface size to ownership, substitutability, and simple tests.
Risk signalAdds methods “for future use” or closes a caller-owned writer.

Probe: Would you return an interface from a constructor? Listen for nuance: concrete returns preserve capability; interface returns can be right when abstraction is intentional.

Effective Go: interfaces · Package io.Writer

Score this answer

Five on concurrency

06

Starting is not joining

What can this print? Is it correct Go? What synchronization guarantee is missing?

var n int

go func() {
    n = 1
}()

fmt.Println(n)
Reveal model answer

It may appear to print 0 or 1, but the program has a data race and no result is guaranteed. The go statement is ordered before the new goroutine starts; it does not order that goroutine’s completion before the print. The process can also finish without waiting for detached work.

Use a channel or WaitGroup to establish completion. For example, write n, close done, receive from done, then read n.

Strong signalNames the data race, missing join, and happens-before direction.
Risk signalPredicts a deterministic result based on “goroutines are fast” or CPU count.

Probe: Would time.Sleep make it correct? No. Timing is not synchronization.

Go Memory Model · Data Race Detector

Score this answer
07

Buffering and backpressure

What happens here? Would changing capacity to two be a robust fix?

ch := make(chan int, 1)
ch <- 1
ch <- 2
fmt.Println(<-ch)
Reveal model answer

The first send fills the single slot. The second send blocks because the buffer is full and no other goroutine can receive, so the runtime reports a deadlock. Capacity two happens to let this snippet proceed, but it encodes an assumed burst size rather than fixing the producer/consumer design.

An unbuffered send waits for a matching receive. A buffered send waits only when the buffer is full. Buffers decouple timing up to a bound and provide backpressure at that bound.

Strong signalExplains both scheduling and design semantics, not merely “deadlock.”
Risk signalTreats a large buffer as a universal deadlock or leak fix.

Probe: When is a capacity-one channel meaningful? Examples include a latest pending signal, a one-token semaphore, or deliberately bounded handoff.

Spec: channel types · Memory model: channel communication

Score this answer
08

Who closes a shared output?

Several workers send to one result channel. Who should close it, and how can the consumer safely range over it?

for i := 0; i < workers; i++ {
    wg.Add(1)
    go worker(jobs, results, &wg)
}

for result := range results {
    use(result)
}
Reveal model answer

No individual worker can know that all sends are finished. A coordinator waits for every worker, then closes the shared output while the consumer is already able to drain it.

go func() {
    wg.Wait()
    close(results)
}()

for result := range results { use(result) }

Closing expresses “no more values,” not “please stop.” A receive from a closed channel returns remaining buffered values, then the zero value with ok == false. Sending to or closing an already-closed channel panics. A nil channel blocks forever, which can intentionally disable a select case.

Strong signalAssigns closure to the sender-side coordinator and spots the need to drain concurrently.
Risk signalHas every worker defer close(results), or asks the receiver to close it.

Probe: Why not call wg.Wait() before the range in the same goroutine? Workers may block sending while nobody drains results.

Go blog: pipelines and cancellation · Package sync.WaitGroup

Score this answer
09

The copied mutex

The method locks before writing. Why is it still unsafe?

type Cache struct {
    mu sync.Mutex
    m  map[string]string
}

func (c Cache) Put(k, v string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.m[k] = v
}
Reveal model answer

The value receiver copies Cache, including its mutex. The copied map field still refers to shared map data, but concurrent calls can lock different mutex copies, so they do not exclude each other. Use a pointer receiver, keep the mutex with the state it guards, and do not copy a mutex after first use.

func (c *Cache) Put(k, v string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.m[k] = v
}

Every access participating in the invariant—including reads—must follow the same locking rule. Verify exercised paths with go test -race ./....

Strong signalConnects struct copying, shared map identity, and ineffective mutual exclusion.
Risk signalFocuses only on defer cost or claims concurrent map reads are always safe.

Probe: When would you choose RWMutex? Only after a clear read-heavy need; it adds complexity and is not automatically faster.

Package sync.Mutex · Data Race Detector

Score this answer
10

Cancellation and goroutine leaks

After first returns, what happens to the producer? Redesign its lifetime.

func produce(out chan<- int) {
    for i := 0; ; i++ {
        out <- expensive(i)
    }
}

func first() int {
    out := make(chan int)
    go produce(out)
    return <-out
}
Reveal model answer

After the first receive, no receiver remains. The producer eventually blocks forever on its next send, retaining its stack and anything it references. Give the operation a cancellation path and make the sending goroutine own closure of its output.

func produce(ctx context.Context) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for i := 0; ; i++ {
            v := expensive(i)
            select {
            case out <- v:
            case <-ctx.Done():
                return
            }
        }
    }()
    return out
}

func first() int {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    return <-produce(ctx)
}

If expensive can itself block, it also needs to accept and observe cancellation. Cancellation must reach every blocking boundary.

Strong signalFinds the blocked send, defines ownership, and propagates cancellation through nested work.
Risk signalAdds an arbitrary buffer or assumes garbage collection stops goroutines.

Probe: Is closing out a cancellation mechanism for the producer? No; a receiver should not close a sender-owned data channel, and a later send would panic.

Go blog: pipelines and cancellation · Go blog: context

Score this answer

Part 3 · Consistent evaluation

Score reasoning, not vocabulary

PointsObservable evidenceInterviewer action
0Cannot predict behavior, invents a rule, or proposes an unsafe fix.Give one small hint, then move on.
1Gets the result or rule, but explanation is shallow or the fix misses an edge case.Use the listed probe once.
2Predicts correctly, explains the underlying model, and gives a safe, proportionate fix.Ask for trade-offs; avoid trivia escalation.

Interpretation for this set

  • 16–20: strong intermediate signal
  • 12–15: solid, with focused gaps
  • 8–11: material gaps for this level
  • 0–7: foundations need strengthening

Treat bands as evidence, not an automatic hiring decision. Weight communication, experience, and the role’s actual work.

Three cross-question signals

  • Ownership: who may mutate, close, or cancel?
  • Lifetime: what starts work, and what guarantees it stops?
  • Ordering: which synchronization edge makes a read safe?

A candidate who repeatedly asks these questions is usually reasoning from durable principles.

Fair-interview rule

Accept different safe designs. Channels and mutexes are both valid when they make ownership and invariants clear. Score the reasoning and trade-off, not whether the candidate picked your favorite primitive.

Primary reading

Keep the authority close

Primary source

The Go Programming Language Specification, especially struct types, interface types, and method sets. Use it when a compile-time claim is disputed.

Concurrency companion

The Go Memory Model. Read the advice, goroutine creation, channel communication, and locks sections; formal notation can wait.