Go Tutorials: Arrays, Slices, Maps, and Structs

These four concepts are fundamental Go data structures.

A simple way to remember them:

  • Array → fixed number of values.
  • Slice → flexible list of values.
  • Map → key → value lookup.
  • Struct → group different fields into one meaningful object.

1. Arrays

What is an Array?

An array stores multiple values of the same data type.

The important point is:

An array has a fixed size.

Example:

var marks [5]int

This creates an array capable of storing exactly 5 integers.

Conceptually:

Index:   0   1   2   3   4
       +---+---+---+---+---+
Value: |10 |20 |30 |40 |50 |
       +---+---+---+---+---+

Indexes start from 0.


How to create an Array

numbers := [5]int{10, 20, 30, 40, 50}

Go can also calculate the size:

numbers := [...]int{10, 20, 30, 40, 50}

Here ... means:

“Go, count the elements and determine the array size.”

The result is still:

[5]int

Accessing Array Elements

fmt.Println(numbers[0])

Output:

10

Change a value:

numbers[2] = 100

Now:

[10 20 100 40 50]

Loop through an Array

for index, value := range numbers {
    fmt.Println(index, value)
}

Output:

0 10
1 20
2 30
3 40
4 50

Why use Arrays?

Arrays are useful when the number of elements is known and fixed.

For example:

days := [7]string{
    "Monday",
    "Tuesday",
    "Wednesday",
    "Thursday",
    "Friday",
    "Saturday",
    "Sunday",
}

There will always be exactly 7 days in this particular representation.


When should I use an Array?

Use an array when:

  • size is known
  • size will not change
  • fixed-size data is meaningful
  • memory layout matters
  • you specifically need [N]T

Examples:

RGB values
coordinates
matrix dimensions
fixed sensor readings
cryptographic data
fixed buffers

Example:

rgb := [3]int{255, 100, 50}

2. Slices

What is a Slice?

A slice is similar to an array but much more flexible.

A slice:

stores multiple values of the same type but can grow and shrink logically.

Example:

names := []string{"John", "David", "Mary"}

Notice the difference.

Array:

[3]string

Slice:

[]string

No number inside [].


Array vs Slice syntax

Array:

numbers := [3]int{10, 20, 30}

Slice:

numbers := []int{10, 20, 30}

That small syntax difference is very important.


Why do Slices exist?

Imagine you’re storing users.

Initially:

Alice
Bob
John

Tomorrow another user registers:

Mary

With a fixed-size array, this can become inconvenient.

With a slice:

users := []string{"Alice", "Bob", "John"}

users = append(users, "Mary")

Now:

[Alice Bob John Mary]

This is why slices are used far more frequently than arrays in normal Go programs.


append()

append() adds an element to a slice.

numbers := []int{10, 20}

numbers = append(numbers, 30)

Result:

[10 20 30]

Add multiple values:

numbers = append(numbers, 40, 50, 60)

Result:

[10 20 30 40 50 60]

len() and cap()

Slices have two important properties:

len(slice)
cap(slice)

len

Number of elements currently inside the slice.

cap

Amount of space available in the underlying storage before Go may need to allocate more space.

Example:

numbers := make([]int, 3, 10)

fmt.Println(len(numbers))
fmt.Println(cap(numbers))

Output:

3
10

Think of it like a parking lot:

Capacity = 10 parking spaces
Length   = 3 cars currently parked

+---+---+---+---+---+---+---+---+---+---+
| X | X | X |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+---+---+

len = 3
cap = 10

make() with Slices

Another common way to create slices is:

numbers := make([]int, 5)

This creates:

[0 0 0 0 0]

Because the zero value of int is 0.

You can specify capacity too:

numbers := make([]int, 5, 10)

Meaning:

length   = 5
capacity = 10

Slicing a Slice

Suppose:

numbers := []int{10, 20, 30, 40, 50}

You can take part of it:

