# Collections of Structs

Course: [Go](https://www.devtutorials.ai/learn/go) · Section: Types and OOP
Source: https://www.devtutorials.ai/learn/go/types-and-oop/collections-of-structs

> 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

```go runnable title="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))
}
```

```text
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

```go runnable title="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)
}
```

```text
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**.

**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

```go runnable title="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))
}
```

```text
[{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.

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

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

```go runnable title="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])
}
```

```text
{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

```go runnable title="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)
}
```

```text
[{Asha 91} {Meena 85}]
```

- This is the remove pattern from **Slices**.
- For a map, it is simply `delete(byRoll, 2)`.

### Searching Records

```go runnable title="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)
}
```

```text
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.

```go runnable title="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)
	}
}
```

```text
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

```go runnable title="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)
}
```

```text
[{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.

```go runnable title="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)
}
```

```text
[{Ravi 42} {Arjun 38}]
```

### Custom Conditions

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

```go runnable title="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))
}
```

```text
[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** = **C**reate, **R**ead, **U**pdate, **D**elete: the four basic jobs on any set of records.

```go runnable title="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)
	}
}
```

```text
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

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| `for _, s := range list { s.Marks++ }` | `s` is the real element | `s` is a copy | Nothing changes. |
| `m[1].Marks = 90` on `map[int]Student` | Map elements can be changed in place | Copy, change, put back | `cannot assign to struct field m[1].Marks in map` |
| Printing a map of records in a loop | It has an order | Map order is random | Output order changes each run. |
| Wrong `sort.Slice` rule: `<=` | `<=` and `<` are the same | Use `<` (strict) | Unstable or odd ordering. |
| Forgetting `list =` after `append` | `append` changes in place | It returns the new slice | New records are lost. |
| Returning `nil` struct for "not found" | Structs can be nil | Return `Student{}, false` | A 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.
