Structs and Interfaces
Reviewed & published by Brayan K
By the end of this lesson you'll model real-world things with your own struct types, give them behaviour with methods, change them safely with pointers, build bigger types by composition, and see how a type quietly satisfies an interface — the heart of idiomatic Go.
Part of the free Go course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- Define your own struct types with named fields
- Create instances using field names and positional values
- Attach methods, and choose value vs pointer receivers
- Use pointers with & (address) and * (dereference)
- Build bigger types by embedding (composition, not inheritance)
- Understand how a type satisfies an interface automatically
1️⃣ Defining a Struct and Making Instances
A struct (short for "structure") is a type you define to group related values together — like a Person with a Name and an Age. You declare the shape once with type Person struct { ... }, then create as many instances (individual values) as you like. You can fill the fields by name (clearest and safest) or by position (you must list them in the exact field order). Any field you leave out gets its type's zero value — 0 for numbers, "" for strings, false for bools.
package main
import "fmt"
// A struct is a named type that groups related fields together.
// Think of it as a blueprint for one "thing" in your program.
type Person struct {
Name string
Age int
}
func main() {
// 1) Field names (clearest, order-independent, safest):
alice := Person{Name: "Alice", Age: 30}
// 2) Positional (must match the field order exactly):
bob := Person{"Bob", 25}
// 3) Zero value — every field gets its type's default:
var empty Person // Name: "" (empty string), Age: 0
// Read and write fields with a dot.
alice.Age = 31 // it was her birthday
fmt.Println("alice:", alice.Name, alice.Age) // alice: Alice 31
fmt.Println("bob: ", bob.Name, bob.Age) // bob: Bob 25
fmt.Printf("empty: %+v\n", empty) // %+v prints field names
}Your turn. Define a small struct and build one instance using field names. Fill in the blanks marked ___ using the // 👉 hints, then run it and check the output.
package main
import "fmt"
func main() {
// 🎯 YOUR TURN — fill in each ___ then run it.
// 1) Define a Book struct with a Title (string) and Pages (int).
type Book struct {
___ // 👉 Title string
___ // 👉 Pages int
}
// 2) Make a book using FIELD NAMES.
b := Book{___} // 👉 Title: "Go 101", Pages: 200
// 3) Print the title and page count (this line already works).
fmt.Printf("%s has %d pages\n", b.Title, b.Pages)
// ✅ Expected output:
// Go 101 has 200 pages
}2️⃣ Methods: Value vs Pointer Receivers
A method is a function attached to a type. You write it with a receiver in parentheses before the name: func (c Counter) Show(). The receiver type decides whether the method sees the original or a copy. A value receiver (c Counter) gets a copy — perfect for read-only methods, but any change is thrown away. A pointer receiver (c *Counter) works on the original, so changes stick. This value-vs-pointer choice is the single most common Go gotcha, so study both halves of this example.
package main
import "fmt"
type Counter struct {
Count int
}
// VALUE receiver (c Counter): works on a COPY.
// Changes here do NOT affect the original.
func (c Counter) Show() {
fmt.Println("count is", c.Count)
}
// POINTER receiver (c *Counter): works on the ORIGINAL.
// Changes here DO stick.
func (c *Counter) Increment() {
c.Count++ // Go auto-dereferences, so it's c.Count, not (*c).Count
}
func main() {
c := Counter{Count: 0}
c.Increment() // pointer receiver -> modifies c
c.Increment() // count is now 2
c.Show() // value receiver -> reads (a copy of) c
// Rule of thumb: use a POINTER receiver when the method must
// change the struct, OR the struct is large (avoid copying).
}3️⃣ Pointers: & and *
A pointer holds the memory address of a value rather than the value itself. Two operators do all the work: &x takes the address of x (giving a pointer), and *p dereferences the pointer (giving back the value, which you can read or overwrite). Passing a pointer into a function lets that function change your original value. Go is friendly here: it auto-dereferences struct pointers (write u.Name, not (*u).Name) and forbids pointer arithmetic, so you can't accidentally walk off into invalid memory.
package main
import "fmt"
type User struct {
Name string
}
// Taking a *User lets the function modify the caller's struct.
func rename(u *User, name string) {
u.Name = name // Go auto-dereferences struct pointers
}
func main() {
x := 42
p := &x // & takes the ADDRESS of x; p is a *int
fmt.Println(*p) // * DEREFERENCES: read the value at p -> 42
*p = 100 // write through the pointer -> changes x
fmt.Println(x) // 100 — x changed!
user := User{Name: "Alice"}
rename(&user, "Bob") // pass the address so the change sticks
fmt.Println("user:", user.Name) // user: Bob
// Go has no pointer arithmetic (no p++ / p + 4) — memory stays safe.
}Your turn — fix a wallet that won't update. The Deposit method currently can't change the balance. Give it a pointer receiver and pass the right amount.
package main
import "fmt"
type Wallet struct {
Balance int
}
// 🎯 YOUR TURN — make Deposit actually change the wallet.
// 1) Give Deposit a POINTER receiver so the change sticks.
func (w ___) Deposit(amount int) { // 👉 *Wallet
w.Balance += amount
}
func main() {
wallet := Wallet{Balance: 100}
// 2) Add 50 to the wallet.
wallet.Deposit(___) // 👉 50
fmt.Println("balance:", wallet.Balance)
// ✅ Expected output:
// balance: 150
// (With a VALUE receiver you'd wrongly get 100 — try it to see why!)
}4️⃣ Embedding: Composition over Inheritance
Go has no classes and no inheritance. Instead you compose bigger types out of smaller ones by embedding: list a type inside a struct with no field name. The embedded type's fields and methods are then promoted — you can reach them directly on the outer struct. So an Employee that embeds User can use e.Name instead of e.User.Name, and inherits any methods User defines. This is how Go reuses code: build from parts rather than extending a base class.
package main
import "fmt"
type Address struct {
City string
}
// A method on the embedded type comes along for free.
func (a Address) Where() string { return "lives in " + a.City }
type User struct {
Name string
}
// Employee EMBEDS User and Address (no field name = embedded).
// Go has no inheritance — this is composition.
type Employee struct {
User // promotes User's fields/methods
Address // promotes Address's fields/methods
Salary int
}
func main() {
e := Employee{
User: User{Name: "Alice"},
Address: Address{City: "London"},
Salary: 50000,
}
// Promoted: reach Name and City directly, no e.User.Name needed.
fmt.Println(e.Name) // Alice (promoted from User)
fmt.Println(e.City) // London (promoted from Address)
fmt.Println(e.Where()) // lives in London (promoted method)
fmt.Println(e.Salary) // 50000
}5️⃣ A First Look at Interfaces
An interface is a named list of method signatures — a contract that says "any type with these methods qualifies." The big idea in Go: a type satisfies an interface automatically, just by having the right methods. There is no implements keyword and nothing to declare. Below, both Dog and Robot have a Speak() string method, so both are Speakers — and one function can accept either. You'll go much deeper into interfaces in a later lesson; for now, just notice how loose and flexible this makes your code.
package main
import "fmt"
// An interface is a list of method signatures — a contract.
// A type satisfies it just by HAVING those methods. There is no
// "implements" keyword; satisfaction is automatic (structural typing).
type Speaker interface {
Speak() string
}
type Dog struct{ Name string }
type Robot struct{ ID int }
// Dog has a Speak() string method -> Dog satisfies Speaker.
func (d Dog) Speak() string { return d.Name + " says Woof" }
// Robot also has Speak() string -> Robot satisfies Speaker too.
func (r Robot) Speak() string { return fmt.Sprintf("Unit %d: Beep", r.ID) }
// announce accepts ANY Speaker — it doesn't care about the concrete type.
func announce(s Speaker) {
fmt.Println(s.Speak())
}
func main() {
announce(Dog{Name: "Rex"})
announce(Robot{ID: 7})
}Common Errors (and the fix)
- My method ran but the struct didn't change. You used a value receiver, so the method edited a copy. Switch to a pointer receiver: func (w *Wallet) Deposit(...). This is the #1 struct gotcha.
- "invalid operation: a == b (struct ... cannot be compared)". Structs are comparable with == only if every field is comparable. A struct containing a slice, map, or function can't be compared — compare the individual fields you care about instead.
- "panic: runtime error: invalid memory address or nil pointer dereference". You dereferenced a pointer that's nil (e.g. var u *User; u.Name). Create the value first (u := &User{}) or guard with if u != nil { ... }.
- "too few values in Person{...}". Positional literals must list every field in order. Either supply them all, or — better — switch to field names: Person{Name: "Al"}.
- "cannot use Robot ... as Speaker value: missing method Speak". The type doesn't have the exact method the interface needs. Check the method name, the receiver, and that the signature (parameters and return types) match precisely.
Pro Tips
- 💡 Prefer field names in struct literals — User{Name: "Al"} survives reordering and adding fields; positional literals break.
- 💡 Be consistent with receivers: if one method needs a pointer receiver, give them all pointer receivers so callers don't have to think about it.
- 💡 Keep interfaces small. "The bigger the interface, the weaker the abstraction." — Rob Pike. One or two methods is ideal.
- 💡 Print with %+v (fmt.Printf("%+v", x)) to see a struct's field names and values while debugging.
📋 Quick Reference
| Task | Go Syntax |
|---|---|
| Define a struct | |
| Instance by name | |
| Instance positional | |
| Value receiver (reads) | |
| Pointer receiver (writes) | |
| Address / dereference | |
| Embedding | |
| Interface |
Mini-Challenge: Rectangle Area
No blanks this time — just a brief and an outline. Define the struct and its method yourself, then run it and check your output against the expected line. This is exactly the shape of code real Go programs are built from.
package main
import "fmt"
func main() {
// 🎯 MINI-CHALLENGE: a Rectangle with a method
//
// 1. Define a Rectangle struct with Width and Height (both int).
// 2. Add a method Area() int that returns Width * Height.
// (A value receiver is fine — Area only reads the struct.)
// 3. In main, make a Rectangle{Width: 4, Height: 5}.
// 4. Print: "area is 20"
//
// ✅ Expected output:
// area is 20
// your code here
}🎉 Lesson Complete!
- ✅ A struct groups related fields; build instances by name or by position
- ✅ Omitted fields take their type's zero value (0, "", false)
- ✅ Value receivers read a copy; pointer receivers change the original
- ✅ & takes an address, * dereferences; Go has no pointer arithmetic
- ✅ Embedding composes types and promotes fields/methods — no inheritance
- ✅ A type satisfies an interface automatically just by having its methods
Practice quiz
What is a struct in Go?
- A function with state
- A list of methods only
- A named type that groups related fields together
- A pointer alias
Answer: A named type that groups related fields together. A struct is a type that groups related fields under one name.
For type Person struct { Name string; Age int }, what is the zero value of var empty Person?
- {Name: "" Age: 0}
- nil
- {Name: nil Age: nil}
- a compile error
Answer: {Name: "" Age: 0}. Each field takes its type's zero value: "" for string, 0 for int.
What is the safest way to build a struct instance?
- Positional: Person{"Al", 30}
- var p Person only
- new(Person)
- By field name: Person{Name: "Al", Age: 30}
Answer: By field name: Person{Name: "Al", Age: 30}. Field-name literals are order-independent and survive added/reordered fields.
A value receiver method gets what?
- The original struct
- A copy of the struct, so changes are discarded
- A pointer to the struct
- Nothing
Answer: A copy of the struct, so changes are discarded. A value receiver operates on a copy; mutations don't affect the original.
Why might a method not change the struct it was called on?
- It used a value receiver instead of a pointer receiver
- The struct was nil
- Go forbids mutation
- The field was const
Answer: It used a value receiver instead of a pointer receiver. Value receivers work on a copy; use a pointer receiver (e.g. *Wallet) to make changes stick.
What does & do?
- Dereferences a pointer
- Allocates a slice
- Takes the address of a value, producing a pointer
- Compares two values
Answer: Takes the address of a value, producing a pointer. &x takes the address of x, producing a pointer.
What does * do to a pointer p?
- Takes its address
- Dereferences it to read or write the pointed-to value
- Doubles it
- Frees it
Answer: Dereferences it to read or write the pointed-to value. *p dereferences the pointer to reach the value it points at.
Does Go support pointer arithmetic like p++ or p + 4?
- Yes, freely
- Only for arrays
- Only with unsafe always required for structs
- No — Go forbids pointer arithmetic to keep memory safe
Answer: No — Go forbids pointer arithmetic to keep memory safe. Go has no pointer arithmetic, which keeps memory access safe.
How does a type satisfy an interface in Go?
- With an implements keyword
- Automatically, just by having the required methods (structural typing)
- By embedding the interface
- By registering it at runtime
Answer: Automatically, just by having the required methods (structural typing). Satisfaction is automatic: any type with the right methods satisfies the interface.
How does Go reuse code across struct types?
- Class inheritance
- Global mixins
- Embedding (composition over inheritance)
- Macros
Answer: Embedding (composition over inheritance). Go composes types via embedding rather than inheriting from base classes.
Continue this course
- Previous: Pointers
- Next: Checkpoint: Go Basics — Combine slices, maps and loops in a build challenge — then a quiz
Frequently asked questions
When should I use a pointer receiver instead of a value receiver?
Use a pointer receiver (e.g. func (c *Counter)) when the method needs to change the struct, or when the struct is large and you want to avoid copying it. Use a value receiver when the method only reads the data. A common convention is to be consistent: if any method needs a pointer receiver, give them all pointer receivers.
Why didn't my method change the struct?
You almost certainly used a value receiver. A value receiver gets a COPY of the struct, so any changes are thrown away when the method returns. Switch to a pointer receiver — func (w *Wallet) Deposit(...) — and the change will stick.
What is the difference between & and *?
& takes the address of a value, producing a pointer (p := &x makes p a *int). * does the opposite: it dereferences a pointer to reach the value it points at (*p reads or writes x). For struct fields Go auto-dereferences, so you can write u.Name even when u is a *User.
Does Go have classes or inheritance?
No. Go has structs and interfaces but no classes and no inheritance. Instead you compose behaviour by embedding one struct inside another, which promotes the inner type's fields and methods. The Go motto is 'composition over inheritance'.
How does a type satisfy an interface in Go?
Automatically — there is no 'implements' keyword. If a type has every method listed in the interface (matching name, parameters, and return types), it satisfies that interface. This is called structural typing, and it lets unrelated types share an interface without any explicit declaration.