Structs & Methods

Go · Lesson 18 of 32 6 min read Updated

A struct groups related values under one name: a student has a name, a city and marks; an order has an ID, items and a total. Go has no classes. Instead, you define a struct for the data and attach methods (functions that belong to a type) to it. Together they do the job that classes do in Java or Python, with less ceremony.

In this lesson you will define structs, create and compare them, add methods with value and pointer receivers, write constructor functions, give types a readable String form, and reuse code by embedding one struct in another.

Defining and creating structs

A struct type lists its fields, each with a name and a type. Field names that start with a capital letter are exported, like any other Go name.

Go
package main

import "fmt"

type Student struct {
	Name  string
	City  string
	Marks int
}

func main() {
	a := Student{Name: "Aarav", City: "Rohtak", Marks: 88}
	b := Student{Name: "Isha"} // missing fields get zero values
	var empty Student

	a.Marks += 2
	fmt.Println(a.Name, a.Marks)
	fmt.Printf("%+v\n", b)
	fmt.Printf("%+v\n", empty)
	fmt.Println(a == Student{Name: "Aarav", City: "Rohtak", Marks: 90})
}
Output
Aarav 90
{Name:Isha City: Marks:0}
{Name: City: Marks:0}
true

Always write field names in literals (Name: "Aarav"). The short form Student{"Aarav", "Rohtak", 88} also works, but it breaks as soon as someone adds or reorders a field. Two structs are equal with == when all their fields are equal, as long as every field type is comparable (slices and maps are not).

Assigning a struct copies all of its fields, just like an array. To share one struct between parts of a program, use a pointer, as you saw in Pointers.

Methods and receivers

A method is a function with a receiver: a parameter written before the method name. There are two kinds:

  • A value receiver (w Wallet) gets a copy. Use it for methods that only read.
  • A pointer receiver (w *Wallet) gets the address. Use it for methods that change the struct, or when the struct is large.
Go
package main

import "fmt"

type Wallet struct {
	Owner   string
	Balance int
}

func (w Wallet) CanPay(amount int) bool {
	return w.Balance >= amount
}

func (w *Wallet) AddMoney(amount int) {
	w.Balance += amount
}

func (w *Wallet) Pay(amount int) error {
	if !w.CanPay(amount) {
		return fmt.Errorf("%s: balance %d is less than %d", w.Owner, w.Balance, amount)
	}
	w.Balance -= amount
	return nil
}

func main() {
	w := Wallet{Owner: "Priya", Balance: 500}
	w.AddMoney(250)

	if err := w.Pay(600); err != nil {
		fmt.Println(err)
	}
	if err := w.Pay(300); err != nil {
		fmt.Println(err)
	}
	fmt.Println("Left:", w.Balance)
}
Output
Priya: balance 150 is less than 300
Left: 150

You call w.AddMoney(250) on a plain variable, and Go automatically passes &w because the method needs a pointer. If AddMoney had a value receiver, it would add to a copy and the wallet would never change, a very common beginner bug. Many teams follow a simple rule: if any method of a type needs a pointer receiver, give all its methods pointer receivers, for consistency. The example mixes both kinds only to show the difference.

Constructors and the String method

Go has no constructors built into the language. By convention you write a function named NewTypeName that returns a ready-to-use value, usually a pointer. A type can also choose how it prints by having a method String() string; fmt uses it automatically for %v and Println.

Go
package main

import "fmt"

type Order struct {
	ID    int
	Items []string
	Total int
}

func NewOrder(id int) *Order {
	return &Order{ID: id}
}

func (o *Order) Add(item string, price int) {
	o.Items = append(o.Items, item)
	o.Total += price
}

func (o *Order) String() string {
	return fmt.Sprintf("Order #%d: %d items, ₹%d", o.ID, len(o.Items), o.Total)
}

func main() {
	o := NewOrder(1042)
	o.Add("Paneer tikka", 280)
	o.Add("Butter naan", 60)
	fmt.Println(o)
}
Output
Order #1042: 2 items, ₹340

Because String has a pointer receiver here, fmt uses it when you print a *Order (which o is). Printing a plain Order value would show the default {1042 [Paneer tikka Butter naan] 340} instead.