part := numbers[1:4]

Result:

[20 30 40]

Why?

Go slicing follows:

[start : end]

Start is included.

End is excluded.

So:

numbers[1:4]

index 1 -> included
index 2 -> included
index 3 -> included
index 4 -> excluded

When should I use a Slice?

Use slices when:

  • number of values can change
  • processing lists of records
  • returning multiple items
  • working with database results
  • working with API results
  • storing users/products/orders
  • filtering data
  • sorting data

Example:

users := []string{"Alice", "Bob"}

users = append(users, "Charlie")

In everyday Go development:

Slices are usually preferred over arrays.


3. Maps

What is a Map?

A map stores data as:

key -> value

For example:

name -> John
age  -> 30

But all keys must have one type and all values must have one type for a given map.

Example:

ages := map[string]int{
    "John":  30,
    "Alice": 25,
    "Bob":   35,
}

Here:

Key type   = string
Value type = int

So:

map[string]int

means:

Map where the key is a string and the value is an integer.


Easy real-world example

Think of a phone book.

Name        Phone Number

John   ->   5551000
Alice  ->   5552000
Bob    ->   5553000

In Go:

phones := map[string]string{
    "John":  "5551000",
    "Alice": "5552000",
    "Bob":   "5553000",
}

Access a Map Value

fmt.Println(phones["John"])

Output:

5551000

Add a Map Value

phones["David"] = "5554000"

Now the map contains David.


Update a Map Value

phones["John"] = "9999999"

The old value is replaced.


Delete from a Map

Use:

delete(phones, "John")

Now John’s entry is removed.


Very Important: Checking if a Key Exists

Suppose:

ages := map[string]int{
    "John": 30,
}

You could write:

age := ages["David"]

But David doesn’t exist.

Go returns the zero value:

0

That creates ambiguity.

Maybe David exists and his age is actually 0.

So Go provides this syntax:

age, exists := ages["David"]

Then:

if exists {
    fmt.Println("Age:", age)
} else {
    fmt.Println("User not found")
}

This pattern is extremely common in Go.


Loop through a Map

for name, age := range ages {
    fmt.Println(name, age)
}

Important:

Do not rely on normal map iteration being in a particular order.


Creating a Map with make()

You can also write:

ages := make(map[string]int)

Then:

ages["John"] = 30
ages["Alice"] = 25

When should I use a Map?

Maps are useful when you need fast lookup using some identifier.

For example:

username -> User
userID   -> User
country  -> capital
productID -> Product
setting -> value
word -> count

Example:

countries := map[string]string{
    "IN": "India",
    "US": "United States",
    "JP": "Japan",
}

Then:

fmt.Println(countries["IN"])

Output:

India

4. Structs

What is a Struct?

A struct groups several related pieces of data together.

Unlike an array or slice, the fields can have different data types.

For example, a person has:

Name
Age
Email
Active

These aren’t necessarily the same type.

We can define:

type Person struct {
    Name   string
    Age    int
    Email  string
    Active bool
}

Now Person becomes our own custom type.


Think of Struct as a Form

Imagine a registration form:

-----------------------
Name:   John
Age:    30
Email:  john@test.com
Active: true
-----------------------

That could be represented as:

type User struct {
    Name   string
    Age    int
    Email  string
    Active bool
}

Creating a Struct

user := User{
    Name:   "John",
    Age:    30,
    Email:  "john@example.com",
    Active: true,
}

Accessing Struct Fields

Use a dot:

fmt.Println(user.Name)
fmt.Println(user.Email)

Output:

John
john@example.com

Updating Struct Fields

user.Age = 31

Now:

Age = 31

Why use Structs?

Without a struct, you might have separate variables:

name := "John"
age := 30
email := "john@example.com"
active := true

Imagine having 100 users.

That quickly becomes difficult.

Instead:

type User struct {
    Name   string
    Age    int
    Email  string
    Active bool
}

