In Go, why can a package define an interface over a dependency type it cannot modify?
answer
- the provider never names your interface
- the import edge points one way
- declare it where it is used
- even an unexported interface works
- no cooperation from upstream needed
basics
~20 sBecause satisfaction is implicit: the dependency's type never names any interface, so any package may declare an interface listing methods that type already has and use the type as that interface. The only import points from the consumer to the dependency.
solid answer
~50 sIn a nominally typed language the implementer has to write `implements Fetcher`, so an abstraction can only exist with the implementer's cooperation and the implementation ends up depending on it. Go removes that constraint: satisfaction is decided by the method set alone, so the *consumer* can declare the interface it wants, over methods a dependency's type already has, and pass that type straight in. Nothing is added to the dependency, it does not import you, and it does not need to know the interface exists. Practically, that means the abstraction is shaped by the caller's needs rather than by the provider's API surface, and the import edge runs one way, from consumer to provider. It also means you can wrap or substitute the dependency later without touching it — and it is why Go code can go a long way with concrete types and introduce an interface only at the moment a second caller or a stub actually needs one.
code
go · 13 lines// Declared in the mirror package, over a client type it does not own.
type moduleFetcher interface {
Fetch(ctx context.Context, path, version string) (io.ReadCloser, error)
}
func mirror(ctx context.Context, f moduleFetcher, path, version string) error {
rc, err := f.Fetch(ctx, path, version)
if err != nil {
return err
}
defer rc.Close()
return store(path, version, rc)
}go deeper
Know that a Go interface can be declared anywhere, and that a type satisfies it without importing it or being changed. Remember that you cannot add methods to a type from another package.
Explain the import direction: naming the interface in the consumer means the provider has no dependency on the abstraction. Be able to write a small consumer-side interface over a dependency's existing method and say why it is narrower than the dependency's full API.
Show judgement about where the interface should live and how wide it should be, and recognise the wrapper-plus-embedding move when a foreign type is one method short. Notice when several packages have grown the same abstraction independently.
Weigh the cost of the freedom: abstractions that can appear anywhere are cheap to add and hard to inventory. Decide when a repeated consumer-side interface should be promoted to a shared one, and hold the line against importing an implementation package purely to name its interface.
## The mechanism Go checks interface satisfaction against a type's method set at the point where a value is used as that interface. Nothing else is consulted — not a declaration, not an import, not a registry. Three consequences follow directly: 1. The interface may be declared in **any** package, including one the implementing type has never heard of. 2. The implementing package needs **no change at all** — no clause to add, no import to gain. 3. The **import edge runs from the package that names the interface to the package that provides the concrete type**, and not the other way round. That third point is the structural payoff. In Java or C#, `class HTTPFetcher implements Fetcher` makes the implementation depend on wherever `Fetcher` lives. To get the dependency arrow pointing the other way, the abstraction has to be hoisted into a package both sides import. Go needs none of that machinery. ## A worked example Suppose you are building a package registry mirror: it pulls module archives from an upstream store and copies them into local storage. The upstream client lives in another module and exposes, among two dozen other methods, this one: ```go func (c *Client) Fetch(ctx context.Context, path, version string) (io.ReadCloser, error) ``` Your mirror package does not need `*Client`. It needs *one method*. So it declares the interface itself: ```go type moduleFetcher interface { Fetch(ctx context.Context, path, version string) (io.ReadCloser, error) } ``` and takes that as a parameter. `*Client` satisfies it with no modification, because it already has a `Fetch` with exactly that signature. Notice what you gained: - The interface is **shaped by your requirements**, not by the upstream client's full surface. - It can be **unexported** (`moduleFetcher`, lowercase) — an abstraction that is entirely an implementation detail of your package. There is no way to express that in a language where the implementer must name the type. - Substituting a different upstream, a caching layer, or a hand-written stub is a matter of having a `Fetch` method, not of anyone declaring anything. ## What you still cannot do Implicit satisfaction does not let you *add* to a foreign type. A method's receiver type must be declared in the same package as the method, so you cannot write `func (c *othermodule.Client) Retry() error`. If a dependency's type is one method short of the interface you want, the answer is a type of your own that wraps it — embedding the foreign type promotes its existing methods, and you declare the missing one on your wrapper. Satisfaction also does not survive a signature change. If the upstream `Fetch` gains a parameter, the type stops satisfying your interface, and your build breaks at the call site where you pass it. That is a genuine break, and it is caught at compile time. ## Two identical interfaces are interchangeable Because satisfaction is structural, two packages can independently declare interfaces with the same method and both are satisfied by the same type — and a value of one interface type can be assigned to the other, since the method-set check is all that matters. This is why so much Go code converges on `io.Reader` and `io.Writer` without coordination: any two packages that describe "a thing you can read bytes from" with that exact signature have described the same interface. ## The judgement that goes with it The freedom is real but not free. Because an interface can be declared anywhere and satisfied silently, a codebase can accumulate several near-identical abstractions over the same concrete type, each in a different package. That is usually acceptable — each one documents one caller's needs — but it is worth noticing when three packages all declare the same two-method interface, since that may be a genuine shared abstraction that belongs in one place. The other thing worth noticing in review is direction. If you find yourself declaring an interface in the package that *implements* it, and importing that package everywhere just to name the interface, you have reproduced the nominal-typing dependency edge that Go was letting you avoid.
- Can you add a method to a type from another module so it satisfies your interface?No. A method's receiver type must be declared in the same package as the method, so you cannot attach anything to a foreign type. Declare your own struct that embeds or holds the foreign value — embedding promotes its existing methods — and declare the extra method on your wrapper.
- Which way does the import go when the consumer declares the interface?Only from consumer to provider, and only because the consumer needs the concrete type to construct it. The provider never imports the consumer. The interface declaration itself creates no edge at all, which is why it can even be unexported inside the consuming package.
- If two packages independently declare interfaces with the same single method, are they the same type for assignment purposes?They are distinct named types, but assignment between them works in both directions because each one's method set satisfies the other. Structural satisfaction applies to interface-to-interface assignment too, as long as the destination's methods are a subset of the source's.
saying these in an interview costs you the question
- Says the dependency must import your package to implement your interface
- Thinks you can attach a method to another package's type
- Believes an interface must be declared beside its implementation
- Assumes a type must list the interfaces it satisfies
- Claims an unexported interface cannot be satisfied from outside