Structs

Course 2 · Ch 1
Structs in Full, Methods, and Pointers
Go's real answer to "object" — a fixed-shape type, functions attached to it, and the one new concept that makes both work together

Fundamentals Chapter 7 previewed struct in passing. This chapter covers it properly: defining one, attaching behaviour to it with methods, and the pointer concept that decides whether a method can actually change the struct it's called on. Go has no class keyword and no this — methods and pointers together are how Go achieves what JavaScript's object methods did with this (Fundamentals Chapter 7).

Defining and Creating a Struct

type Person struct { Name string Age int } p := Person{Name: "Philip", Age: 35} fmt.Println(p.Name) // "Philip" p.Age = 36 // fields can be reassigned after creation

type Person struct { ... } defines a new named type with a fixed set of fields. Field access uses the same dot notation as JavaScript object properties — but unlike a map (Chapter 7), a struct's fields are fixed at compile time: there's no way to add an unplanned field later, and each field can have its own distinct type.

Capitalised fields are intentional
Name and Age start with capital letters deliberately — in Go, capitalisation controls visibility outside the current package (covered fully later). Lowercase struct fields still work everywhere in this course's single-file examples, but capitalised fields are the convention for anything meant to be used elsewhere.

Methods — Functions Attached to a Type

func (p Person) Greet() string { return "Hi, I'm " + p.Name } p := Person{Name: "Philip", Age: 35} fmt.Println(p.Greet()) // "Hi, I'm Philip"

(p Person) between func and the method name is the receiver — it's what makes this an ordinary function into a method that can be called as p.Greet(). p inside the method body plays the same role JavaScript's this played inside an object method (Fundamentals Chapter 7) — but it's an explicit, named parameter here, not an implicit keyword.

Pointers — Why Greet() Can't Change p, But Something Else Can

age := 35 agePointer := &age // & gets the memory address of age fmt.Println(agePointer) // 0xc0000140a0 — an address, not a value fmt.Println(*agePointer) // 35 — * "dereferences": follows the pointer to the actual value *agePointer = 36 fmt.Println(age) // 36 — changing through the pointer changed the original

&age ("address of") produces a pointer — a value that points to where age actually lives in memory, rather than a copy of 35 itself. *agePointer ("dereference") goes the other way: follow the pointer back to the real value. This is a genuinely new concept with no real JavaScript equivalent — every JavaScript variable is already accessed "directly," with no separate address-vs-value distinction to think about.

Why This Matters for Methods

func (p Person) HaveBirthday() { p.Age++ // only changes the COPY inside this method } func (p *Person) HaveBirthdayPointer() { p.Age++ // changes the REAL struct, through the pointer } person := Person{Name: "Philip", Age: 35} person.HaveBirthday() fmt.Println(person.Age) // 35 — unchanged! person.HaveBirthdayPointer() fmt.Println(person.Age) // 36 — actually changed

A receiver of type Person (no *) receives a brand-new copy of the struct — any changes inside the method vanish once it returns, the value-passing behaviour every Go function has by default. A receiver of type *Person (a pointer receiver) receives a pointer to the original struct instead, so changes made through it persist. Go automatically handles the & when calling person.HaveBirthdayPointer() — no manual &person.HaveBirthdayPointer() needed.

If a method needs to change the struct, it needs a pointer receiver
This is the single most common beginner mistake with Go structs: writing a method that's supposed to update a field, using a plain (non-pointer) receiver, and then being confused why the change doesn't stick outside the method. If a method modifies p.Field and that change should be visible afterward, the receiver must be *Person, not Person.
JavaScriptGo
{ name: "Philip", age: 35 }type Person struct { Name string; Age int }
greet: function() { ...this... }func (p Person) Greet() string { ...p... }
this (implicit)Receiver variable (explicit, named by you)
Objects are always reference-sharedPlain receiver = copy; pointer receiver (*Type) = shared
No address/value distinction&value (address), *pointer (dereference)

Coding Challenges

Challenge 1

Define a struct Book with fields Title (string), Pages (int). Create one, then write a method (b Book) Describe() string that returns a sentence combining both fields. Print the result of calling Describe().

📄 View solution
Challenge 2

Define a struct Account with field Balance (float64). Write a method (a *Account) Deposit(amount float64) using a pointer receiver that adds amount to Balance. Create an account, call Deposit twice with different amounts, and print Balance after each call to confirm it actually changes.

📄 View solution
Challenge 3

Write a function double(n *int) that takes a pointer to an int and doubles the value it points to (using dereferencing, not a return value). Declare a variable, pass its address to double, and print the variable afterward to confirm it changed.

📄 View solution

Chapter 1 Quick Reference (Intermediate)

  • type Name struct { Field type } — defines a fixed-shape record type
  • func (receiver Type) MethodName() { } — attaches a method to a type
  • &value — gets a pointer (the address) to a value
  • *pointer — dereferences a pointer, accessing/changing the real value
  • Plain receiver (p Type) — method gets a copy; changes don't persist
  • Pointer receiver (p *Type) — method gets the original; changes DO persist
  • Go calls automatically handle & when invoking a pointer-receiver method on a plain variable
  • Next chapter: interfaces — how Go achieves polymorphism without classes or inheritance