Collections of Structs

By Syncfusion®, Inc. · Last updated

Work with slices and maps of structs: add, update, delete and search records, sort by fields with sort.Slice, filter with custom conditions, and build a CRUD program.

What Are Collections of Structs?

Collection of structs = a slice or a map whose elements are structs.

  • One struct = one record, like one student.
  • A slice of structs = a list of records, like a whole class.
  • A map of structs = records you find by a key, like by roll number.

Real programs work like this: a list of orders, a table of books, a class of students.

Simple lesson: a collection of structs is a table of records in your program.

A Real-World Example

Think of the class register.

  • Each row is one student: roll number, name, marks.
  • The teacher adds a row when a new student joins.
  • She updates marks after each test.
  • She removes a row when a student leaves.
  • She sorts by marks to find the top three.
  • She filters to see only the students who need extra classes.

Now the mapping:

  • One row = one struct.
  • The register = a slice (or map) of structs.
  • Add, update, remove, find = CRUD operations.

This lesson uses ideas from earlier lessons:

  • structs from Structs and Methods
  • slices from Slices and maps from Maps
  • pointers from Pointers

What You Will Learn

In this lesson, you will learn:

  1. How to make slices of structs and maps of structs.
  2. How to add, update, delete and search records.
  3. How to sort records by a field value.
  4. How to filter records with custom conditions.
  5. How to put it together in a small CRUD program.

Collections of Structs

Slices of Structs

A class as a slice of structs

package main

import "fmt"

type Student struct {
	Roll  int
	Name  string
	Marks int
}

func main() {
	class := []Student{
		{1, "Asha", 91},
		{2, "Ravi", 78},
		{3, "Meena", 85},
	}
	for _, s := range class {
		fmt.Printf("%d %-6s %d\n", s.Roll, s.Name, s.Marks)
	}
	fmt.Println("Students:", len(class))
}
1 Asha   91
2 Ravi   78
3 Meena  85
Students: 3
  • Inside []Student{ ... }, each element can drop the word Student.
  • %-6s prints text in a 6-character column, lined up on the left.

Maps of Structs

Students by roll number

package main

import "fmt"

type Student struct {
	Name  string
	Marks int
}

func main() {
	byRoll := map[int]Student{
		1: {"Asha", 91},
		2: {"Ravi", 78},
	}
	s, ok := byRoll[2]
	fmt.Println(s.Name, s.Marks, ok)
	_, ok = byRoll[9]
	fmt.Println(ok)
}
Ravi 78 true
false
  • Use a map when you look records up by a key.
  • Use the comma-ok idiom for missing keys. See Maps.

Slice of structs vs map of structs

Simple lesson: use a slice of structs for an ordered list, and a map of structs to look records up by key.

Working with Struct Collections

Adding Records

Add new students

package main

import "fmt"

type Student struct {
	Name  string
	Marks int
}

func main() {
	var class []Student
	class = append(class, Student{"Asha", 91})
	class = append(class, Student{Name: "Kabir", Marks: 88})
	fmt.Println(class, len(class))
}
[{Asha 91} {Kabir 88}] 2

Updating Records

The range value is a copy. Changing it does not change the slice.

Press Play to see the trap, and the fix.

Updating inside a range loop

  • Use for i := range list and list[i].Field = ... to change elements.

For a map of structs, you cannot change a field directly:

Update a struct inside a map

package main

import "fmt"

type Student struct {
	Name  string
	Marks int
}

func main() {
	byRoll := map[int]Student{1: {"Asha", 91}}
	s := byRoll[1]
	s.Marks = 95
	byRoll[1] = s
	fmt.Println(byRoll[1])
}
{Asha 95}
  • byRoll[1].Marks = 95 does not build: cannot assign to struct field byRoll[1].Marks in map.
  • Take a copy, change it, and put it back.
  • Or store pointers: map[int]*Student. Then byRoll[1].Marks = 95 works.

Deleting Records

Remove a student by name

package main

import "fmt"

type Student struct {
	Name  string
	Marks int
}

func removeByName(list []Student, name string) []Student {
	for i, s := range list {
		if s.Name == name {
			return append(list[:i], list[i+1:]...)
		}
	}
	return list
}

