Go has no classes, no inheritance, and no constructors. It has structs and methods. A struct is a typed collection of fields. A method is a function with a receiver argument. Together, they provide everything you need to model domain objects, encapsulate behavior, and satisfy interfaces -- without a class hierarchy.
Composition replaces inheritance through struct embedding. When you embed a type inside another struct, its methods are promoted to the outer type. This means the outer type automatically satisfies any interface the embedded type satisfies. There is no fragile base class problem, no diamond inheritance, and no confusion about which superclass method gets called. The embedded type's methods always operate on the embedded value, and the outer type can override any promoted method by defining its own.
The choice between pointer and value receivers is one of the most important decisions in Go API design. It affects mutability, interface satisfaction, memory allocation, and whether nil receivers are possible. Struct tags provide metadata for serialization, validation, and database mapping. Constructor functions (the NewX convention) handle initialization logic that zero values cannot express. These mechanisms are simple individually, but combining them well is what makes Go code clean and maintainable.
Master go structs & methods
Take the Go Programming course with hands-on lessons and challenges.
NewServer is a constructor function that enforces required parameters (host, port) and applies sensible defaults for optional ones. It returns a pointer because Start() mutates the struct. Unexported fields (startedAt) are only accessible within the package, providing encapsulation without access modifiers.
Value receivers (Point) work on a copy -- the caller's variable is never changed. Pointer receivers (*Point) modify the original. The general rule: use pointer receivers when the method mutates state or when the struct is large enough that copying is expensive. Use value receivers for small, immutable data. Be consistent within a type -- if one method needs a pointer receiver, give all methods pointer receivers.
Service embeds *Logger and *Metrics, promoting their methods. You call svc.Log() and svc.Inc() directly, as if they were defined on Service. This is composition, not inheritance -- Logger and Metrics are independent, reusable components. Service can override any promoted method by defining its own with the same name.
Struct tags are string literals attached to fields that provide metadata for serialization and other tools. json:"-" excludes Password from JSON output entirely. omitempty skips zero-value fields. Multiple tag systems (json, db, validate) can coexist on the same field. The encoding/json package reads these tags via reflection at runtime.
A value of type T can only call value receivers. A value of type *T can call both pointer and value receivers. This matters for interface satisfaction: Document (value) satisfies Sizer but not StringSizer. *Document (pointer) satisfies both. This is why most Go types use pointer receivers consistently -- it avoids surprising interface satisfaction failures.
Anonymous structs are declared inline without a type name. They are idiomatic for table-driven tests (the []struct pattern is everywhere in Go test files), one-off JSON decoding when you only need a few fields from a large payload, and quick API responses. They avoid polluting the package namespace with types used in only one place.
Mixing pointer and value receivers on the same type without understanding the consequences for interface satisfaction
Be consistent. If any method needs a pointer receiver (because it mutates state or the struct is large), make all methods use pointer receivers. Mixing means values of that type can only satisfy interfaces defined by the value-receiver methods, which leads to confusing compile errors when you pass a value where a pointer is expected.
Embedding a mutex by value and then copying the struct, which copies the mutex state
Always embed sync.Mutex (and types containing one) as a field, not by value in copied structs. If your struct is ever assigned or passed by value, the mutex gets copied in an inconsistent state. Use pointer receivers on all methods of structs containing a mutex, and consider using go vet to detect this.
Using struct embedding to simulate inheritance, then being surprised when the embedded type's methods do not have access to the outer struct's fields
Embedding is delegation, not inheritance. When you call an embedded method, its receiver is the embedded value, not the outer struct. The embedded Logger does not know about the Service that contains it. If you need the outer context, pass it explicitly or use an interface instead of embedding.
Forgetting that json:"-" and json:",omitempty" behave differently -- omitempty still includes the field if it has a non-zero value
json:"-" always excludes the field from JSON output. json:",omitempty" excludes it only when the value is the zero value for its type. For sensitive fields like passwords, always use json:"-". Use omitempty for optional fields that should appear when set.
Go structs and methods replace classes and inheritance with a simpler, more explicit model. Structs hold data, methods add behavior via receivers, and embedding provides composition without the pitfalls of class hierarchies. Pointer receivers enable mutation and consistent interface satisfaction. Struct tags drive serialization and metadata. The NewX constructor pattern handles initialization. These building blocks combine to produce Go's distinctive style: explicit, composable, and free of hidden control flow.
Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.
Interactive lessons and challenges, right in your code editor.
Check the free courses. No credit card.