Structs, Methods & Interfaces
Structs, embedding, method sets, implicit interfaces, type switches
- 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?EasyUnkeyed (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" … - 02
map[Point]intcompiles until someone adds aTags []stringfield toPoint, and then a==you have elsewhere starts panicking at run time instead of failing to build. What is the rule?EasyA struct is comparable exactly when every one of its fields is comparable — one slice, map or func field removes
==, map-key use and thecomparableconstraint 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 } … - 03
func (u User) String() string { return fmt.Sprintf("User %v", u) }compiles, passes review, and kills the process withfatal error: stack overflow. What happened, and what would have caught it before merge?Easy%von the receiver re-entersString()—fmtsees a value implementingfmt.Stringerand 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 … - 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?EasyThe one-result assertion panics when the dynamic type does not match —
interface conversion: interface {} is nil, not string— while the comma-ok formid, ok := v.(string)hands back the zero value andfalse.package mw import ( "context" "net/http" ) … - 05
A test calls
c.Inc()a hundred times andc.nis still zero; the same afternoon CI starts failing withpasses lock by value: Registry contains sync.Mutex. What single rule is behind both?MediumA value receiver is a copy —
func (c Counter) Inc()increments a temporary that dies atreturn, and for the same reason copying a struct that contains async.Mutexcopies the lock itself.package counter import "sync" type Counter struct { mu sync.Mutex // named, not embedded: Lock/Unlock stay private … - 06
Two lines apart,
w.Run()compiles andvar _ Handler = Worker{}is rejected withWorker does not implement Handler (method Run has pointer receiver). Why is the interface stricter than the direct call?MediumBecause
w.Run()is sugar for(&w).Run()and only works whilewis addressable — the copy an interface stores has no address, so the method set ofTholds 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++ } … - 07
sessions["abc"].TouchedAt = nowis rejected withcannot assign to struct field sessions["abc"].TouchedAt in map, while the identical line on a slice element compiles. What separates the two?MediumMap 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 … - 08
OuterembedsBase, you add aName()method toOuterto override the embedded one, and the promotedDescribe()— which callsName()internally — still returns the base name. What did you assume Go does that it does not?MediumEmbedding is composition, not inheritance:
Describewas compiled againstBase's ownName, 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 } … - 09
Your struct embeds two types that both carry a
Namefield, and the package builds fine — until someone writest.Nameand the compiler answersambiguous selector t.Name. What promotion rules produced both outcomes?MediumSelectors 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
The response comes back as
{"id":1,"Nick":""}— theidtag worked, thenickone did not, and nothing in the build complained. What is a struct tag, and what would have caught the typo?MediumA 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 failsreflect.StructTag.Getandencoding/jsonsilently falls back to the field name.package api import ( "encoding/json" "fmt" "time" … - 11
You rename a method on
*PostgresStore, its own package still builds, and the failure surfaces asdoes not implement Storein a service three directories away. How do you make the type's own package fail first?MediumAdd 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
A package exports
type Store interfacewith fourteen methods and exactly one implementation, and every test in the repo drags along a 200-line fake. Which two Go conventions were skipped?MediumThe 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
A helper returns
io.Writer; on the path with nothing to write to it returns a*bytes.Bufferthat happens to be nil. The caller'sif w != nilpasses andw.Writesegfaults. Explain the interface value.HardAn interface value is two words — a type descriptor and a data pointer — and it is nil only when both are; assigning a nil
*bytes.Bufferfills the type word, so the interface is non-nil while its payload is not.package sink import ( "bytes" "io" ) … - 14
Your hot decode loop got noticeably slower after a refactor changed one parameter from
*Decoderto an interface, and the profile now showsruntime.convT64andruntime.mallocgcthat were not there before. What does an interface value cost?HardAn 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
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?HardEach 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
A
switch v := x.(type)putscase nilfirst andcase Namedafter it; a(*File)(nil)argument sails past the nil case, enterscase Namedand panics. Walk through what the switch actually tested.HardA type switch tests the interface's dynamic type, and
case nilmatches only when there is no dynamic type at all — a(*File)(nil)has one, so it matchescase Namedand then dereferences a nil receiver.package tsw import "fmt" type Named interface{ Name() string } …