Structs, Methods & Interfaces
Irbisa · cheatsheetSeptember 19, 2026

Structs, Methods & Interfaces

Structs, embedding, method sets, implicit interfaces, type switches

Junior Developer16 itemscompressed for a skim
  1. 01

    After a dependency bump the build fails with too few values in struct literal of type url.URL. Which form of composite literal did the code use, and what should it have used?

    Easy

    Unkeyed (positional) composite literals pin your code to another package's field order and count, so any field added upstream breaks the build — write T{Field: v} instead.

    package geo
    
    import (
    	"bytes"
    	"net"
    	"sync"
    …
  2. 02

    map[Point]int compiles until someone adds a Tags []string field to Point, and then a == you have elsewhere starts panicking at run time instead of failing to build. What is the rule?

    Easy

    A struct is comparable exactly when every one of its fields is comparable — one slice, map or func field removes ==, map-key use and the comparable constraint from the whole struct.

    package cat
    
    import "fmt"
    
    // Comparable: every field is comparable, so this works as a map key.
    type Point struct{ X, Y int }
    …
  3. 03

    func (u User) String() string { return fmt.Sprintf("User %v", u) } compiles, passes review, and kills the process with fatal error: stack overflow. What happened, and what would have caught it before merge?

    Easy

    %v on the receiver re-enters String()fmt sees a value implementing fmt.Stringer and calls its method, so the method calls itself until the goroutine stack hits the 1 GB limit.

    package user
    
    import "fmt"
    
    type User struct {
    	Name  string
    …
  4. 04

    Middleware reads the caller's id with id := ctx.Value(userKey).(string), and one unauthenticated request takes the whole process down. Which form should it use, and what exactly does the second result tell you?

    Easy

    The one-result assertion panics when the dynamic type does not matchinterface conversion: interface {} is nil, not string — while the comma-ok form id, ok := v.(string) hands back the zero value and false.

    package mw
    
    import (
    	"context"
    	"net/http"
    )
    …
  5. 05

    A test calls c.Inc() a hundred times and c.n is still zero; the same afternoon CI starts failing with passes lock by value: Registry contains sync.Mutex. What single rule is behind both?

    Medium

    A value receiver is a copyfunc (c Counter) Inc() increments a temporary that dies at return, and for the same reason copying a struct that contains a sync.Mutex copies the lock itself.

    package counter
    
    import "sync"
    
    type Counter struct {
    	mu sync.Mutex // named, not embedded: Lock/Unlock stay private
    …
  6. 06

    Two lines apart, w.Run() compiles and var _ Handler = Worker{} is rejected with Worker does not implement Handler (method Run has pointer receiver). Why is the interface stricter than the direct call?

    Medium

    Because w.Run() is sugar for (&w).Run() and only works while w is addressable — the copy an interface stores has no address, so the method set of T holds only value-receiver methods while *T's holds both.

    package work
    
    type Worker struct{ done int }
    
    // Pointer receiver => Run is in the method set of *Worker only.
    func (w *Worker) Run() { w.done++ }
    …
  7. 07

    sessions["abc"].TouchedAt = now is rejected with cannot assign to struct field sessions["abc"].TouchedAt in map, while the identical line on a slice element compiles. What separates the two?

    Medium

    Map elements are not addressable — a map may grow and move its entries, so Go refuses to hand out a pointer into one, while a slice element sits at a fixed offset in a backing array and &s[i] stays valid.

    package sess
    
    import "time"
    
    type Session struct {
    	Hits      int
    …
  8. 08

    Outer embeds Base, you add a Name() method to Outer to override the embedded one, and the promoted Describe() — which calls Name() internally — still returns the base name. What did you assume Go does that it does not?

    Medium

    Embedding is composition, not inheritance: Describe was compiled against Base's own Name, and a promoted method runs with the embedded field as its receiver, so there is no dispatch back up to the outer type.

    package emb
    
    import "fmt"
    
    type Base struct{ id int }
    …
  9. 09

    Your struct embeds two types that both carry a Name field, and the package builds fine — until someone writes t.Name and the compiler answers ambiguous selector t.Name. What promotion rules produced both outcomes?

    Medium

    Selectors resolve by shallowest depth wins, and a tie is an error only where the ambiguous selector is written — which is why two Names at the same depth are legal in the type declaration and illegal in one expression.

    package ambig
    
    import "fmt"
    
    type Audit struct{ Name string }
    type Owner struct{ Name string }
    …
  10. 10

    The response comes back as {"id":1,"Nick":""} — the id tag worked, the nick one did not, and nothing in the build complained. What is a struct tag, and what would have caught the typo?

    Medium

    A struct tag is an opaque string literal stored in the type's metadata, checked by nothing at compile time — so json: "nick" with a space after the colon fails reflect.StructTag.Get and encoding/json silently falls back to the field name.

    package api
    
    import (
    	"encoding/json"
    	"fmt"
    	"time"
    …
  11. 11

    You rename a method on *PostgresStore, its own package still builds, and the failure surfaces as does not implement Store in a service three directories away. How do you make the type's own package fail first?

    Medium

    Add the compile-time assertion var _ Store = (*PostgresStore)(nil) — interface satisfaction in Go is implicit and structural, so nothing ties a type to an interface until some assignment forces the check.

    package store
    
    import "context"
    
    // The consumer declares what it needs; the implementer declares nothing.
    type Store interface {
    …
  12. 12

    A package exports type Store interface with fourteen methods and exactly one implementation, and every test in the repo drags along a 200-line fake. Which two Go conventions were skipped?

    Medium

    The interface was declared by the producer instead of the consumer, and sized to the implementation instead of the call site — Go's rule is accept interfaces, return structs, and keep the interface as small as the function that takes it.

    package report
    
    import (
    	"context"
    	"io"
    )
    …
  13. 13

    A helper returns io.Writer; on the path with nothing to write to it returns a *bytes.Buffer that happens to be nil. The caller's if w != nil passes and w.Write segfaults. Explain the interface value.

    Hard

    An interface value is two words — a type descriptor and a data pointer — and it is nil only when both are; assigning a nil *bytes.Buffer fills the type word, so the interface is non-nil while its payload is not.

    package sink
    
    import (
    	"bytes"
    	"io"
    )
    …
  14. 14

    Your hot decode loop got noticeably slower after a refactor changed one parameter from *Decoder to an interface, and the profile now shows runtime.convT64 and runtime.mallocgc that were not there before. What does an interface value cost?

    Hard

    An interface value is two words, and converting anything that is not already a pointer into one usually heap-allocates a copy — on top of which every call through it is an indirect jump the compiler can rarely inline.

    package hot
    
    import "testing"
    
    type Decoder struct{ n int }
    …
  15. 15

    Two structs with exactly the same four fields measure 24 and 16 bytes under unsafe.Sizeof, and at ten million rows that difference is 80 MB of live heap. What decides the layout, and where does the rule bite in a way reordering cannot fix?

    Hard

    Each field is placed at an offset that is a multiple of its own alignment, strictly in declaration order — the compiler never reorders fields, so the padding is yours to remove by sorting from widest to narrowest.

    package layout
    
    import (
    	"fmt"
    	"unsafe"
    )
    …
  16. 16

    A switch v := x.(type) puts case nil first and case Named after it; a (*File)(nil) argument sails past the nil case, enters case Named and panics. Walk through what the switch actually tested.

    Hard

    A type switch tests the interface's dynamic type, and case nil matches only when there is no dynamic type at all — a (*File)(nil) has one, so it matches case Named and then dereferences a nil receiver.

    package tsw
    
    import "fmt"
    
    type Named interface{ Name() string }
    …