Skip to content
Educora
Intermediate16 min6 / 10

Structs, methods and pointers

Build your own types with structs, understand why pointers are needed, write methods with value and pointer receivers, and use composition.

Check yourself
In this lesson you will learn
  • Declare a struct, create it with a literal and print it with %+v
  • Use pointers with & and *
  • Choose correctly between value and pointer receivers
  • Work with NewX constructor functions and struct embedding

Information about a student — name, age, average score — belongs together. Keeping it in three separate variables is awkward. Go has no classes; instead it has structs, which group related fields into one type, and methods, which add behaviour to that type. In this lesson you will also learn pointers — without them a function cannot change the original data.

Structs

A struct is declared as type Name struct { ... }, listing its fields and their types. To create a value you write a struct literal: Student{Name: "Aysel", Age: 15}. Fields are accessed with a dot: s.Name. Any field you don't set gets its zero value, so even an empty struct is a fully usable value.

Go
package main

import "fmt"

type Student struct {
	Name  string
	Age   int
	Score float64
}

func main() {
	s := Student{Name: "Aysel", Age: 15, Score: 92.5}
	var empty Student
	s.Age++
	fmt.Println(s)
	fmt.Printf("%+v\n", s)
	fmt.Printf("%+v\n", empty)
	fmt.Println(s.Name, s.Score)
}
Expected output
{Aysel 16 92.5}
{Name:Aysel Age:16 Score:92.5}
{Name: Age:0 Score:0}
Aysel 92.5

%v (and Println) show only the values, while %+v adds the field names — very handy when debugging. Structs whose fields are all comparable can be compared with ==: two Student values are equal when all their fields are equal.

Pointers

In Go everything passed to a function is copied. If the function changes its parameter, the caller's variable stays untouched. To change the original you need a pointer — the variable's address in memory. &x takes the address, and *p reads or changes the value at that address. The type *int means “pointer to an int”. Go has no pointer arithmetic (no p++ as in C), so pointers are safe.

Go
package main

import "fmt"

func doubleValue(n int) {
	n *= 2
}

func doublePointer(n *int) {
	*n *= 2
}

func main() {
	x := 5
	p := &x
	*p = 7
	fmt.Println(x, *p, p == &x)
	doubleValue(x)
	fmt.Println(x)
	doublePointer(&x)
	fmt.Println(x)
}
Expected output
7 7 true
7
14
Definition
Pointer

A value that holds the memory address of another variable. Its zero value is nil. & takes an address, and * accesses the value stored at it.

Methods: value and pointer receivers

A method is a function with a special parameter called the receiver. The receiver is written between the word func and the method name: func (a Account) Summary() string. If the receiver is a value (a Account), the method works on a copy of the object. If it is a pointer (a *Account), the method can change the original object.

Go
package main

import "fmt"

type Account struct {
	Owner   string
	Balance float64
}

func (a Account) Summary() string {
	return fmt.Sprintf("%s: %.2f AZN", a.Owner, a.Balance)
}

func (a *Account) Deposit(amount float64) {
	a.Balance += amount
}

func (a Account) BrokenDeposit(amount float64) {
	a.Balance += amount
}

func main() {
	acc := Account{Owner: "Murad", Balance: 100}
	acc.Deposit(50)
	acc.BrokenDeposit(1000)
	fmt.Println(acc.Summary())
	p := &acc
	p.Deposit(25.5)
	fmt.Println(p.Summary())
}
Expected output
Murad: 150.00 AZN
Murad: 175.50 AZN

BrokenDeposit tried to add 1000 manat, but the balance did not change, because the method worked on a copy. Notice that acc is not a pointer, yet acc.Deposit(50) works — Go automatically calls (&acc).Deposit(50). In the same way you can call the value-receiver method Summary through the pointer p.

ReceiverWhen to choose it
func (a Account)The method does not change the object and the type is small (a few fields, like time.Time).
func (a *Account)The method changes fields, the type is large, or it contains a field that must not be copied, such as sync.Mutex.
Bug: the change is lost
type Counter struct {
	value int
}

func (c Counter) Increment() {
	c.value++ // changes a copy
}
Fixed: pointer receiver
type Counter struct {
	value int
}

func (c *Counter) Increment() {
	c.value++ // changes the original
}

Constructors and embedding

Go has no special constructors. Instead you write an ordinary function whose name starts with New: NewTeacher(...) creates an object and returns a pointer to it. There is no inheritance either — Go prefers composition. You can embed one struct in another as a field without a name: the embedded struct's fields and methods then become directly available on the outer struct.

Go
package main

import "fmt"

type Person struct {
	Name string
}

func (p Person) Greet() string {
	return "Hi, I am " + p.Name
}

type Teacher struct {
	Person
	Subject string
}

func NewTeacher(name, subject string) *Teacher {
	return &Teacher{Person: Person{Name: name}, Subject: subject}
}

func main() {
	t := NewTeacher("Leyla", "Math")
	fmt.Println(t.Greet())
	fmt.Println(t.Name, "teaches", t.Subject)
	fmt.Printf("%+v\n", *t)
}
Expected output
Hi, I am Leyla
Leyla teaches Math
{Person:{Name:Leyla} Subject:Math}

Key points

  • A struct groups related fields into one type; %+v prints it with field names.
  • Go passes everything by value; to change the original you need a pointer (&x, *p).
  • A value receiver works on a copy, a pointer receiver on the original; Go adds the & for you.
  • Accessing a field through a nil pointer causes a panic.
  • Go uses NewX functions instead of constructors, and struct embedding (composition) instead of inheritance.

Check yourself

10 questions. Every correct answer earns XP.

1 / 10
What does fmt.Printf("%+v", Point{X: 1, Y: 2}) print?