Interfaces in Go: two words, a nil that is not nil, and a call four times dearer
An interview on interfaces climbs a ladder: what sits in the variable — why nil is sometimes not nil — when boxing allocates — what a call through an interface costs — how a type assertion works. The lesson climbs all of it, and everything follows from one fact: there are two words, not one.
Full technical treatment
TL;DR
A type implements an interface silently. No implements, no list of
interfaces on the type: having the required methods is enough. Hence the main
design consequence — an interface belongs to whoever consumes it, is
declared small, and lives next to the need rather than next to the
implementation.
And hence the two places where an interface behaves unlike expected. First:
a method with a pointer receiver is not in the method set of the value, and
the compiler says so in as many words —
Point does not implement Stringer (method String has pointer receiver).
Second: an interface variable carries a pair — a dynamic type and a dynamic
value — and equals nil only when both halves are empty. Put a nil pointer into
it: there is no value, but the type is known, and err == nil gives false.
This is Go's most famous trap.
Beyond that come the layout and the numbers. In the current implementation
that pair sits in memory as two words: measured, any and Stringer take
16 bytes against 8 for a pointer. The second word is a pointer, so boxing a
struct allocates (1 allocation) while boxing a ready pointer does not (0);
small integers are 0 too, the runtime keeps a ready array of 0 to 255. The first
word is a method table, so the compiler cannot see which code will run and does
not inline the call: on an empty method it came out at 11,461 ns against
2,804, a factor of 4.1. A type assertion and a type switch are a
type-pointer comparison rather than a walk over a structure; the gap between
them is small (4,521–5,023 ns against 3,661–4,128) and does not decide the form.
- types can have methods, and a method is called as
x.M(); - a function can take a value it knows nothing about except what that value can do;
- Go has pointers:
&xis the address of a value, and through it the value can be changed.
- what a method set is, what a dynamic type is, what boxing a value into an interface means;
any,itab, type assertions,type switch, the typednil.
What is really being asked
The ladder is almost always this one:
- "How is an interface built inside?" — testing whether you say "two words" or stop at the word "contract".
- "Why did
err != nilfire when nil was returned?" — the famous trap; asked more often than everything else combined. - "When does putting a value into an interface allocate?" — testing whether you understand that the second word is a pointer.
- "Is a call through an interface dearer than a direct one? By how much?" — a question about honesty: "negligible" and "many times" are equally bad without a number.
- "How does
x.(T)differ fromswitch x := v.(type)?" — testing whether you know both forms do the same thing. - "Why does my type not implement the interface?" (with code) — about method sets and pointer receivers.
The lesson climbs that ladder from the bottom: first what an interface means, then what follows from it, and only at the end how it is laid out in memory and what it costs.
Base: implementation without a declaration
An interface in Go is a list of methods with a name on it. Stringer says
nothing about what a value is made of or where it came from; it says one thing:
"this can do String() string". A function declaring a Stringer parameter
therefore demands not a particular type but an ability.
Then comes the decision most languages do not make: a type implements an
interface silently. No implements, no list of interfaces on the type — the
compiler checks for itself whether the methods are there, and if they are, the
value fits. Nothing has to declare the intent, and there is nowhere to declare
it.
bench/gointerface/methodsets.sh builds and runs a program in which Point
never mentions Stringer:
type Stringer interface{ String() string }
type Point struct{ X, Y int }
func (p Point) String() string { return fmt.Sprintf("(%d,%d)", p.X, p.Y) }
var s Stringer = Point{1, 2}assigned: (1,2)
--- exit code: 0
It built and it printed. Two consequences follow, and interviewers value them above knowing how an interface is laid out in memory.
An interface belongs to whoever CONSUMES it, not to whoever implements it.
You can declare Stringer in your own package — and Point from somebody else's
library will implement it while knowing nothing about your existence. In a
language with explicit implementation that is impossible: to make a type fit an
interface you have to change the type.
Hence "accept interfaces, return structs". A function has no business demanding an interface declared somewhere else: it declares its own, minimal one — exactly the methods it needs. Everything that has them will fit.
The practical corollary: interfaces in Go are small. io.Reader is one
method, io.Writer is one. An interface with ten methods almost always means it
was declared from the implementation's side rather than from the need's.
That is already enough to answer the basic interview question: an interface is a
requirement on behaviour, and a type meets it simply because the methods are
there. Everything below is about where that silence has its boundary, what an
interface variable actually holds, and why it can differ from nil exactly when
"there is a nil inside".
Mechanism 1: method sets — where the silence ends
Silent implementation has exactly one boundary, and everybody trips over it. A method with a pointer receiver is not in the method set of the value.
The same script tries to build a program where the receiver is a pointer and the value is assigned:
func (p *Point) String() string { return "point" }
var byValue Stringer = Point{1, 2} // an error
var byPointer Stringer = &Point{1, 2} // legalbad.go:10:25: cannot use Point{…} (value of struct type Point) as Stringer value in variable declaration: Point does not implement Stringer (method String has pointer receiver)
--- go build exit code: 1
The error is worth reading out loud in full: it names the reason itself.
The reason is not syntax. A value put into an interface is a copy, and a copy has no address. A method with a pointer receiver could not change the original; silently changing the copy would be worse than a compile error, so the compiler refuses up front.
The other direction is allowed: a pointer has an address, and dereferencing it for a value method always works. Hence a table worth keeping in mind:
| method declared as | in the method set of T | in the method set of *T |
|---|---|---|
func (t T) M() | yes | yes |
func (t *T) M() | no | yes |
The practical rule: one kind of receiver for the whole type. Mixing is legal, but then the method set depends on what exactly was assigned — and the error turns up somewhere other than where it was made.
Mechanism 2: the dynamic type and the dynamic value
Now the model everything else follows from — and it is not about the runtime. An interface variable carries two things: which concrete type is in it right now, and which value of that type.
a variable of interface type stores a pair: the concrete value assigned to the
variable, and that value's type descriptor
Two things, not one — and this is not an implementation detail but observable semantics:
var s Stringer // type: none, value: none
s = Point{1, 2} // type: Point, value: {1, 2}
s = &Point{1, 2} // type: *Point, value: an addressThree separately-asked things follow immediately.
A type assertion hands the concrete type back. s.(Point) asks not "is it
similar" but "is the dynamic type exactly Point?". Which is why *Point will
not satisfy Point — those are different dynamic types.
Comparing interfaces compares the whole pair. They are equal if both the type and the value match. Hence, too, the panic when comparing interfaces holding an incomparable dynamic type: there is nothing to compare a slice with.
And most importantly — an interface is empty only when BOTH halves are. That is the next mechanism, and it is the most-asked question in Go interviews.
Mechanism 3: the nil that is not nil
This is the single most common Go interview question, and it must be answered with a mechanism rather than a phrase.
There is one rule, and it is written in the language FAQ:
An interface value is nil only if the V and T are both unset
Now look at what happens to the two words in each of four cases — switch between them:
The whole trap is visible in switching between the first and second case: the second word is empty in both, and they differ only in the first. That is exactly why
var p *NotFound // a nil pointer
var err error = p // but the interface is no longer empty: the type is known
fmt.Println(err == nil) // falsegives false. The value inside is a nil pointer, but the word holding the type
is filled, and the rule demands that both be empty.
Where this fires in real code. Always in one place — when a function is
declared as returning error while inside it returns a concrete error type:
func do() error {
var e *MyErr // nil
if bad() {
e = &MyErr{}
}
return e // TRAP: even when e == nil, error != nil
}The caller writes if err != nil — and gets an "error" that does not exist.
There are three cures, and all three are worth naming:
- Declare the variable with the interface type, not the concrete one:
var err error— then, with no error, both words really are empty. - Return
nilexplicitly in the success branch:return nil, notreturn e. - Do not keep concrete error types in intermediate variables — that is the root cause, and the first two points treat its symptoms.
A rule worth memorising: a concrete type does not quietly survive assignment into an interface. Once a value is put into an interface, its type stays there — even when the value itself is absent.
Mechanism 4: a type assertion is a pointer comparison
"How does x.(T) differ from a type switch" is asked expecting an answer
about speed. The right answer starts with the fact that both forms do the
same thing: they compare the pointer to the type descriptor in the first word
against one known at compile time. This is not a walk over a structure and not
reflection — it is a comparison of two addresses.
Measured on equal work — in the same runs, on the same machine as every other number in this lesson:
| form | time |
|---|---|
if x, ok := v.(int); ok | 4,521–5,023 ns |
switch x := v.(type) | 3,661–4,128 ns |
The run ranges do not overlap and the ratio is 0.81 — but that is a fifth of a difference on an empty operation, and it does not decide the form. Nor should that ratio be carried over to your own code: it is about these two loops on this machine. The form is chosen by the number of types: one branch is written as an assertion, several as a switch.
What really does need knowing about these forms:
- The one-result form panics, the two-result one does not:
v.(int)againstv, ok := x.(int). In code that does not control where the value came from, always the second. - The specification requires
xnot to benil:x.(T)asserts that x is not nil and that the value stored in x is of type T. An assertion on a cleannilinterface never succeeds. case nilin atype switchcatches exactly the cleannil— the one where both words are empty. A typed nil lands in the branch for its own type, and that is one more place where the nil-that-is-not-nil trap bites.
Mechanism 5: how this gets used
The last mechanism is not about the layout but about what all of the above turns into in real code. Three rules, each following from what has already been said.
Interfaces are declared small and where they are consumed. That follows directly from the Base: since implementation is implicit, an interface need not live next to the type. A function declares exactly the methods it needs — and everything that has them fits. A sign of trouble: an eight-method interface declared next to its single implementation. Such an interface abstracts nothing; it restates a struct.
Accept interfaces, return structs. An interface parameter widens the range of
what can be passed in; an interface result, conversely, narrows what the
caller can do with it and hides methods the concrete type actually has. There is
one accepted exception: returning an interface makes sense when the concrete type
genuinely varies — as with error.
At boundaries, watch the type-and-value pair. The typed-nil trap lives
exactly where an interface crosses a package boundary: inside, the function works
with *MyErr; outward it hands back an error. The rule is simple — at the
boundary declare the variable with the interface type, var err error, and
return nil explicitly.
Hence, too, the answer to the frequent "why have an interface at all if there is
one implementation". For the same reason io.Writer exists: so the caller can
substitute their own. If there is nobody to substitute and nobody planned to —
the interface is not needed, and its absence is not a design flaw.
Deeper: how it is built inside
Everything above described what an interface means, and it rests on the specification. Now how it is made — and here the claims change kind: not rules of the language but the current implementation.
The type-and-value pair lives in memory as two machine words. That is not a promise of the language but how it is made today — and it is measurable. What an interface variable takes:
| size | |
|---|---|
any | 16 bytes |
Stringer | 16 bytes |
*Point | 8 bytes |
Point (a struct of two ints) | 16 bytes |
Sixteen bytes where a pointer needs eight — that is two machine words. What lies in them is said plainly on the Go blog:
a variable of interface type stores a pair: the concrete value assigned to the
variable, and that value's type descriptor
One refinement about the first word: for an empty interface (any) it is just
a type descriptor, while for an interface with methods it is a method table
(itab), where the addresses of the implementations sit beside the type. That
difference matters for the cost of a call, below.
One consequence per word from here on: the second explains allocations, the first the cost of a call.
The second word is a pointer, so boxing allocates
The second word holds not the value but its address. Hence a simple rule: if what is being boxed does not fit in a pointer, it has to be put somewhere — and that is a heap allocation.
Measured (bench/gointerface/internals.go):
what goes into any | allocations |
|---|---|
an int variable holding 42 | 0 |
an int variable holding 1000 | 1 |
a Point struct variable | 1 |
a ready *Point pointer | 0 |
Three rows are explained by one sentence, the fourth separately.
A pointer goes in as is: there is nothing to copy, zero allocations. A struct has to be placed on the heap, because only an address fits in the second word. But 42 against 1000 is a separate fact worth knowing: the runtime keeps a ready array of small integers, and for numbers from 0 to 255 an address is taken from there rather than allocated afresh.
That, incidentally, is a good answer to "how would you measure it": take the same code with a constant instead of a variable and the allocations become zero in both cases — the compiler boxes a constant at compile time. The first draft of this measurement got it wrong in exactly that way.
The practical point they want to hear: the cost of an interface is not "the interface" but the fact that the boxed value does not fit in a word. So in hot code interfaces over pointers are free memory-wise, and interfaces over structs are not.
The first word is a table, so a call through it is dearer
A call through the table is dearer than a direct one. Here the cause must not be muddled, or the answer sounds like superstition.
What is dear is not looking the method up in the table — that is pennies. What is dear is that the compiler stops seeing which code will run: a direct call it inlines whole and optimises together with the surrounding code, while a call through an interface has to go through the method table and stays a call.
Measured over 4,096 shapes of two concrete types — in one run on one machine, and the ratio holds for exactly that run:
| time | |
|---|---|
| direct call | 2,804 ns |
| through an interface | 11,461 ns |
| ratio | 4.1 |
And right here — how such a measurement must be done. The first draft compared a call on a concrete-typed variable with a call on an interface-typed one and got 0.87 — "the interface is faster". That was not a finding but an admission that the measurement was worthless: for a package-level variable of interface type the compiler knows the single concrete type, devirtualises the call and inlines it. To keep a call a call you need two different concrete types in a slice and data from memory rather than from a literal.
This is worth saying out loud in an interview: "how did you measure it" almost always follows, and "I used two types so the compiler could not devirtualise" is the answer that separates someone who measured from someone who repeated.
What follows for code. Nothing like "do not use interfaces". Four times is four times on an empty method in a tight loop; as soon as there is real work inside, the call's share falls to nothing. The rule is this: an interface at a boundary is normal, an interface inside a hot million-iteration loop is something to measure.
How to answer in an interview
Short answer: an interface is a list of methods, and a type implements it
silently, simply because it has those methods. Hence the two things asked
next: a method with a pointer receiver is not in the method set of the value,
and the interface variable itself carries a pair — a dynamic type and a dynamic
value — that equals nil only when both halves are empty.
That is enough for a correct answer. What follows is what you add when the interviewer digs.
If the interviewer digs deeper
To "how is an interface built" answer "two words". "A type or a method table and a pointer to the value; sixteen bytes against eight for a pointer — that is how it is made today, the specification promises only the pair." Everything else follows from that, and the interviewer hears it.
Explain the nil trap with the rule, not the example. "An interface equals nil only when both words are empty; here the second is empty and the first holds the type." The example comes after — it takes two lines and confirms rather than replaces.
On boxing say "the second word is a pointer". "A pointer goes in as is, zero allocations; a struct has to be placed somewhere, one. Small integers are the exception, the runtime keeps a ready array of 0 to 255." That last sentence shows you ran it.
On the cost of a call give a number and a cause. "I got four times on an empty method, and what is dear is not the table lookup but the lost inlining." And immediately: how it was measured — two concrete types in a slice, otherwise the compiler devirtualises.
On x.(T) and type switch say "the same thing". "Both compare a type
pointer; the form is chosen by the number of branches, not by speed." Saying
"switch is faster" with no number sounds worse than "there is a gap, but it is a
fifth on an empty operation".
On method sets name the reason, not the rule. "A value in an interface has no address, so a method with a pointer receiver is not in its method set."
Next they ask
Can interfaces be compared with ==?
They can, but carefully. The specification: two interface values are equal if
they have identical dynamic types and equal dynamic values, or if both are
nil. That is, both halves of the pair are compared.
The danger is that the comparison can panic at run time. If an uncomparable
type sits inside — a slice, a map, a function — the comparison gives
comparing uncomparable type. The compiler will not catch it: it sees only the
interface. So == on an any holding external data is a source of panics, and
there reflect.DeepEqual, or a comparison after a type assertion, is safer.
Where should an interface be declared — beside the implementation or at the consumer?
The Go idiom is at the consumer, and it follows directly from implementation being implicit: a type needs to know nothing about an interface to satisfy it. So an interface can be declared where it is needed, at exactly the size needed.
The practical consequence: a one- or two-method interface beside the function that accepts it is normal and right. A large interface beside its single implementation is almost always a spare layer — it abstracts nothing but forces you to change two places instead of one.
What is the empty interface for and how does it differ from any?
It does not: any is an alias for interface{}, introduced in Go 1.18 for
readability. They are literally the same thing, and modern gofmt rewrites one
into the other itself.
Its meaning is simple: every type satisfies the empty interface, because it
demands nothing. The first word is still filled, though — with a type descriptor
rather than a method table. So any is 16 bytes too, and the typed-nil trap
works on it exactly the same way.
What is an itab and when is it created?
An itab is what sits in the first word of an interface with methods: a
"type plus interface" pair with the addresses of the method implementations
beside it. The call target is taken from there.
It is built once per "concrete type — interface" pair and cached by the runtime, so repeated assignments of the same type into the same interface build nothing anew. Hence the answer to a common follow-up: the cost of assigning into an interface is the cost of boxing the value, not of building the table.
How do you check at compile time that a type implements an interface?
With a stub line:
var _ Stringer = (*Point)(nil)Nothing is allocated and nothing runs here — it is an assertion for the
compiler. If *Point stops implementing Stringer, the build fails in the
package where the type is declared rather than where it is used.
The trick is worth knowing not for elegance: without it the error turns up in someone else's package six months later, pointing at a line nobody touched.
Interface over a value or over a pointer — what should go in?
Look at two questions. First, the method set: if even one method has a pointer receiver, a pointer has to go in or it will not compile. Second, size: boxing a struct is a heap allocation, boxing a ready pointer is zero.
Hence the usual choice: if the type already lives behind a pointer, put the pointer in — it is both cheaper and more uniform. Putting a value in makes sense for small immutable types, where the copy is a feature rather than a cost.
Common misconceptions
an interface is just a pointer to an object
Semantically it is a pair — a dynamic type and a dynamic value — and in the current Go implementation two machine words. Measurable: any and Stringer take 16 bytes while *Point takes 8. The first word is the type or the method table, the second a pointer to the value.
if a nil pointer sits inside an interface, the interface is nil too
No: an interface equals nil only when both words are empty. The language FAQ says it verbatim — an interface value is nil only if the V and T are both unset. The second word is empty but the first holds the type, and err == nil is false.
the err != nil check is reliable on its own
It is reliable only if a concrete error type is never put into error. A function returning error but keeping a *MyErr inside will return an "error" even when there is none. Cured by declaring the variable with the interface type and returning nil explicitly in the success branch.
putting a value into an interface always allocates
Not always: a ready pointer goes into the second word as is — zero allocations. Small integers are zero too: the runtime keeps a ready array of 0 to 255. An allocation appears where the boxed value does not fit in a word — a struct, for instance.
a call through an interface is dearer because the method is looked up in a table
The table lookup is pennies. What is dear is that the compiler stops seeing the target and does not inline the call: a direct call is optimised together with the surrounding code, while a call through a table stays a call. Measured on an empty method: 11,461 ns against 2,804, four times.
a type switch is noticeably faster than a type assertion
Both forms do the same thing — compare a pointer to a type descriptor. On equal work it came out at 3,661–4,128 against 4,521–5,023 ns: the gap is real and stable, but it is a fifth on an empty operation. The form is chosen by how many types must be told apart, not by speed.
if a type has the method, it implements the interface
Only if the method is in the method set of what you are assigning. A method with a pointer receiver is not in the method set of the value: a copy in an interface has no address, and such a method could not change the original. Hence Point does not implement Stringer for a struct and success for its address.
interfaces can be compared freely with ==
Comparing interfaces is legal but panics at run time if an uncomparable type sits inside — a slice, a map or a function: comparing uncomparable type. The compiler will not see it, it knows only the interface. For values from external data, comparing after a type assertion is safer.
Practice
Two problems. Answer first, then check against the real output: in both, the correct answer is taken from a run of the script rather than assigned.
Practice · predict the output
type NotFound struct{ Key string }
func find(ok bool) error {
var missing *NotFound
if ok {
return nil
}
return missing
}
err := find(false)
fmt.Println(err == nil)
var p *NotFound
fmt.Println(p == nil)
fmt.Println(reflect.TypeOf(err), err == nil)
fmt.Println(find(true) == nil)Practice · estimate
Knowledge check
A function returns error and inside returns a variable of type *MyErr that equals nil. What is err == nil at the caller?
This is neither a retelling nor a separate text: everything below is taken from the article itself — its own summary, the section headings, the “actually” column and the version table. Which is why these theses cannot drift from the article.
The gist
- A type implements an interface silently. No
implements, no list of interfaces on the type: having the required methods is enough. Hence the main design consequence — an interface belongs to whoever consumes it, is declared small, and lives next to the need rather than next to the implementation. - And hence the two places where an interface behaves unlike expected. First: a method with a pointer receiver is not in the method set of the value, and the compiler says so in as many words —
Point does not implement Stringer (method String has pointer receiver). Second: an interface variable carries a pair — a dynamic type and a dynamic value — and equalsnilonly when both halves are empty. Put a nil pointer into it: there is no value, but the type is known, anderr == nilgives false. This is Go's most famous trap. - Beyond that come the layout and the numbers. In the current implementation that pair sits in memory as two words: measured,
anyandStringertake 16 bytes against 8 for a pointer. The second word is a pointer, so boxing a struct allocates (1 allocation) while boxing a ready pointer does not (0); small integers are 0 too, the runtime keeps a ready array of 0 to 255. The first word is a method table, so the compiler cannot see which code will run and does not inline the call: on an empty method it came out at 11,461 ns against 2,804, a factor of 4.1. A type assertion and atype switchare a type-pointer comparison rather than a walk over a structure; the gap between them is small (4,521–5,023 ns against 3,661–4,128) and does not decide the form.
In fact
- Semantically it is a pair — a dynamic type and a dynamic value — and in the current Go implementation two machine words. Measurable:
anyandStringertake 16 bytes while*Pointtakes 8. The first word is the type or the method table, the second a pointer to the value. - No: an interface equals
nilonly when both words are empty. The language FAQ says it verbatim — an interface value is nil only if the V and T are both unset. The second word is empty but the first holds the type, anderr == nilis false. - It is reliable only if a concrete error type is never put into
error. A function returningerrorbut keeping a*MyErrinside will return an "error" even when there is none. Cured by declaring the variable with the interface type and returningnilexplicitly in the success branch. - Not always: a ready pointer goes into the second word as is — zero allocations. Small integers are zero too: the runtime keeps a ready array of 0 to 255. An allocation appears where the boxed value does not fit in a word — a struct, for instance.
- The table lookup is pennies. What is dear is that the compiler stops seeing the target and does not inline the call: a direct call is optimised together with the surrounding code, while a call through a table stays a call. Measured on an empty method: 11,461 ns against 2,804, four times.
- Both forms do the same thing — compare a pointer to a type descriptor. On equal work it came out at 3,661–4,128 against 4,521–5,023 ns: the gap is real and stable, but it is a fifth on an empty operation. The form is chosen by how many types must be told apart, not by speed.
- Only if the method is in the method set of what you are assigning. A method with a pointer receiver is not in the method set of the value: a copy in an interface has no address, and such a method could not change the original. Hence
Point does not implement Stringerfor a struct and success for its address. - Comparing interfaces is legal but panics at run time if an uncomparable type sits inside — a slice, a map or a function:
comparing uncomparable type. The compiler will not see it, it knows only the interface. For values from external data, comparing after a type assertion is safer.
What is covered
- What is really being asked
- Base: implementation without a declaration
- Mechanism 1: method sets — where the silence ends
- Mechanism 2: the dynamic type and the dynamic value
- Mechanism 3: the nil that is not nil
- Mechanism 4: a type assertion is a pointer comparison
- Mechanism 5: how this gets used
- Deeper: how it is built inside
- How to answer in an interview
- Next they ask
- Common misconceptions
- Practice
- Knowledge check
Sources & further reading
4 SOURCES
- The Go specification — Interface types, Type assertions, Comparison operatorsOfficial documentation. The rules every behaviour here is derived from. On comparison: «Two interface values are equal if they have identical dynamic types and equal dynamic values or if both have the value nil». On type assertions: «For an expression x of interface type, but not a type parameter, and a type T, the primary expression x.(T) asserts that x is not nil and that the value stored in x is of type T».https://go.dev/ref/spec#Interface_types
- Go FAQ — Why is my nil error value not equal to nil?Official documentation. The answer to the most common interview question, given by the language authors themselves: «Under the covers, interfaces are implemented as two elements, a type T and a value V», and then the decisive part: «An interface value is nil only if the V and T are both unset».https://go.dev/doc/faq#nil_error
- The Laws of Reflection — the Go blogOfficial documentation. The wording about the representation of an interface variable, from which this whole topic grows: «a variable of interface type stores a pair: the concrete value assigned to the variable, and that value's type descriptor».https://go.dev/blog/laws-of-reflection
- Effective Go — Interfaces and methodsOfficial documentation. On why interfaces are small in the first place: «Interfaces in Go provide a way to specify the behavior of an object: if something can do this, then it can be used here». Hence the idiom of declaring an interface on the consumer's side rather than beside the implementation.https://go.dev/doc/effective_go#interfaces_and_types