Then you can create:

user1 := User{...}
user2 := User{...}
user3 := User{...}

Or even better:

users := []User{
    {...},
    {...},
    {...},
}

This combines slices + structs.

This is extremely common in Go applications.


When should I use a Struct?

Use structs to represent real things/concepts such as:

User
Employee
Product
Order
Vehicle
Customer
Server
Configuration
API response
Database record

Example:

type Product struct {
    ID       int
    Name     string
    Price    float64
    InStock  bool
}

Big Comparison Table

FeatureArraySliceMapStruct
PurposeFixed collectionFlexible collectionKey-value storageRepresent an object/entity
Syntax[5]int[]intmap[string]intstruct {...}
SizeFixedDynamicDynamicFixed fields by type definition
Same value type?YesYesValues share a typeFields can have different types
Access usingIndexIndexKeyField name
Example accessa[0]s[0]m["John"]u.Name
Can grow?NoYesYesFields don’t dynamically grow
append()NoYesNoNo
delete()NoNoYesNo
Common useFixed dataListsLookupsObjects/entities
Typical exampleRGB [3]int[]Usermap[int]UserUser
Used frequently?Less oftenVery oftenVery oftenVery often

Think About Them This Way

StructureReal-world analogy
ArrayRow of exactly 5 lockers
SliceExpandable shopping list
MapDictionary / phone book
StructRegistration form

One Complete Example Using All Four

Imagine we’re creating a small employee application.

package main

import "fmt"

// Employee is a STRUCT.
//
// A struct groups related information into one type.
//
// Every employee has:
// - ID
// - Name
// - Department
// - Skills
type Employee struct {
	ID         int
	Name       string
	Department string
	Skills     []string // A struct field can itself contain a slice.
}

func main() {

	// ---------------------------------------------------
	// 1. ARRAY
	// ---------------------------------------------------

	// An array has a FIXED size.
	//
	// Here we are saying:
	// "Store exactly 3 office locations."
	//
	// [3]string
	//  ^   ^
	//  |   |
	// size data type

	offices := [3]string{
		"New York",
		"London",
		"Tokyo",
	}

	fmt.Println("Offices:")

	for _, office := range offices {
		fmt.Println(office)
	}


	// ---------------------------------------------------
	// 2. SLICE
	// ---------------------------------------------------

	// A slice is a flexible collection.
	//
	// Unlike the array above, we don't specify a size.
	//
	// []Employee means:
	// "A slice containing Employee structs."

	employees := []Employee{
		{
			ID:         1,
			Name:       "Alice",
			Department: "Engineering",
			Skills:     []string{"Go", "Docker"},
		},
		{
			ID:         2,
			Name:       "Bob",
			Department: "DevOps",
			Skills:     []string{"Linux", "Kubernetes"},
		},
	}


	// append() allows the slice to grow.
	//
	// We add another Employee to the existing slice.

	employees = append(employees, Employee{
		ID:         3,
		Name:       "Charlie",
		Department: "Engineering",
		Skills:     []string{"Go", "AWS"},
	})


	// ---------------------------------------------------
	// 3. MAP
	// ---------------------------------------------------

	// We create a map for quick employee lookup.
	//
	// Key:
	//      int
	//
	// Value:
	//      Employee
	//
	// Therefore:
	//
	// map[int]Employee

	employeeByID := make(map[int]Employee)


	// Loop through employees and put each one into the map.

	for _, employee := range employees {

		// employee.ID becomes the key.
		//
		// Example:
		//
		// 1 -> Alice
		// 2 -> Bob
		// 3 -> Charlie

		employeeByID[employee.ID] = employee
	}


	// ---------------------------------------------------
	// MAP LOOKUP
	// ---------------------------------------------------

	// Search for employee ID 2.

	employee, exists := employeeByID[2]

	// exists tells us whether the key was found.

	if exists {
		fmt.Println("\nEmployee found:")
		fmt.Println("Name:", employee.Name)
		fmt.Println("Department:", employee.Department)
		fmt.Println("Skills:", employee.Skills)
	}


	// ---------------------------------------------------
	// LOOP THROUGH SLICE OF STRUCTS
	// ---------------------------------------------------

	fmt.Println("\nAll Employees:")

	for _, employee := range employees {

		// employee is an Employee STRUCT.
		//
		// Therefore we access fields using:
		//
		// employee.Name
		// employee.Department

		fmt.Println(
			employee.ID,
			employee.Name,
			employee.Department,
		)
	}
}

