PidokuInfra

Structs, Methods and Interfaces

Foundations Beginner 50 min Difficulty 2/5 Topic 04 of 05

Prerequisites 02, 03

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 queue

How it really works#

Structs#

Go
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 -> operator

Structs are values (I.02): assigning one copies every field. Two structs are comparable with == if all their fields are.

Methods and receivers#

Go
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 whenUse a value receiver when
The method modifies the receiverThe 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#

Go
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 out

Embedding 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#

Go
type Embedder interface {
    Embed(text string) ([]float32, error)
}

Guidelines that the standard library follows:

  • Keep them small. io.Reader and io.Writer have 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.
  • any is an alias for interface{}: the empty interface, satisfied by everything.

Getting the concrete type back:

Go
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:

Go
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#

Go
// 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#

  1. Run shapes.go. Uncomment the bad line and read the compiler’s explanation.
  2. Add a CachedEmbedder that wraps another Embedder and memoizes by text. It should need no change to main besides construction.
  3. Write a function returning error that returns a nil *MyErr variable. Compare the result with nil in the caller. Keep the program for II.05.

Check yourself#

  1. Which methods are in the method set of T, and of *T?
  2. Why are Go interfaces usually defined in the consuming package?
  3. How is embedding different from inheritance?

↑↓ navigate↵ openesc close