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:
- How to make slices of structs and maps of structs.
- How to add, update, delete and search records.
- How to sort records by a field value.
- How to filter records with custom conditions.
- 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 wordStudent. %-6sprints 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 listandlist[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 = 95does 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. ThenbyRoll[1].Marks = 95works.
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].Marksputs 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
icome before elementj?" 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.SliceStablekeeps 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:
Addmakes a book with the next ID. - Read:
Listcopies the books into a slice and sorts them by ID, because map order is random. - Update:
Borrowchanges a field through the pointer stored in the map. - Delete:
Removeusesdelete. - The second
Borrowfails, 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
- Slice of structs =
[]Student{{...}, {...}}. Ordered list of records. - Map of structs =
map[int]Student. Look up records by key. - Add =
list = append(list, Student{...}). Update =
list[i].Marks = 90(not therangecopy).- Map: copy, change, put back, or use
map[int]*Student.
- Map: copy, change, put back, or use
- Delete =
append(list[:i], list[i+1:]...)ordelete(m, key). - Search = loop, return
(record, found). - Sort =
sort.Slice(list, func(i, j int) bool { return list[i].X < list[j].X }). - Filter = new slice, append the records that pass.
- 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 therangecopy. - For a map of structs, copy, change and put back, or store pointers.
- Sort with
sort.Sliceand 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.