Go has no classes and no inheritance. Methods attach to any named type, encapsulation works per package through capitalised names, reuse comes from composition and embedding, and polymorphism comes from interfaces.
Encapsulation: an identifier starting with a lowercase letter is visible only inside its package; the unit of encapsulation is the package, not the type. There is no private or protected; two types in one package see each other's unexported fields.
Methods: declared outside the type with a receiver, func (a *Account) Deposit(n int), on any named type of the same package, not only on structs.
No inheritance: embedding promotes the fields and methods of another type, but the outer type is not a subtype: an Outer cannot be passed where a Base is expected, and nothing is overridden.
Polymorphism: through interfaces, satisfied implicitly: a function that takes io.Writer works with a file, a buffer or a network connection.
GoJunior
Structs, Methods & Interfaces
Structs, embedding, method sets, implicit interfaces, type switches
Questions closed
0 / 24
- 0Review
- 6Asked often
Questions
A keyed literal names the fields it sets and leaves the rest at their zero value; a positional one must list every field in declaration order. Write keyed literals: a field added or reordered later breaks or silently changes every positional one.
A field added upstream turns each positional literal into too few values in struct literal. Two fields of the same type swapped upstream still compile, with the values crossed.
go vet's composites check flags positional literals of types from other packages. For a small local type, Point{1, 2} stays idiomatic.
Inside slice and map literals the element type can be left out: []Point{{1, 2}, {3, 4}}.
&T{...} takes the address of a literal directly, the usual way to build a *T.
A method can only be declared on a named type defined in the same package, so time.Time and int are closed to you. Define your own type (type Celsius float64, or a struct that embeds time.Time) and put the method there.
The rule keeps method sets fixed: importing a package never changes how another package's type behaves, and two packages cannot add conflicting String() methods to one type.
A defined type such as type Deadline time.Time keeps the fields but has none of the original methods; convert back with time.Time(d).
Embedding, struct{ time.Time }, keeps every original method through promotion and lets you add new ones.
Often a plain function is the honest answer: func IsWeekend(t time.Time) bool.
A struct is comparable only if every one of its fields is comparable, so one slice, map or func field takes == and map-key use away from the whole struct, and the compiler tells you so.
Comparable: numbers, strings, bools, pointers, channels, interfaces (== panics at run time if the dynamic values are not comparable), arrays of comparable elements, and structs made of those. == compares field by field.
Not comparable: slices, maps and funcs; they compare only against nil. That is why [4]string works in a map key and []string does not.
The errors read invalid map key type Loose and struct containing []string cannot be compared.
Pointers compare by address: two *Point pointing at equal values are not ==.
When you need equality anyway: slices.Equal for the field, reflect.DeepEqual in tests, or a separate key struct with only comparable fields.
struct{} has no fields and takes zero bytes, so it is the type for values that carry no data: set members in map[K]struct{}, signals on chan struct{}, and types that exist only for their methods.
Sets: seen[k] = struct{}{} stores the key only, _, ok := seen[k] checks membership. Unlike map[K]bool there is no third state to misread.
Signals: done := make(chan struct{}) and close(done) wake every receiver; the type says there is no payload, as with ctx.Done().
Method holders: type noopLogger struct{} satisfies an interface with no state and costs nothing to create.
Distinct zero-size values may share one address, so never compare their pointers.
An interface is a set of method signatures, and any type that has those methods satisfies it automatically; there is no implements declaration. The type and the interface need not know about each other, so you can write an interface for a type you do not own.
The compiler checks satisfaction where a value is assigned to the interface type, so it is still static typing, often called structural or static duck typing.
In Java a class must name its interfaces, so a third-party class cannot satisfy yours without an adapter. In Go *os.File, *bytes.Buffer and net.Conn all satisfy io.Reader without any of them mentioning your code.
That lets the consumer declare the interface it needs instead of the producer declaring what it offers.
A call through an interface variable is dispatched at run time to the method of the value stored in it.
Method names and signatures must match exactly. A method with a pointer receiver belongs only to *T, so a plain T value does not satisfy such an interface.
any is an alias for interface{}, the interface with no methods, so every type satisfies it. Use it when values of genuinely unknown type pass through (fmt, JSON, context values) and generics when the type varies between callers but is fixed for each call.
To use a value stored in any you need a type assertion or a type switch; the compiler checks nothing, so a wrong guess becomes a panic at run time.
[]any is not []int: converting takes a loop, and each element is boxed into an interface, often with an allocation.
A type parameter keeps the type: func Max[T cmp.Ordered](xs []T) T returns a T and rejects a slice of mixed types at compile time.
Legitimate any: fmt.Println(a ...any), decoding JSON of unknown shape into map[string]any, heterogeneous containers.
Rule of thumb: if every caller would write a type assertion right after the call, you wanted a type parameter.
A type assertion checks for one type: s := v.(string) panics if the value inside is not a string, while s, ok := v.(string) gives the zero value and false. A type switch tests several types in one statement and binds the value with the matching type in each case.
The panic names both types: interface conversion: interface {} is int, not string.
With a concrete T, v.(T) asks for exactly that type: an int inside never passes .(int64). With an interface type it asks whether the value implements it, which is how you probe optional behaviour such as w.(http.Flusher).
In switch x := v.(type) a case with one type gives x that type; a case listing several (case int, int64) leaves x as the interface type.
case nil matches only an interface with no value at all.
Cases are tried top to bottom, so when interface cases overlap the first match wins.
fmt.Stringer is the one-method interface String() string, and fmt calls it for %v, %s and Println. Formatting the receiver with %v inside String therefore calls String again, and again, until the stack overflows.
Break the loop with a defined type that has no methods: type plainUser User, then format plainUser(u). A new defined type does not inherit methods.
Or format only the fields you want, which is also a chance to keep secrets such as tokens out of logs.
go vet reports it as causes recursive (user.User).String method call, and go test runs that check.
With a pointer receiver, String belongs to *T only: printing a T value shows the raw fields, printing &t uses String.
If a type also implements error, fmt uses Error() instead of String().
Use a pointer receiver when the method changes the receiver, when the struct holds something that must not be copied such as a sync.Mutex, or when copying it is measurably expensive. Otherwise a value receiver is fine; it always works on a copy.
func (c Counter) Inc() { c.n++ } compiles and does nothing visible: it increments a copy that is gone at return.
Calls are forgiving: on a variable, c.Inc() means (&c).Inc(), and a pointer can call value methods. Interfaces are not forgiving; that is the method-set rule.
Be consistent: if one method needs a pointer receiver, give all of them pointer receivers, so the type is always passed around as *T.
Value receivers suit small immutable values such as time.Time or a Point: copying a few words is cheap, and copies are safe to share between goroutines.
"Large" is decided by a profile, not a byte count: a pointer can make the value escape to the heap.
The method set of T holds only its value-receiver methods; the method set of *T holds both kinds. Interface satisfaction reads the method set, so a Worker value has no Run and only *Worker implements the interface.
A direct call w.Run() works because w is an addressable variable and Go rewrites it to (&w).Run(). A value stored in an interface is a hidden copy, so Go cannot do that rewrite.
If it did, Run would change the hidden copy and you could never read the change back: a silent no-op, worse than a compile error.
The error is Worker does not implement Handler (method Run has pointer receiver). Pass &Worker{} and have the constructor return *Worker.
Map elements and function results are not addressable either: m["a"].Run() fails with cannot call pointer method Run on Worker.
Value-receiver methods are in both sets, so a type with only value receivers fits as T and as *T.
A constructor is an ordinary function, by convention NewT (or New when the package has one main type), that validates input, fills defaults and returns the value, often with an error. Functional options pass optional settings as a variadic list of func(*T) values.
First try to make the zero value useful, as bytes.Buffer and sync.Mutex do; then no constructor is needed.
Return *T when the type has pointer-receiver methods, holds a mutex or needs an identity; return T for small values like time.Time.
Return (*T, error) when construction can fail. A MustT variant that panics is for package-level variables and tests.
Options read like named arguments (NewServer(addr, WithTimeout(5*time.Second))), defaults live in one place, and a new option breaks no caller.
The simpler alternative is a config struct, NewServer(Config{...}). Options pay off in public APIs with many rarely-set knobs.
It prints outer, I am base, base. Embedding is composition, not inheritance: o.Describe() is just o.Base.Describe(), its receiver is a Base, and inside it b.Name() is Base.Name; nothing dispatches up to Outer.
Promotion: the fields and methods of an embedded type can be used as if declared on the outer type; the compiler rewrites o.Describe() to o.Base.Describe().
Shadowing, not overriding: Outer.Name sits at depth 0 and hides the promoted Base.Name for selectors on Outer, but the methods of Base still call their own.
Ties: if two embedded types at the same depth both have Name, neither is promoted. The struct compiles; o.Name fails with ambiguous selector o.Name only where it is written, and the struct no longer satisfies an interface that needs Name.
Dispatch the Go way: take an interface: func Describe(n interface{ Name() string }) calls whichever Name the value has.
Embedding also widens your API: embed sync.Mutex and every caller can Lock your type. Use a named field when you want reuse without exposure.
The struct gets every method of the interface, forwarded to whatever value the field holds, so it satisfies the interface and you redefine only the methods you want to change. sort.Reverse works exactly this way: it embeds sort.Interface and redefines Less.
Decorators: to record the status code of a response, embed http.ResponseWriter and redefine WriteHeader; everything else forwards.
Partial fakes in tests: embed a large interface in the fake and implement only the two methods the test touches.
The catch: a method you did not redefine, called while the embedded field is nil, panics with a nil pointer dereference at run time. In a fake that is a useful loud failure; in production code always set the field.
Hidden optional interfaces: the wrapper has only the embedded interface's methods, so w.(http.Flusher) fails on it. Add an Unwrap() http.ResponseWriter method and http.ResponseController reaches Flush underneath.
Embedding time.Time promotes its MarshalJSON to Event, so Event itself implements json.Marshaler and json.Marshal writes only the timestamp, "2026-10-15T09:00:00Z". name and user_id are silently dropped.
Why: promoted methods join the outer type's method set, so Event satisfies every interface time.Time does: json.Marshaler, json.Unmarshaler, fmt.Stringer. encoding/json checks for Marshaler before it looks at any field.
The other half: UnmarshalJSON is promoted too: decoding {"name":"login"} into an Event fails with Time.UnmarshalJSON: input is not a JSON string, and fmt.Println(e) prints only the time.
Fix: a named field, At time.Time. Methods of a named field are not promoted, so Event has no MarshalJSON and is encoded field by field.
Catch it: a round-trip test (Marshal, Unmarshal, compare) for every wire type. In review, embedding a type that has MarshalJSON or String into a DTO is a red flag.
A struct tag is a string attached to a field, kept in the type's metadata and read at run time through reflect.StructField.Tag. The compiler does not check it, so a malformed tag is silently ignored and the package falls back to its defaults.
The format is strict: space-separated key:"value" pairs in backquotes, no space after the colon, for example json:"id" db:"user_id" . Keys such as json and db are conventions of each package, not language features.
A package reads its key with field.Tag.Get("json"); Lookup also reports whether the key is present.
With json: "nick" (a space after the colon) the tag does not parse, Get returns "", and encoding/json uses the field name Nick as the key.
go vet's structtag check reports bad syntax for struct tag value and duplicate JSON keys. go test does not run this check, so CI needs go vet ./....
A misspelt key such as jsno:"id" is valid tag syntax, so even go vet stays silent; only a test of the encoded output catches it.
Add var _ Store = (*PostgresStore)(nil) next to the type. It assigns a typed nil pointer to the interface, so the build fails in the type's own package the moment a method is renamed or its signature drifts.
Without it the check happens only where a value is used as a Store, often in another package that the author of the change never builds, so the error lands far from its cause.
No method runs and nothing is allocated; the blank identifier _ keeps the line out of the package API.
Use the pointer form when any method has a pointer receiver: var _ Store = PostgresStore{} would fail even when everything is fine.
Put it in a regular file, not a _test.go file, so go build catches the drift.
Read the error past its first line: (missing method Put) or (wrong type for method Get) names exactly what changed.
Declare an interface in the package that uses it, with only the methods that package calls, and have constructors return concrete types. The consumer then depends on a small contract, and the producer stays free to add methods without breaking anyone.
Consumer side: a report package that only calls Get declares type userGetter interface{ Get(ctx context.Context, id int64) (User, error) }. Its test fake is five lines, and a new method on the store breaks nobody.
Return structs: NewPostgresStore returns *PostgresStore. Callers see every method and its docs, use it directly if they want, and are not forced through an abstraction they did not ask for.
Small interfaces compose: io.Reader and io.Writer have one method each, and io.ReadWriteCloser embeds them. The bigger the interface, the weaker the abstraction.
The smell: an exported 14-method interface next to its only implementation, plus a mock generated from it. That is a header file, not an abstraction.
Exceptions: return an interface when the concrete type must stay hidden or depends on input: error, hash.Hash from sha256.New(), net.Conn from net.Dial.
It prints 0 and then 3. A method call on a pointer is an ordinary function call with the pointer as its first argument, so a nil receiver is legal; it panics only when the method dereferences it, and Sum checks for nil first.
t.Sum() with var t *Tree is (*Tree).Sum(t): nothing is dereferenced at the call site.
A usable nil receiver is a deliberate design: an empty tree or list that needs no special case, or protobuf-generated getters such as GetName() that return "" on a nil message.
A value-receiver method is different: calling it through a nil *T must copy *t first, and that copy panics with invalid memory address or nil pointer dereference.
Calling a method on a nil interface (one with no type inside) always panics: there is no method to dispatch to.
An interface value holds two things (the dynamic type and a pointer to the value) and equals nil only when both are empty. Returning a nil *bytes.Buffer as io.Writer fills in the type, so w == nil is false, and Write then runs on a nil receiver.
w != nil is honest: the interface carries "a *bytes.Buffer that is nil". The call dispatches fine to (*bytes.Buffer).Write, which dereferences its receiver and panics.
The shape is always the same: a variable of a concrete pointer type, assigned on some branches only, returned through an interface result. With error it is the classic err != nil for a nil *MyError.
Fix it at the source: if !enabled { return nil }; an untyped nil leaves both parts empty. Better, return io.Discard, and the caller needs no check at all.
A type switch follows the same rule: a nil *bytes.Buffer skips case nil and lands in case *bytes.Buffer.
Checking the value inside needs reflection, reflect.ValueOf(w).IsNil(), which is a sign the API is wrong, not a fix.
Two interface values are equal when both are nil, or when they hold the same dynamic type and equal values. If the types match but are not comparable, such as slices or maps, == compiles and then panics with comparing uncomparable type []int.
Different dynamic types are simply unequal, with no conversion: any(1) == any(int64(1)) is false.
Pointers inside compare by address: two errors.New("x") values are different, which is why sentinel errors are package-level variables.
The compiler cannot see the dynamic types, so the check moves to run time. Interface-typed map keys share the trap: m[any([]int{1})] = 1 panics with hash of unhashable type []int.
Comparing an interface with a concrete value, as in err == ErrNotFound, converts the concrete side to the interface type first.
A struct inside is compared field by field, so a struct with a slice field panics just like a bare slice.
Get has a value receiver, so each call copies the whole Cache, mutex included, and locks the copy. Get and Set never exclude each other on the shared map, which crashes with concurrent map read and map write; a copy taken while Set holds the lock blocks forever.
Shared map, separate lock: a map value is a pointer to the runtime's hash table, so the copy reaches the same data. A sync.Mutex is a plain struct of two integers, so its copy is an unrelated lock.
Why it hangs: copying the mutex while Set holds it copies the locked state, and Get then waits on a lock nobody will ever release. In a small program that ends in all goroutines are asleep - deadlock!; in a server, requests hang and goroutines leak.
Fix: a pointer receiver on Get too, so it locks the caller's mutex. A type that holds a mutex gets pointer receivers on every method and travels as *Cache.
Catch it: go vet reports Get passes lock by value: <pkg>.Cache contains sync.Mutex (the copylocks check; go test does not run it, so put go vet in CI). go test -race shows the data race even on runs that do not crash.
An interface value is two machine words: a type word and a data pointer. For any (an eface) the first word is the dynamic type; for an interface with methods (an iface) it points to an itab holding the type and the method table. A non-pointer value must be copied to get an address.
Layout
itab pairs one interface type with one concrete type and stores the function pointers of the interface's methods. The compiler emits itabs it can see statically; others, needed by assertions and interface-to-interface conversions, are built once and cached in a global table.
The data word is always a pointer. Pointers, maps, channels and funcs go in directly, so io.Writer(&buf) costs nothing.
Boxing
Anything else is copied into memory the interface points to (runtime.convT64, convTstring, convTslice, or convT plus mallocgc in a profile).
The runtime skips the allocation for zero values and for integers below 256, and escape analysis can keep the copy on the stack when the interface does not escape. -gcflags=-m shows which case you are in.
Dispatch
A call loads itab.fun[i] and jumps through it: a few nanoseconds, but it blocks inlining, and arguments tend to escape because the compiler cannot see the callee.
An assertion to a concrete type compares one word; an assertion to an interface type needs an itab lookup.
PGO (Go 1.21+) devirtualises hot calls whose concrete type dominates the profile.
Keep concrete types on hot paths and interfaces at boundaries, and confirm with go test -bench . -benchmem.
Fields are placed in declaration order, each at an offset that is a multiple of its own alignment. The compiler never reorders them, so you remove the padding yourself by declaring the widest fields first.
On 64-bit platforms an int64 or a pointer must start at a multiple of 8, an int32 at a multiple of 4, a bool anywhere. struct{ A bool; B int64; C bool; D int32 } wastes seven bytes after A and three after C: 24 bytes in total.
Sorted widest to narrowest the same fields take 16. The fieldalignment analyzer from golang.org/x/tools finds these, but go vet does not run it.
A zero-size field placed last costs a whole alignment unit, because a pointer to it would otherwise point past the object into the next allocation.
Worth doing only for types you allocate in bulk: at ten million rows, 8 bytes each is 80 MB of heap. The allocator rounds to size classes, so shrinking 24 bytes to 17 gives back nothing.
Order also serves readability and locking: keep a mutex next to the fields it guards.