Methods on your own named types

Methods are not limited to structs. Any named type you define can have them. This is how you finish the iota enumerations from Variables & Constants: give the constants their own type and a String method.

Go
package main

import "fmt"

type Status int

const (
	Pending Status = iota
	Shipped
	Delivered
)

func (s Status) String() string {
	switch s {
	case Pending:
		return "Pending"
	case Shipped:
		return "Shipped"
	case Delivered:
		return "Delivered"
	}
	return fmt.Sprintf("Status(%d)", int(s))
}

func main() {
	s := Shipped
	fmt.Println(s, Delivered, Status(7))
	fmt.Printf("%v %d\n", s, s)
}
Output
Shipped Delivered Status(7)
Shipped 1

The separate type Status stops you from passing a random int where a status is expected by accident, and the String method makes logs readable. %d still prints the number. Inside String, converting with int(s) avoids calling String again by mistake.

Embedding: reuse without inheritance

Go has no inheritance. Instead, a struct can embed another type by listing it without a field name. The inner type's fields and methods are promoted: you can use them as if they belonged to the outer struct.

Go
package main

import "fmt"

type Person struct {
	Name string
	City string
}

func (p Person) Greeting() string {
	return "Namaste, I am " + p.Name + " from " + p.City
}

type Employee struct {
	Person
	EmpID  string
	Salary int
}

func main() {
	e := Employee{
		Person: Person{Name: "Kabir", City: "Pune"},
		EmpID:  "EMP-07",
	}
	fmt.Println(e.Name, e.EmpID)
	fmt.Println(e.Greeting())
	fmt.Printf("%+v\n", e)
}
Output
Kabir EMP-07
Namaste, I am Kabir from Pune
{Person:{Name:Kabir City:Pune} EmpID:EMP-07 Salary:0}

e.Name is short for e.Person.Name. An Employee has a Person inside it; it is not a kind of Person, so you cannot pass an Employee where a Person is required. This "composition over inheritance" keeps Go types simple.

Common mistakes

  • Value receiver on a method that changes the struct. The change is lost; use a pointer receiver.
  • Positional literals. Student{"Aarav", "Rohtak", 88} breaks when fields change; use field names.
  • Lower-case field names in data you share. Unexported fields are invisible to other packages, including encoding/json.
  • Expecting embedding to work like inheritance. An outer type does not become the inner type.

Key points

  • type Name struct { ... } groups fields; literals with field names set them, the rest get zero values.
  • Methods have receivers: value receivers read a copy, pointer receivers can change the original.
  • Write NewX functions as constructors; a String() string method controls how fmt prints a type.
  • Any named type, such as type Status int, can have methods.
  • Embedding promotes fields and methods for reuse without inheritance.

Exercise

Create a Book struct for a Jaipur library with Title, Author and Copies fields. Add a pointer-receiver method Borrow() error that reduces Copies by one, or returns an error when no copies are left, and a String() method that prints like Godan by Premchand (1 left). Borrow the book three times when it starts with 2 copies, printing the book or the error after each try.

Show solution
Go
package main

import "fmt"

type Book struct {
	Title  string
	Author string
	Copies int
}

func (b *Book) Borrow() error {
	if b.Copies == 0 {
		return fmt.Errorf("%q: no copies left", b.Title)
	}
	b.Copies--
	return nil
}

func (b *Book) String() string {
	return fmt.Sprintf("%s by %s (%d left)", b.Title, b.Author, b.Copies)
}

func main() {
	book := &Book{Title: "Godan", Author: "Premchand", Copies: 2}
	for range 3 {
		if err := book.Borrow(); err != nil {
			fmt.Println("Error:", err)
			continue
		}
		fmt.Println(book)
	}
}
Output
Godan by Premchand (1 left)
Godan by Premchand (0 left)
Error: "Godan": no copies left

Borrow needs a pointer receiver because it changes Copies. for range 3 repeats three times without a loop variable.

Quick Quiz

1. Which receiver should a method that changes a struct's field use?

2. What does embedding Person inside Employee give you?

3. What happens to fields you leave out of a struct literal like Student{Name: "Isha"}?

4. Which method makes fmt.Println print a type in your own format?

Learn Go with a mentor

Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call