Code of the Day
BeginnerCore syntax

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.

GoBeginner12 min read
Recommended first
By the end of this lesson you will be able to:
  • 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)   // 10

Struct 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 modified

Methods — attaching behaviour to types

A method is a function with a 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())  // 16

Value receivers vs pointer receivers

The receiver in (r Rectangle) is a r is a copy of the struct. The method can read fields but not modify the original.

A (r *Rectangle) receives a 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 modified

When 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 method

This 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. 1.
    You want to write a method that doubles a Rectangle's Width field. Which receiver should you use?
  2. 2.
    When you assign a struct value to a new variable in Go, both variables point to the same underlying data.
  3. 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 — returns math.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.

Finished reading? Mark it complete to track your progress.

On this page