1. Defining Structs
A struct is a named collection of fields — Go's only real way to group related data together, with none of the class machinery (constructors, access modifiers, inheritance) that comes bundled with it in other languages.
type User struct {
ID int
Name string
Email string
}
func main() {
u := User{ID: 1, Name: "Asha", Email: "asha@example.com"}
fmt.Println(u.Name) // Asha
// a "zero value" struct — every field defaults to its type's zero value
var empty User
fmt.Println(empty) // {0 }
}
There's no separate constructor syntax — the idiomatic pattern for validated or
defaulted construction is a plain function, conventionally named
NewUser, that returns a fully-formed struct (or an error).
2. Methods: Value vs. Pointer Receivers
A method is a function with a receiver — the type it's attached to, written before the function name. The receiver can be a value (a copy) or a pointer (a reference to the original).
func (u User) Greeting() string { // value receiver: gets a copy of u
return "Hi, " + u.Name
}
func (u *User) SetEmail(email string) { // pointer receiver: mutates the original
u.Email = email
}
func main() {
u := User{Name: "Asha"}
fmt.Println(u.Greeting()) // "Hi, Asha" — reading, a copy is fine
u.SetEmail("asha@new.com") // Go automatically takes &u here
fmt.Println(u.Email) // "asha@new.com" — the original was mutated
}
The rule of thumb: use a pointer receiver whenever the method needs to mutate the receiver, or the struct is large enough that copying it on every call is wasteful. Mixing receiver types on the same struct's methods is legal but considered poor style — pick one and stay consistent.
3. Interfaces & Structural Typing
An interface is a set of method signatures. A type satisfies it automatically the
moment it implements those methods — there's no implements keyword,
and a type can satisfy an interface it was never written with in mind.
type Greeter interface {
Greeting() string
}
func Welcome(g Greeter) {
fmt.Println(g.Greeting())
}
func main() {
u := User{Name: "Asha"}
Welcome(u) // User satisfies Greeter just by having a Greeting() method
}
This is Go's version of duck typing, checked at compile time — Welcome
accepts anything with a Greeting() string method, whether or not that
type's author ever heard of Greeter. It's what makes small, focused
interfaces (often just one or two methods) so common in idiomatic Go: a caller only
needs to define the interface it depends on, not coordinate with the implementer.
4. Composition over Inheritance
Instead of subclassing, Go lets a struct embed another — the outer struct gains the inner struct's fields and methods directly, without an explicit delegation step.
type Base struct {
ID int
}
func (b Base) Describe() string {
return fmt.Sprintf("entity #%d", b.ID)
}
type Product struct {
Base // embedded, no field name — "Base" is implied
Name string
Price float64
}
func main() {
p := Product{Base: Base{ID: 7}, Name: "Widget", Price: 9.99}
fmt.Println(p.ID) // 7 — promoted field, accessed directly
fmt.Println(p.Describe()) // "entity #7" — promoted method
}
This is composition, not inheritance — Product doesn't "become a"
Base, it just has one, with its exported fields and methods promoted
up for convenience. Combined with small interfaces, this is how Go achieves what
inheritance-heavy languages use class hierarchies for, without the fragility of a
deep inheritance tree.
5. Hands-on Exercise
Model a small inventory system
Structs, methods, an interface, and embedding, all in one small program.
Requirements:
- A
Itemstruct withName string,Quantity int, andPrice float64fields. - A pointer-receiver method
Restock(n int)that adds toQuantity, and a value-receiver methodTotal() float64that returnsPrice * float64(Quantity). - A
Describerinterface with aDescribe() stringmethod, satisfied byItemwithout ever mentioning the interface inItem's own definition. - A
PerishableItemstruct that embedsItemand adds anExpiresInDays intfield, confirming its promotedTotal()still works correctly. - A function
PrintDescription(d Describer)called with both anItemand aPerishableItem.
If Restock doesn't seem to actually change the quantity when called, check whether you're calling it on a value or a pointer, and whether the method itself has a pointer receiver — a value-receiver method mutates only its own copy, so Restock needs *Item as its receiver to have any lasting effect.
6. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why does SetEmail(email string) need a pointer receiver to actually mutate a User, where Greeting() does not?
Why does SetEmail(email string) need a pointer receiver to actually mutate a User, where Greeting() does not?
A value receiver method operates on a copy of the struct — any change it makes is local to that copy and disappears when the method returns. A pointer receiver operates on the original via a reference, so a mutation there is visible after the call. Greeting() only reads, so a copy is harmless and even simpler; SetEmail needs to persist a change, so it needs the pointer.
Q2
What does it mean for Go's interfaces to use "structural typing," and how is that different from an implements-keyword language?
What does it mean for Go's interfaces to use "structural typing," and how is that different from an implements-keyword language?
A type satisfies a Go interface automatically the moment its method set matches the interface's — there's no explicit declaration linking the two, and a type's author doesn't even need to know the interface exists. In an implements-keyword language, satisfying an interface requires the type to explicitly declare that relationship at definition time.
Q3
In the embedding example, why does p.Describe() work even though Product never defines a Describe method itself?
In the embedding example, why does p.Describe() work even though Product never defines a Describe method itself?
Embedding Base inside Product promotes Base's exported fields and methods up to Product automatically — Product effectively gains Describe() for free by containing a Base, without Product having to redeclare or delegate to it manually.
Q4
Why might mixing value and pointer receivers on the same struct's methods cause subtle bugs, even though Go allows it?
Why might mixing value and pointer receivers on the same struct's methods cause subtle bugs, even though Go allows it?
A caller working with a value (not a pointer) can call a pointer-receiver method directly only if the value is addressable — but a value stored in certain contexts (like a map value, or a non-addressable temporary) isn't addressable and won't compile at all for a pointer-receiver method. Inconsistent receiver types also make it unpredictable, at a glance, whether a given method mutates the original or a copy — which is why idiomatic Go picks one receiver kind per type and sticks with it.