skip to content

A teammate adds a backend to a map[string]Backend registry and hits "method Close has pointer receiver". How do you diagnose and fix it?

level: seniorimportance: should knowfreq 45%

answer

  1. read the parenthesis, not the line number
  2. the method exists, on the other type
  3. the registration is wrong, not the backend
  4. retyping the map defeats the registry
  5. hand out the interface from a constructor

basics

~20 s

The registered value is a struct value whose Close is declared on the pointer type, so its method set lacks Close. Register the address instead. To stop it recurring, have each backend package export a constructor that returns the interface.

solid answer

~50 s

Read the message literally: it does not say `Close` is missing, it says it lives on `*fileBackend`. So the registry line stored a `fileBackend` value, and a value's method set holds only value-receiver methods. The immediate fix is `registry["file"] = &fileBackend{}`. Before accepting it, check whether the pointer receivers are load-bearing - if the methods mutate fields or the struct carries state that must be shared, the pointer is the right thing and the registration site was wrong. Retyping the map as `map[string]*fileBackend` "fixes" the error by destroying the registry: it can then hold only one concrete backend. The durable fix is that each backend package exports `func New(...) Backend` returning `&fileBackend{...}`, so the value-to-interface conversion happens once, in the package that owns the type, and a new contributor never writes a bare struct literal into the map.

code

go · 17 lines
go
type Backend interface {
	Close() error
}

type fileBackend struct {
	path string
}

func (b *fileBackend) Close() error {
	return nil
}

// NewFileBackend returns the interface, so the value-versus-pointer
// decision is made once, here, and not at every registration site.
func NewFileBackend(path string) Backend {
	return &fileBackend{path: path}
}

go deeper

for a junior

Recognise the message: the method is not missing, it belongs to the pointer type. Storing the address of the struct instead of the struct is the fix you should reach for first.

for a middle

Explain why the value's method set lacks the method, and why an interface-typed container is what forces the check at that line rather than at the method declaration.

for a senior

Show the triage: read the message, decide whether the pointer receivers are load-bearing, reject the value-receiver 'fix' if the methods mutate, and move the conversion into a constructor so the next contributor cannot repeat it.

for a principal

Frame it as an API boundary: whether packages hand out a concrete type or an interface determines where this class of error surfaces, and retrofitting that convention across many implementations is the cost you are weighing.

## Reading the error before changing anything The message has three parts and each one is information: ``` cannot use fileBackend{} (value of type fileBackend) as Backend value in map index expression: fileBackend does not implement Backend (method Close has pointer receiver) ``` - *value of type fileBackend* - the expression you registered is a value, not a pointer. - *does not implement Backend* - satisfaction is being checked against `fileBackend`'s method set. - *method Close has pointer receiver* - the method exists; it is declared `func (b *fileBackend) Close() error`, so it is in `*fileBackend`'s method set and not in `fileBackend`'s. Someone onboarding usually misreads the third line as "my method is wrong" and starts editing the backend. Nothing is wrong with the backend. The registration line is what is wrong. A second detail worth explaining: the compiler names **one** method even when the interface has several in the same situation. It reports a representative failure. Fixing the receiver form fixes all of them at once, so do not go hunting for the others. ## The immediate fix, and the check before you take it ```go registry["file"] = &fileBackend{path: p} ``` One character of real change. Before you accept it, ask the question the error is hiding: **why are these methods on the pointer?** Usually because they mutate - opening a handle, caching a parsed config, recording state between calls. If so, the pointer receiver is correct and the registration was the mistake; storing a value would have meant every method call in the registry mutated a copy nobody could observe, which is precisely what the compiler saved you from. The opposite reading - "just change the receivers to values so the literal compiles" - is where a review must push back. That silently converts a stateful type into one whose mutations are lost, and the compiler will not complain, because value-receiver methods are perfectly legal. A green build is not evidence here. ## The fix that looks right and is not A tempting move is to retype the map: ```go var registry = map[string]*fileBackend{} ``` The error disappears. So does the registry: it can now hold exactly one concrete implementation, which is the opposite of what a plugin host is for. The map must stay interface-typed; the pointer has to be produced at the value, not by weakening the container. ## Making it not happen again The structural problem is that the conversion from a concrete type to `Backend` happens far from the type, in a file the type's author does not open. Two changes move it back: **Export a constructor that returns the interface.** Each backend package provides `func New(...) Backend { return &fileBackend{...} }`. Now the value-versus-pointer decision is made once, in the package that knows the answer, and every registration site is `registry["file"] = filebackend.New(path)` - a call, not a literal, with nothing to get wrong. As a bonus the concrete struct can stay unexported. **Register from the implementation side.** Have each backend package call a `Register(name string, b Backend)` function in its own `init`, so the failure, if it ever happens, appears in the package being edited rather than in a central file the newcomer has never seen. Either way the goal is the same: the error should surface where the methods are written, not in a registry file three packages away. ## What this costs and what it buys Storing pointers in the registry means the backends are shared mutable objects with a lifetime as long as the map. That is normally what a plugin host wants - one instance per backend, initialised at startup. It does mean concurrent use of a backend needs its own synchronisation, and it means a backend that keeps growing internal state keeps that memory for the process's life. Those are real properties to state out loud, not accidents of dodging a compile error. ## The shape of a good answer A strong candidate reads the parenthesis, names the method-set rule in one sentence, fixes the registration rather than the type, notices that retyping the map defeats the registry, and then proposes the constructor so the next contributor cannot hit it. A weaker one flips the receivers to make the build green and does not mention that mutations now go to a copy.

  • The teammate proposes changing Close to a value receiver so the literal compiles. What do you say in review?
    Ask what the method does. If it touches any field, a value receiver makes every call mutate a copy that is thrown away, and nothing will fail loudly - the build goes green and the behaviour goes wrong. The compile error was reporting a real mismatch between how the type is written and how it was registered; the registration is what should change.
  • The interface has three methods but the compiler named only Close. Why one?
    The compiler reports a representative failure rather than enumerating every method that is absent from the value's method set. All three are in the same position here, and correcting the registration to store a pointer satisfies all of them at once. Chasing the other two individually is wasted effort.
  • Does typing the registry as map[string]*fileBackend solve it?
    It removes the error and removes the registry's purpose - the map can then hold only that one concrete type, so no second backend can ever be registered. The container has to stay interface-typed; the pointer must be produced at the value being stored, either with an ampersand or by a constructor that returns the interface.
  • What do you have to accept once the registry holds pointers?
    The backends become shared long-lived objects: every caller that looks one up gets the same instance, so any state a method accumulates is visible to all of them and concurrent use needs its own synchronisation. For a plugin host that is usually the intent, but it should be a stated decision rather than a side effect of silencing a compiler.

saying these in an interview costs you the question

  • Changes receivers to value form to make the build pass
  • Retypes the registry map to the concrete pointer type
  • Says the interface must be missing a method
  • Adds a conversion, expecting it to change the method set
  • Fixes each named method separately without seeing the pattern
  • Never asks whether the methods mutate the receiver