Why must Push and Pop on a container/heap queue use a pointer receiver?
answer
- the length has to change
- a value receiver gets a copy
- which header does append rewrite
- three methods on the value, five on the pointer
basics
~20 sPush and Pop change the queue's length. A value receiver would append to a copy of the slice header, so both assign back through the pointer — which means only the pointer type satisfies heap.Interface.
solid answer
~40 sA slice value is a three-word header — pointer, length, capacity — and a value receiver gets a copy of it. `Push` does `*q = append(*q, job)` and `Pop` does `*q = old[:n-1]`; both rewrite the length, and possibly the data pointer when `append` reallocates, so both must write through a `*JobQueue` receiver or the change dies with the copy. `Len`, `Less` and `Swap` can keep a value receiver because they only read the length or write through the backing array that the copy shares. The consequence is a method-set one: `JobQueue` has three of the five methods, `*JobQueue` has all five, so `*JobQueue` is the type that satisfies `heap.Interface` and you pass `&q` to `heap.Init`, `heap.Push`, `heap.Pop` and `heap.Fix`. `heap.Push(q, job)` does not compile.
code
go · 14 linesfunc (q *JobQueue) Push(x any) {
job := x.(*Job)
job.index = len(*q)
*q = append(*q, job) // rewrites the caller's header
}
func (q *JobQueue) Pop() any {
old := *q
n := len(old)
job := old[n-1]
old[n-1] = nil
*q = old[:n-1] // shorter header, written back
return job
}go deeper
Recall the rule: methods that change a slice's length need a pointer receiver, and heap functions are called with &q. Being able to say why the caller would otherwise see nothing is enough here.
Describe the slice header explicitly — pointer, length, capacity — and connect it to the method-set rule that puts all five methods on *JobQueue and only three on JobQueue.
Show the practical consequence: keep the queue as a field on a long-lived struct so it stays addressable, and know the addressability shorthand and where it stops working, such as a queue stored as a map value.
Decide the type's shape before others depend on it. Weigh mixed receivers, which keep the value usable as sort.Interface, against all-pointer receivers, which remove one thing a reader has to think about.
## Two rules meet here The pointer receiver on `Push` and `Pop` is where Go's value semantics and its method-set rule intersect, which is why interviewers like the question: one answer tests both. ### Rule one: a slice value is a header A `JobQueue` (defined as `[]*Job`) is not the array. It is a three-word descriptor: a pointer to the backing array, a length, and a capacity. Passing it — including passing it as a receiver — copies those three words. The copy points at the *same* array, so element writes are shared, but the length belongs to the copy. That is exactly the split between the heap's five methods: - `Swap(i, j)` writes `q[i]` and `q[j]`. Those go through the shared data pointer, so the caller sees them even with a value receiver. - `Len()` reads the copy's length, which equals the caller's. Fine. - `Less(i, j)` reads two elements. Fine. - `Push` calls `append`, which may return a header with a bigger length and possibly a *different* data pointer if the capacity was exhausted. A value receiver would write that new header into a local variable and drop it on return. - `Pop` reslices to `old[:n-1]`, which produces a header with a smaller length. Same problem. So the two length-changing methods take `*JobQueue` and assign through it: `*q = append(*q, job)` and `*q = old[:n-1]`. ### Rule two: method sets Go's rule is that the method set of `*T` contains methods declared with receiver `T` **and** `*T`, while the method set of `T` contains only those declared with receiver `T`. Declaring `Push` and `Pop` on `*JobQueue` therefore means: - `JobQueue` has `Len`, `Less`, `Swap` — three methods, which satisfies `sort.Interface` but not `heap.Interface`. - `*JobQueue` has all five and satisfies `heap.Interface`. Every `container/heap` function takes a `heap.Interface`, so every call site is `heap.Push(&q, job)`, `heap.Pop(&q)`, `heap.Init(&q)`, `heap.Fix(&q, i)`. Writing `heap.Push(q, job)` fails at compile time with a message saying `JobQueue` does not implement `heap.Interface` because `Push` has a pointer receiver — a good error, once you know how to read it. ### The addressability wrinkle Calling the method by hand, `q.Push(job)`, still compiles on a local variable, because `q` is **addressable** and the compiler rewrites the call as `(&q).Push(job)`. That shorthand only works for addressable operands. A `JobQueue` sitting in a map (`m["k"].Push(job)`) or returned directly from a function (`newQueue().Push(job)`) is not addressable, and the compiler rejects it. Storing the queue as a struct field on a long-lived `*Scheduler` sidesteps the whole issue: `s.queue` is addressable through the pointer, so `heap.Push(&s.queue, job)` reads naturally. ### Why not make everything a pointer receiver? You can — declaring all five on `*JobQueue` is equally correct and arguably more consistent. The standard library's example keeps `Len`, `Less` and `Swap` on the value receiver, which has one practical benefit: a `JobQueue` value still satisfies `sort.Interface`, so you can hand the drained slice to `sort.Sort` or walk it in a test without taking an address. The mixed style is idiomatic here; mixed receivers on a type whose value is copied around freely are a different and worse idea, because then whether a call mutates depends on how you happened to reach it. ### The mistake this prevents Someone who writes `func (q JobQueue) Push(x any) { q = append(q, x.(*Job)) }` gets a program that compiles as a method but fails to satisfy the interface — so it never even reaches `heap.Push`. That is the lucky case. The unlucky version is a `Push` that mutates only element slots and appears to work in a test with a pre-sized slice, then silently drops jobs in production once the queue has to grow.
- Why can Swap keep a value receiver even though it mutates the queue?Swap writes `q[i]` and `q[j]`, and those writes go through the data pointer that the receiver copy shares with the caller's header. The length never changes, so nothing needs to be written back. Only `Push` and `Pop` alter length or reallocate, which is what forces the pointer receiver on those two.
- What compile error do you get from heap.Push(q, job) when q is a JobQueue value?The compiler reports that `JobQueue` does not implement `heap.Interface`, noting that the `Push` method has a pointer receiver. `JobQueue`'s method set holds only `Len`, `Less` and `Swap`; the two heap-specific methods live in `*JobQueue`'s method set. Passing `&q` fixes it.
- If Push has a pointer receiver, why does q.Push(job) compile on a local variable?Because `q` is addressable, so the compiler rewrites the call as `(&q).Push(job)`. That shorthand is not available for non-addressable operands: a queue stored as a map value or returned directly by a function call cannot have a pointer-receiver method invoked on it. Interface satisfaction never gets that shorthand, which is why `heap.Push` still needs `&q`.
saying these in an interview costs you the question
- Says Swap needs a pointer receiver too
- Claims a JobQueue value satisfies heap.Interface
- Believes append always mutates the caller's slice in place
- Passes the queue by value to heap.Push
- Thinks mixing receivers on one type is a compile error