func main() {
	class := []Student{{"Asha", 91}, {"Ravi", 78}, {"Meena", 85}}
	class = removeByName(class, "Ravi")
	fmt.Println(class)
}
[{Asha 91} {Meena 85}]
  • This is the remove pattern from Slices.
  • For a map, it is simply delete(byRoll, 2).

Searching Records

Find a student

package main

import "fmt"

type Student struct {
	Name  string
	Marks int
}

func findByName(list []Student, name string) (Student, bool) {
	for _, s := range list {
		if s.Name == name {
			return s, true
		}
	}
	return Student{}, false
}

func main() {
	class := []Student{{"Asha", 91}, {"Ravi", 78}}
	if s, ok := findByName(class, "Ravi"); ok {
		fmt.Println("Found:", s.Name, s.Marks)
	}
	_, ok := findByName(class, "Zoya")
	fmt.Println("Zoya found?", ok)
}
Found: Ravi 78
Zoya found? false
  • Return the record and a bool, like the comma-ok idiom.
  • Student{} is an empty record for "not found".

Simple lesson: add with append, update with list[i], delete by slicing, and search with a loop that returns (record, found).

Sorting Struct Collections

Sorting by Field Values

sort.Slice = sort a slice using your own "comes before" rule.

Pick a field and a direction, and filter by class.

Look at the code under the table:

  • The rule list[i].Marks > list[j].Marks puts higher marks first.
  • Change > to < for lowest first.

Sort by marks, highest first

package main

import (
	"fmt"
	"sort"
)

type Student struct {
	Name  string
	Marks int
}

func main() {
	class := []Student{{"Asha", 91}, {"Ravi", 78}, {"Kabir", 94}, {"Meena", 85}}
	sort.Slice(class, func(i, j int) bool {
		return class[i].Marks > class[j].Marks
	})
	for rank, s := range class[:3] {
		fmt.Println(rank+1, s.Name, s.Marks)
	}
}
1 Kabir 94
2 Asha 91
3 Meena 85
  • The function answers: "should element i come before element j?"
  • class[:3] takes the top three after sorting.

sort.Slice sorts the slice in place: it changes the order of the original slice.

If you still need the old order, copy the slice first. See Slices.

Sorting by Two Fields

Sort by class, then by name

package main

import (
	"fmt"
	"sort"
)

type Student struct {
	Name  string
	Class string
}

func main() {
	list := []Student{{"Ravi", "8B"}, {"Asha", "8A"}, {"Kabir", "8B"}, {"Meena", "8A"}}
	sort.Slice(list, func(i, j int) bool {
		if list[i].Class != list[j].Class {
			return list[i].Class < list[j].Class
		}
		return list[i].Name < list[j].Name
	})
	fmt.Println(list)
}
[{Asha 8A} {Meena 8A} {Kabir 8B} {Ravi 8B}]
  • First compare classes. Only when they are equal, compare names.
  • sort.SliceStable keeps equal elements in their old order, if that matters.

Simple lesson: sort.Slice sorts records by any field, with a small "comes before" function.

Filtering Struct Collections

Filtering Data

Filter = make a new list with only the records that pass a test.

Students who need extra classes

package main

import "fmt"

type Student struct {
	Name  string
	Marks int
}

func main() {
	class := []Student{{"Asha", 91}, {"Ravi", 42}, {"Meena", 85}, {"Arjun", 38}}
	var needHelp []Student
	for _, s := range class {
		if s.Marks < 50 {
			needHelp = append(needHelp, s)
		}
	}
	fmt.Println(needHelp)
}
[{Ravi 42} {Arjun 38}]

Custom Conditions

Pass the test in as a function, and one filter works for any rule. See Functions and Generics.

One filter, many rules

package main

import "fmt"

type Student struct {
	Name   string
	Class  string
	Marks  int
	Hostel bool
}

func filter(list []Student, keep func(Student) bool) []Student {
	var out []Student
	for _, s := range list {
		if keep(s) {
			out = append(out, s)
		}
	}
	return out
}

func names(list []Student) []string {
	var out []string
	for _, s := range list {
		out = append(out, s.Name)
	}
	return out
}

