The idea in one minute#
A struct groups fields. A method is a function attached to a type. An interface is a
set of method signatures, and a type satisfies it simply by having those methods — there is
no implements keyword. Go has no classes and no inheritance; you build larger types by
embedding smaller ones.
The practical consequence: interfaces are defined by the code that uses a behaviour, are usually one or two methods long, and existing types satisfy them without knowing they exist.
An analogy#
A wall socket. The socket does not care what brand the appliance is or what it does — only that its plug has the right pins. Any device with that plug works, including ones built before the socket was installed. An interface is the socket shape; a type’s methods are the plug.
A picture#
flowchart LR
subgraph IF["interface Embedder"]
M["Embed(text string) []float32"]
end
A["type LocalModel struct<br/>func (m *LocalModel) Embed(...)"] -.->|"has the method,<br/>so it satisfies"| IF
B["type RemoteAPI struct<br/>func (r RemoteAPI) Embed(...)"] -.-> IF
C["type fakeEmbedder struct<br/>in a test"] -.-> IF
IF --> USE["func Index(e Embedder, docs []string)<br/>depends only on the behaviour"]
class M neutral
class A,B,C compute
class USE queueHow it really works#
Structs#
type Model struct {
Name string
Params int64
Dtype string
weights []float32 // lower case: private to the package
}
m := Model{Name: "tiny", Params: 1_000_000} // unnamed fields get zero values
p := &m // pointer to it
p.Params++ // automatic dereference: no -> operatorStructs are values (I.02): assigning one copies every field. Two structs are comparable with
== if all their fields are.
Methods and receivers#
func (m Model) SizeBytes() int64 { return m.Params * 4 } // value receiver: works on a copy
func (m *Model) Load(w []float32) { m.weights = w } // pointer receiver: can modify| Use a pointer receiver when | Use a value receiver when |
|---|---|
| The method modifies the receiver | The type is small and immutable in spirit (a point, a timestamp) |
| The struct is large (copying costs) | You want copies to be independent |
It contains a sync.Mutex or similar that must not be copied |
Be consistent within a type: if any method needs a pointer receiver, give them all one.
Method sets decide interface satisfaction: a value of type T has only the value-receiver
methods; a *T has both. So if Load has a pointer receiver, *Model satisfies an interface
requiring Load and plain Model does not.
Embedding#
type Timestamps struct{ Created, Updated time.Time }
type Document struct {
Timestamps // embedded: no field name
Title string
}
d.Created // promoted field
d.Timestamps.Created // the same thing, spelled outEmbedding is composition with promoted names, not inheritance: Document has a
Timestamps; a function taking Timestamps does not accept a Document. Embedding an
interface in a struct is a common way to wrap an implementation and override one method.
Interfaces#
type Embedder interface {
Embed(text string) ([]float32, error)
}Guidelines that the standard library follows:
- Keep them small.
io.Readerandio.Writerhave one method each and are the most reused abstractions in Go. - Define them where they are consumed, not next to the implementation. The consumer knows which behaviour it needs.
- Accept interfaces, return concrete types. Callers get flexibility on input and full information on output.
anyis an alias forinterface{}: the empty interface, satisfied by everything.
Getting the concrete type back:
if lm, ok := e.(*LocalModel); ok { /* ... */ } // type assertion, safe form
switch v := x.(type) { // type switch
case string: /* v is a string */
case fmt.Stringer: /* v has String() */
}A compile-time check that a type satisfies an interface, with no runtime cost:
var _ Embedder = (*LocalModel)(nil)The nil interface trap, in one line#
An interface value is nil only when it holds no type and no value. A nil *LocalModel
stored in an Embedder produces an interface that is not nil. The classic bug is a function
that returns a nil *MyError as error. II.05 draws why; the fix is to return a literal nil.
Code#
// shapes.go — value vs pointer receivers, embedding, and implicit interface satisfaction.
package main
import (
"fmt"
"math"
"strings"
)
type Embedder interface {
Embed(text string) []float32
}
// HashEmbedder is a toy: it turns text into a small vector by hashing words.
type HashEmbedder struct {
Dim int
calls int
}
func (h *HashEmbedder) Embed(text string) []float32 { // pointer receiver: it counts calls
h.calls++
v := make([]float32, h.Dim)
for _, w := range strings.Fields(strings.ToLower(text)) {
sum := 0
for _, c := range w {
sum = sum*31 + int(c)
}
v[sum%h.Dim]++
}
return v
}
// Logged wraps any Embedder by embedding the interface and overriding the method.
type Logged struct {
Embedder
Log []string
}
func (l *Logged) Embed(text string) []float32 {
l.Log = append(l.Log, text)
return l.Embedder.Embed(text)
}
type Vec []float32
func (v Vec) Norm() float64 { // value receiver on a slice type: cheap, the header is copied
s := 0.0
for _, x := range v {
s += float64(x) * float64(x)
}
return math.Sqrt(s)
}
func cosine(a, b Vec) float64 {
dot := 0.0
for i := range a {
dot += float64(a[i]) * float64(b[i])
}
return dot / (a.Norm() * b.Norm())
}
// Compile-time proof that the types satisfy the interface.
var (
_ Embedder = (*HashEmbedder)(nil)
_ Embedder = (*Logged)(nil)
)
func main() {
base := &HashEmbedder{Dim: 16}
var e Embedder = &Logged{Embedder: base}
a := Vec(e.Embed("the cat sat on the mat"))
b := Vec(e.Embed("a cat sat on a mat"))
c := Vec(e.Embed("quarterly revenue grew"))
fmt.Printf("similar sentences: %.2f\n", cosine(a, b))
fmt.Printf("unrelated sentences: %.2f\n", cosine(a, c))
if l, ok := e.(*Logged); ok {
fmt.Println("wrapper logged", len(l.Log), "calls; inner counted", base.calls)
}
// Method sets: the value type does NOT satisfy the interface.
// var bad Embedder = HashEmbedder{Dim: 4} // compile error: Embed has a pointer receiver
fmt.Printf("dynamic type inside the interface: %T\n", e)
}Remember this#
- A type satisfies an interface by having its methods. Nothing is declared.
- Pointer receivers to modify or to avoid copying; be consistent per type.
- Embedding composes and promotes; it is not inheritance.
- Small interfaces, defined by the consumer. Accept interfaces, return concrete types.
Try it#
- Run
shapes.go. Uncomment thebadline and read the compiler’s explanation. - Add a
CachedEmbedderthat wraps anotherEmbedderand memoizes by text. It should need no change tomainbesides construction. - Write a function returning
errorthat returns a nil*MyErrvariable. Compare the result withnilin the caller. Keep the program for II.05.
Check yourself#
- Which methods are in the method set of
T, and of*T? - Why are Go interfaces usually defined in the consuming package?
- How is embedding different from inheritance?