Possible output:

Offices:
New York
London
Tokyo

Employee found:
Name: Bob
Department: DevOps
Skills: [Linux Kubernetes]

All Employees:
1 Alice Engineering
2 Bob DevOps
3 Charlie Engineering

What Happened in This Program?

The program demonstrates how these data structures often work together, rather than separately.

Array

offices := [3]string{
    "New York",
    "London",
    "Tokyo",
}

We know there are exactly three office locations for this example.

Array

[New York] [London] [Tokyo]
     0         1        2

Struct

We define what an employee looks like:

type Employee struct {
    ID         int
    Name       string
    Department string
    Skills     []string
}

One employee might be:

Employee
+-------------------------+
| ID         = 1          |
| Name       = Alice      |
| Department = Engineering|
| Skills     = Go,Docker  |
+-------------------------+

Slice

Then we need many employees:

employees := []Employee{
    ...
}

Conceptually:

employees slice

        |
        v

+-----------+
| Employee  |
| Alice     |
+-----------+
      |
+-----------+
| Employee  |
| Bob       |
+-----------+
      |
+-----------+
| Employee  |
| Charlie   |
+-----------+

A slice of structs is extremely common:

[]Employee

Other examples:

[]User
[]Product
[]Order
[]Vehicle
[]Server

Map

Then suppose we want to quickly find an employee using their ID.

We create:

map[int]Employee

Conceptually:

Employee ID         Employee

    1       ---->     Alice

    2       ---->     Bob

    3       ---->     Charlie

Then:

employeeByID[2]

gives us Bob.


Which One Should I Choose?

A very useful decision process is:

Do I need to represent ONE thing
with different properties?

        YES
         |
         v
       Struct

Example:

type User struct {
    Name string
    Age  int
}

If you’re storing many things:

Do I know the EXACT fixed number
and it should remain fixed?

        YES
         |
         v
       Array

Example:

coordinates := [3]float64{x, y, z}

Otherwise:

Need an ordered/flexible list?

        YES
         |
         v
       Slice

Example:

users := []User{}

If instead:

Need to find something
using a KEY?

        YES
         |
         v
        Map

Example:

usersByID := map[int]User{}

Practical Application Example

Imagine you’re building an e-commerce system.

Product Struct

One product:

type Product struct {
    ID    int
    Name  string
    Price float64
}

Slice of Products

All products:

products := []Product{
    {ID: 1, Name: "Laptop", Price: 1200},
    {ID: 2, Name: "Mouse", Price: 25},
    {ID: 3, Name: "Keyboard", Price: 70},
}

Think:

Product
Product
Product
Product
Product
...

Map of Products

Need fast lookup by product ID:

productsByID := map[int]Product{
    1: {ID: 1, Name: "Laptop", Price: 1200},
    2: {ID: 2, Name: "Mouse", Price: 25},
}

Then:

product := productsByID[2]

Array

Maybe your application has exactly 3 pricing tiers:

pricingTiers := [3]string{
    "Basic",
    "Professional",
    "Enterprise",
}

Very Important Combination Patterns

In real Go applications, you’ll frequently encounter combinations.

Slice of Structs

[]User

Meaning:

Many users.

Example:

users := []User{
    {Name: "Alice"},
    {Name: "Bob"},
}