func main() {
	all := []Student{
		{"Asha", "8A", 91, true},
		{"Ravi", "8B", 78, false},
		{"Meena", "8A", 85, false},
		{"Kabir", "8B", 94, true},
	}
	toppers := filter(all, func(s Student) bool { return s.Marks >= 90 })
	hostel8B := filter(all, func(s Student) bool { return s.Class == "8B" && s.Hostel })
	fmt.Println(names(toppers))
	fmt.Println(names(hostel8B))
}
[Asha Kabir]
[Kabir]

Easy way to remember

  • Change elements → loop with the index: list[i].X = ...
  • Map of structs → copy, change, put back (or store pointers)
  • Sort → sort.Slice(list, func(i, j int) bool { ... })
  • Filter → a new slice with append

Simple lesson: filter by looping and appending the records that pass your test.

Practical Examples

CRUD Operations

CRUD = Create, Read, Update, Delete: the four basic jobs on any set of records.

A small library manager

package main

import (
	"fmt"
	"sort"
)

type Book struct {
	ID       int
	Title    string
	Borrowed bool
}

type Library struct {
	books  map[int]*Book
	nextID int
}

func NewLibrary() *Library {
	return &Library{books: make(map[int]*Book), nextID: 1}
}

func (l *Library) Add(title string) int {
	id := l.nextID
	l.books[id] = &Book{ID: id, Title: title}
	l.nextID++
	return id
}

func (l *Library) Borrow(id int) bool {
	b, ok := l.books[id]
	if !ok || b.Borrowed {
		return false
	}
	b.Borrowed = true
	return true
}

func (l *Library) Remove(id int) {
	delete(l.books, id)
}

func (l *Library) List() []Book {
	var out []Book
	for _, b := range l.books {
		out = append(out, *b)
	}
	sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID })
	return out
}

func main() {
	lib := NewLibrary()
	lib.Add("Panchatantra")
	id := lib.Add("Malgudi Days")
	lib.Add("Wings of Fire")

	fmt.Println(lib.Borrow(id))
	fmt.Println(lib.Borrow(id))
	lib.Remove(1)
	for _, b := range lib.List() {
		fmt.Println(b.ID, b.Title, b.Borrowed)
	}
}
true
false
2 Malgudi Days true
3 Wings of Fire false
  • Create: Add makes a book with the next ID.
  • Read: List copies the books into a slice and sorts them by ID, because map order is random.
  • Update: Borrow changes a field through the pointer stored in the map.
  • Delete: Remove uses delete.
  • The second Borrow fails, because the book is already out.

Simple lesson: wrap a collection of structs in a type with methods for create, read, update and delete.

TL;DR

  1. Slice of structs = []Student{{...}, {...}}. Ordered list of records.
  2. Map of structs = map[int]Student. Look up records by key.
  3. Add = list = append(list, Student{...}).
  4. Update = list[i].Marks = 90 (not the range copy).

    • Map: copy, change, put back, or use map[int]*Student.
  5. Delete = append(list[:i], list[i+1:]...) or delete(m, key).
  6. Search = loop, return (record, found).
  7. Sort = sort.Slice(list, func(i, j int) bool { return list[i].X < list[j].X }).
  8. Filter = new slice, append the records that pass.
  9. CRUD = create, read, update, delete.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
for _, s := range list { s.Marks++ }s is the real elements is a copyNothing changes.
m[1].Marks = 90 on map[int]StudentMap elements can be changed in placeCopy, change, put backcannot assign to struct field m[1].Marks in map
Printing a map of records in a loopIt has an orderMap order is randomOutput order changes each run.
Wrong sort.Slice rule: <=<= and < are the sameUse < (strict)Unstable or odd ordering.
Forgetting list = after appendappend changes in placeIt returns the new sliceNew records are lost.
Returning nil struct for "not found"Structs can be nilReturn Student{}, falseA build error.

Check Yourself

Exercises

What to Remember

  • A slice of structs is an ordered list of records. A map of structs finds records by key.
  • Change elements with list[i].Field, never through the range copy.
  • For a map of structs, copy, change and put back, or store pointers.
  • Sort with sort.Slice and a "comes before" rule.
  • Filter by appending the records that pass a test.
  • CRUD = create, read, update, delete.

Next: Error Handling. You will learn how Go reports problems with error values, and how to handle them well.