Structs and methods
Define custom types with struct, access fields, and attach behaviour with value and pointer receivers — Go's approach to object-oriented design.
- Define a struct type with named fields
- Create struct values using literals and field names
- Access and modify struct fields
- Attach a method to a type using a value receiver
- Explain the difference between value receivers and pointer receivers
Go has no classes. There is no class keyword, no inheritance, no constructor syntax. What Go has instead is structs — composite types that group related data — and methods — functions attached to a type. This is simpler than a class hierarchy, and it's enough to model most real-world problems clearly.
This design choice reflects Go's philosophy: orthogonal features that compose cleanly rather than a single large abstraction. When you understand structs and methods, you also have the foundation for interfaces, which are next in the intermediate track.
Defining a struct
type Point struct {
X float64
Y float64
}type introduces a named type. struct { ... } defines its fields. Field names are capitalised here because capitalised identifiers in Go are exported (visible outside the package). Lowercase fields are unexported. Capitalisation is Go's visibility mechanism — there are no public/private keywords.
You can create a Point value several ways:
// Named field literals (recommended — order-independent)
p1 := Point{X: 3.0, Y: 4.0}
// Zero value — all fields start at their zero values
var p2 Point // Point{X: 0, Y: 0}
// Positional literal — requires knowing the field order (fragile, avoid)
p3 := Point{1.0, 2.0}Prefer named field syntax. If the struct gains a new field later, positional literals break.
Field access
Use dot notation to read and write fields:
p := Point{X: 3.0, Y: 4.0}
fmt.Println(p.X) // 3
p.Y = 10.0
fmt.Println(p.Y) // 10Struct values are copied by default. Assigning a struct to a new variable copies all the fields:
a := Point{X: 1, Y: 2}
b := a // b is an independent copy
b.X = 99
fmt.Println(a.X) // still 1 — a was not modifiedMethods — attaching behaviour to types
A method is a function with a receiver argument that ties it to a type:
type Rectangle struct {
Width float64
Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}The (r Rectangle) before the function name is the receiver. Call the method with dot notation:
rect := Rectangle{Width: 5, Height: 3}
fmt.Println(rect.Area()) // 15
fmt.Println(rect.Perimeter()) // 16Value receivers vs pointer receivers
The receiver in (r Rectangle) is a value receiver — r is a copy of the struct. The method can read fields but not modify the original.
A pointer receiver (r *Rectangle) receives a pointer to the struct. Changes to the fields persist:
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
rect := Rectangle{Width: 5, Height: 3}
rect.Scale(2)
fmt.Println(rect.Width) // 10 — original was modifiedWhen to use which receiver? Use a pointer receiver when the method needs to modify the struct, or when the struct is large enough that copying it every call would be wasteful. Use a value receiver for read-only methods on small structs. Conventionally, if any method on a type uses a pointer receiver, all methods on that type should use pointer receivers for consistency.
Go automatically handles taking the address and dereferencing when you call a method. You don't need to write (&rect).Scale(2) — rect.Scale(2) works whether rect is a value or a pointer.
Struct embedding (a glimpse ahead)
Go doesn't have inheritance, but it has embedding — you can include one struct inside another and its fields and methods are promoted to the outer type:
type Animal struct {
Name string
}
func (a Animal) Speak() string {
return a.Name + " makes a sound"
}
type Dog struct {
Animal // embedded — no field name
Breed string
}
d := Dog{Animal: Animal{Name: "Rex"}, Breed: "Labrador"}
fmt.Println(d.Name) // Rex — promoted from Animal
fmt.Println(d.Speak()) // Rex makes a sound — promoted methodThis isn't inheritance — Dog is not an Animal subtype. But it composes the behaviour cleanly. We'll return to this when we cover interfaces in the intermediate track.
No implicit this or self. In Go, the receiver name is explicit and up to you. By convention, use a short abbreviation of the type name (e.g., r for Rectangle, d for Dog). Do not use self or this — that's not idiomatic Go.
Check your understanding
Knowledge check
- 1.You want to write a method that doubles a Rectangle's Width field. Which receiver should you use?
- 2.When you assign a struct value to a new variable in Go, both variables point to the same underlying data.
- 3.In Go, how do you make a struct field visible (exported) outside its package?
Do it yourself
Define a Circle struct with a Radius float64 field. Add two methods:
Area() float64— returnsmath.Pi * r.Radius * r.Radius(import"math").Scale(factor float64)— multiplies the radius by factor (use a pointer receiver).
Call both from main, scale the circle, and print the new area.
go run main.go
go fmt ./...Where to go next
You've reached the end of the core syntax module. You can define types, attach behaviour, and model data with structs. The lab next brings everything together — variables, functions, control flow, collections, and structs — in a set of quiz sections and coding challenges you can try in your local editor.