Probably the most common pattern.


Map of Structs

map[int]User

Meaning:

Find a user using an integer ID.

Example:

users := map[int]User{
    1: {Name: "Alice"},
    2: {Name: "Bob"},
}

Map of Slices

map[string][]string

Meaning:

Each key points to a list.

Example:

skills := map[string][]string{
    "Alice": {"Go", "Docker", "AWS"},
    "Bob":   {"Java", "Spring"},
}

Conceptually:

Alice ----> Go
            Docker
            AWS

Bob ------> Java
            Spring

Struct containing Slices

type User struct {
    Name   string
    Skills []string
}

Meaning:

Each user can have many skills.


Struct containing a Map

type User struct {
    Name     string
    Settings map[string]string
}

Example:

user := User{
    Name: "Alice",
    Settings: map[string]string{
        "theme":    "dark",
        "language": "English",
    },
}

Important Difference: Array vs Slice

This is one of the most important things beginners should understand.

These are different types:

[3]int

and

[4]int

For example:

a := [3]int{1, 2, 3}

b := [4]int{1, 2, 3, 4}

a and b have different types.

But:

a := []int{1, 2, 3}

b := []int{1, 2, 3, 4}

Both are:

[]int

That is another reason slices are more convenient for general application development.


Quick Syntax Cheat Sheet

Array

numbers := [3]int{10, 20, 30}

Think:

Exactly 3 integers

Slice

numbers := []int{10, 20, 30}

Think:

Flexible list of integers

Add:

numbers = append(numbers, 40)

Map

ages := map[string]int{
    "Alice": 30,
    "Bob":   40,
}

Think:

string -> int

Lookup:

age := ages["Alice"]

Safe lookup:

age, exists := ages["Alice"]

Struct

type User struct {
    Name string
    Age  int
}

Create:

user := User{
    Name: "Alice",
    Age:  30,
}

Access:

fmt.Println(user.Name)

Final Mental Model

The easiest way to remember everything is:

ARRAY
"I need exactly N values."

[3]int
   ↓
[10, 20, 30]
SLICE
"I need a flexible list."

[]int
  ↓
[10, 20, 30, ...]
MAP
"I want to find a value using a key."

map[string]int

Alice ----> 30
Bob   ----> 40
John  ----> 25
STRUCT
"I want to describe one thing."

User
 |
 +-- Name
 +-- Age
 +-- Email
 +-- Active

And in a typical Go application, these often become:

Struct = defines ONE object

          ↓

type User struct {
    ID   int
    Name string
}

          ↓

Slice = stores MANY objects

[]User

          ↓

Map = quickly FINDS objects

map[int]User

So a particularly useful rule to remember is:

Struct = one thing. Slice = many things. Map = find things by key. Array = fixed number of things.

Related Posts

Go Tutorials: Go Methods

Go methods become much easier once you understand one idea: A method is simply a function attached to a type. And the important rule is: A receiver…

Read More

Go Tutorials: Modules, Packages, Subpackages, Submodules, and Workspaces

This tutorial gives you one complete mental model for: The most important idea is: And sometimes: These two designs are very different. 1. What is a Go…

Read More

Go Tutorials: Numeric Types Beginner Tutorial

Go has four main families of numeric types: The simplest way to remember them is: Type family Stores Example int Whole numbers, positive or negative -10, 0,…

Read More

Go Tutorials: Go Testing, Benchmarking, and Profiling

These three topics become much easier once you separate the questions they answer: Topic Main question Main Go tool Result Testing Does my code work correctly? go…

Read More

Go Tutorials: Generics

1. What are Generics? Generics let you write one piece of code that works with multiple types while keeping Go’s compile-time type safety. Suppose you want a…

Read More

Go Concurrency Management Made Simple

This tutorial covers: The goal is: One concept → one purpose → one complete runnable example. Every example is independent. Save any example as: and run: 0….

Read More