Course contents
Lesson 4 — Collections: slices and maps
By Dorian Chávez · founder of Hábil and integration architect ·
Duration: 90 minutes (or 2 sessions of 45).
By the end you will be able to:
- Tell an array from a slice and say why you will always use the second one.
- Explain why copying a slice doesn't copy the data, and when that is going to bite you.
- Use a map and know why iterating over it comes out unordered on purpose.
- Iterate over collections with
for rangeand know what the_means. - Sort results so your program always produces the same output.
- Read a file and turn it into a list of data.
Why it matters
This is the lesson where almost everyone stumbles. Not because it is hard, but because there is a trap that no course explains until it has already bitten you: in Go, copying a slice doesn't copy its data, and that behavior doesn't produce any error — it corrupts data silently.
The revisor needs collections for two very concrete things: a list of services you are going to
query (those are slices) and a report that associates each service with its state (those are maps).
Without slices and maps there is no program: you can't have "several services" or "the state of each one"
with what you saw up to Lesson 3.
This is the map of the lesson:
| 4.1 | Arrays: the ones you will hardly use |
| 4.2 | Slices: the ones you will always use |
| 4.3 | The shared-memory trap |
| 4.4 | Maps |
| 4.5 | The random order, and why it is deliberate |
| 4.6 | Reading a real file |
The concepts
4.1 Arrays: the ones you will hardly use
An array is a list of fixed size. The size is part of the type.
Fig. 4.1 | Arrays, so you can recognize them.
1 // fig04_01.go
2 // Muestra arreglos, que en Go casi no se usan directamente.
3 package main
4
5 import "fmt"
6
7 func main() {
8 var puertos [3]int // tres enteros, todos en 0
9 fmt.Println("recien creado:", puertos)
10
11 puertos[0] = 443
12 puertos[1] = 8080
13 fmt.Println("con valores:", puertos)
14 fmt.Println("cuantos caben:", len(puertos))
15
16 otros := [3]int{80, 443, 8443}
17 fmt.Println("otro arreglo:", otros)
18
19 copia := otros // los arreglos SÍ se copian completos
20 copia[0] = 999
21 fmt.Println("original:", otros)
22 fmt.Println("copia: ", copia)
23 }
$ go run fig04_01.go
recien creado: [0 0 0]
con valores: [443 8080 0]
cuantos caben: 3
otro arreglo: [80 443 8443]
original: [80 443 8443]
copia: [999 443 8443]
Notice two things:
Line 8. The array is born with zero values, like everything in Go. There is no garbage.
Lines 19-22. When you assign an array to another variable, it is copied in full: modifying the copy doesn't touch the original. Remember this, because with slices the same thing does not happen, and that is where this lesson's trap lies.
4.2 Slices: the ones you will always use
A slice is a list of variable size. It is what in other languages you would call a list or a dynamic array.
Fig. 4.2 | Creating and growing a slice.
1 // fig04_02.go
2 // Crea un slice y lo hace crecer con append.
3 package main
4
5 import "fmt"
6
7 type Servicio struct {
8 Nombre string
9 Puerto int
10 }
11
12 func main() {
13 // forma 1: vacio, y crece
14 var nombres []string
15 fmt.Printf("vacio: %v · largo %d · es nil: %t\n", nombres, len(nombres), nombres == nil)
16
17 nombres = append(nombres, "catalogo")
18 nombres = append(nombres, "pagos", "inventario")
19 fmt.Printf("con datos: %v · largo %d\n", nombres, len(nombres))
20
21 // forma 2: con valores desde el inicio
22 servicios := []Servicio{
23 {Nombre: "catalogo", Puerto: 443},
24 {Nombre: "pagos", Puerto: 443},
25 {Nombre: "local", Puerto: 8080},
26 }
27 fmt.Println("cuantos servicios:", len(servicios))
28
29 // acceder por posicion, empezando en 0
30 fmt.Println("el primero:", servicios[0].Nombre)
31 fmt.Println("el ultimo: ", servicios[len(servicios)-1].Nombre)
32 }
$ go run fig04_02.go
vacio: [] · largo 0 · es nil: true
con datos: [catalogo pagos inventario] · largo 3
cuantos servicios: 3
el primero: catalogo
el ultimo: local
Line 15. An uninitialized slice is nil and its length is 0. But notice something important:
Line 17: nombres = append(nombres, "catalogo"). Notice that you have to reassign. append
doesn't modify the slice: it returns a new one, and if you don't store the result, you lose it.
Line 31: servicios[len(servicios)-1]. In Go there is no negative index as in Python: for the last
element you compute it. And if you go past the end, the program blows up:
Fig. 4.3 | Going out of range.
1 // fig04_03.go — este programa COMPILA pero truena
2 package main
3
4 import "fmt"
5
6 func main() {
7 puertos := []int{443, 8080}
8 fmt.Println(puertos[0])
9 fmt.Println(puertos[5]) // solo hay 2 elementos
10 }
$ go run fig04_03.go
443
panic: runtime error: index out of range [5] with length 2
goroutine 1 [running]:
main.main()
/tmp/fig04_03.go:9 +0x1c
exit status 2
4.3 The shared-memory trap
This is the most important section of the lesson. It is a behavior that surprises everyone, that doesn't produce any error, and that can corrupt data silently.
4.3.1 The problem
Fig. 4.4 | Copying a slice does not copy the data.
1 // fig04_04.go
2 // Demuestra que asignar un slice NO copia sus datos.
3 package main
4
5 import "fmt"
6
7 func main() {
8 original := []int{443, 8080, 3000}
9 copia := original // parece una copia...
10
11 copia[0] = 999 // modifico la "copia"
12
13 fmt.Println("original:", original)
14 fmt.Println("copia: ", copia)
15 }
$ go run fig04_04.go
original: [999 8080 3000]
copia: [999 8080 3000]
You modified copia and original changed. There was no error, there was no warning, and both slices
show the same thing.
Compare it with Fig. 4.1, where the same code with an array did copy. The difference is fundamental.
4.3.2 Why it happens
A slice doesn't contain the data: it is a window that points to it. Inside it stores three things:
| a pointer | to where the real data is |
the length (len) |
how many elements it has now |
the capacity (cap) |
how many fit before it has to move |
When you write copia := original, Go copies those three things — not the data. Both variables end up
pointing to the same place.
It is like giving you the address of a house instead of building you an identical one: if you paint "your" house, mine changes, because it is the same one.
4.3.3 The solution
Fig. 4.5 | Copying for real.
1 // fig04_05.go
2 // Las dos formas de copiar un slice de verdad.
3 package main
4
5 import "fmt"
6
7 func main() {
8 original := []int{443, 8080, 3000}
9
10 // forma 1: make + copy
11 copia1 := make([]int, len(original))
12 copy(copia1, original)
13 copia1[0] = 111
14
15 // forma 2: append sobre un slice nil (mas corta)
16 copia2 := append([]int(nil), original...)
17 copia2[1] = 222
18
19 fmt.Println("original:", original)
20 fmt.Println("copia1: ", copia1)
21 fmt.Println("copia2: ", copia2)
22 }
$ go run fig04_05.go
original: [443 8080 3000]
copia1: [111 8080 3000]
copia2: [443 222 3000]
Now they really are independent.
Line 11: make([]int, len(original)). make creates a slice with reserved space. It is the way to
say "I want a slice of this length, with its zero values".
Line 16: original.... Those three dots mean "spread the elements one by one". Without them you would
be trying to append the whole slice as a single element, and it doesn't compile.
4.3.4 And the truly treacherous part: append
Fig. 4.6 | The same code behaves differently depending on the capacity.
1 // fig04_06.go
2 // append comparte o no memoria segun la capacidad disponible.
3 package main
4
5 import "fmt"
6
7 func main() {
8 // CASO A: hay capacidad de sobra (cap 5, largo 3)
9 a := make([]int, 3, 5)
10 a[0], a[1], a[2] = 1, 2, 3
11 fmt.Printf("A: len=%d cap=%d %v\n", len(a), cap(a), a)
12
13 b := append(a, 99) // cabe: NO se muda, comparte memoria
14 b[0] = 777
15 fmt.Println(" tras modificar b, a vale:", a, "← cambió")
16
17 // CASO B: no hay capacidad (cap 3, largo 3)
18 c := make([]int, 3, 3)
19 c[0], c[1], c[2] = 1, 2, 3
20 fmt.Printf("B: len=%d cap=%d %v\n", len(c), cap(c), c)
21
22 d := append(c, 99) // NO cabe: se muda a otra memoria
23 d[0] = 777
24 fmt.Println(" tras modificar d, c vale:", c, "← NO cambió")
25 }
$ go run fig04_06.go
A: len=3 cap=5 [1 2 3]
tras modificar b, a vale: [777 2 3] ← cambió
B: len=3 cap=3 [1 2 3]
tras modificar d, c vale: [1 2 3] ← NO cambió
Read that output twice. It is the same code —append and then modify— and the result is different
depending on how much spare capacity there was.
4.4 Maps
A map stores key-value pairs. It is what in other languages is called a dictionary or an associative table.
Fig. 4.7 | Creating, writing, and reading a map.
1 // fig04_07.go
2 // Operaciones basicas con un map.
3 package main
4
5 import "fmt"
6
7 type Estado struct {
8 Codigo int
9 Ms int
10 }
11
12 func main() {
13 // se crea con make, o vacio con {}
14 estados := make(map[string]Estado)
15
16 estados["catalogo"] = Estado{Codigo: 200, Ms: 142}
17 estados["pagos"] = Estado{Codigo: 200, Ms: 87}
18 estados["inventario"] = Estado{Codigo: 503, Ms: 2310}
19
20 fmt.Println("cuantos:", len(estados))
21
22 // leer una llave que SÍ existe
23 e := estados["catalogo"]
24 fmt.Println("catalogo:", e.Codigo, e.Ms)
25
26 // 🔑 leer una que NO existe: devuelve el valor cero, SIN error
27 fantasma := estados["no-existe"]
28 fmt.Printf("no-existe: %+v ← el valor cero\n", fantasma)
29
30 // la forma correcta de preguntar: el segundo valor
31 if e, hay := estados["pagos"]; hay {
32 fmt.Println("pagos si esta:", e.Codigo)
33 }
34 if _, hay := estados["no-existe"]; !hay {
35 fmt.Println("no-existe NO esta")
36 }
37
38 // borrar
39 delete(estados, "inventario")
40 fmt.Println("tras borrar:", len(estados))
41 }
$ go run fig04_07.go
cuantos: 3
catalogo: 200 142
no-existe: {Codigo:0 Ms:0} ← el valor cero
pagos si esta: 200
no-existe NO esta
tras borrar: 2
Lines 27-28: here is what you need to understand. Reading a key that doesn't exist doesn't give an
error: it returns the zero value of the type. For an int that is 0, which could be a legitimate
value.
4.4.1 The nil map: the only one that bites
Fig. 4.8 | A nil map can be read but not written.
1 // fig04_08.go — este programa truena al escribir
2 package main
3
4 import "fmt"
5
6 func main() {
7 var m map[string]int // nil: NO inicializado
8
9 fmt.Println("leer de un map nil:", m["x"]) // esto funciona
10 fmt.Println("su largo:", len(m)) // esto tambien
11
12 m["x"] = 1 // esto truena
13 }
$ go run fig04_08.go
leer de un map nil: 0
su largo: 0
panic: assignment to entry in nil map
goroutine 1 [running]:
main.main()
/tmp/fig04_08.go:12 +0x3c
exit status 2
4.5 The random order, and why it is deliberate
Fig. 4.9 | The same program, two runs, two orders.
1 // fig04_09.go
2 // El recorrido de un map sale en orden distinto cada vez.
3 package main
4
5 import "fmt"
6
7 func main() {
8 puertos := map[string]int{
9 "catalogo": 443,
10 "pagos": 443,
11 "inventario": 8080,
12 "reportes": 3001,
13 }
14
15 for nombre, puerto := range puertos {
16 fmt.Printf("%s=%d ", nombre, puerto)
17 }
18 fmt.Println()
19 }
$ go run fig04_09.go
reportes=3001 catalogo=443 pagos=443 inventario=8080
$ go run fig04_09.go
pagos=443 inventario=8080 reportes=3001 catalogo=443
$ go run fig04_09.go
inventario=8080 reportes=3001 catalogo=443 pagos=443
Three runs, three different orders. And it is not a defect: Go does it on purpose, randomizing the starting point on each iteration.
4.5.1 Sorting so the output is stable
If you need order, you ask for it explicitly.
Fig. 4.10 | Iterating over a map in alphabetical order.
1 // fig04_10.go
2 // Ordena las llaves para producir una salida estable.
3 package main
4
5 import (
6 "fmt"
7 "sort"
8 )
9
10 type Estado struct {
11 Codigo int
12 Ms int
13 }
14
15 func main() {
16 estados := map[string]Estado{
17 "reportes": {Codigo: 0, Ms: 0},
18 "catalogo": {Codigo: 200, Ms: 142},
19 "inventario": {Codigo: 503, Ms: 2310},
20 "pagos": {Codigo: 200, Ms: 87},
21 }
22
23 // 1. saca las llaves a un slice
24 nombres := make([]string, 0, len(estados))
25 for nombre := range estados {
26 nombres = append(nombres, nombre)
27 }
28
29 // 2. ordenalas
30 sort.Strings(nombres)
31
32 // 3. recorre el slice ordenado, no el map
33 fmt.Println("SERVICIO CODIGO TIEMPO")
34 fmt.Println("-----------------------------")
35 for _, nombre := range nombres {
36 e := estados[nombre]
37 fmt.Printf("%-12s %6d %5dms\n", nombre, e.Codigo, e.Ms)
38 }
39 }
$ go run fig04_10.go
SERVICIO CODIGO TIEMPO
-----------------------------
catalogo 200 142ms
inventario 503 2310ms
pagos 200 87ms
reportes 0 0ms
And now, yes: the same output, always. Run the program ten times and it doesn't change.
Line 24: make([]string, 0, len(estados)). Length 0, capacity len(estados). You know how many keys
you are going to add, so the space is reserved all at once.
Line 25: for nombre := range estados. Over a map, range gives the key in the first variable.
Over a slice it gives the index. It is a difference worth keeping in mind:
| Collection | first variable | second variable |
|---|---|---|
| slice | the index (0, 1, 2…) | the element |
| map | the key | the value |
Line 35: for _, nombre := range nombres. Here nombres is a slice, so the first variable is the
index, and I don't need it: that is why _.
4.6 Reading a real file
Until now the data has been written in the program. That is no good: every change requires recompiling.
Fig. 4.11 | Reading services from a text file.
First the data file, servicios.txt:
catalogo https://catalogo.example.com 443
pagos https://pagos.example.com 443
inventario https://inventario.example.com 8080
And the program:
1 // fig04_11.go
2 // Lee los servicios desde un archivo de texto.
3 package main
4
5 import (
6 "fmt"
7 "os"
8 "strconv"
9 "strings"
10 )
11
12 type Servicio struct {
13 Nombre string
14 URL string
15 Puerto int
16 }
17
18 // Cargar lee el archivo y devuelve los servicios, o un error.
19 func Cargar(ruta string) ([]Servicio, error) {
20 datos, err := os.ReadFile(ruta)
21 if err != nil {
22 return nil, fmt.Errorf("leyendo %s: %w", ruta, err)
23 }
24
25 var servicios []Servicio
26 lineas := strings.Split(strings.TrimSpace(string(datos)), "\n")
27
28 for i, linea := range lineas {
29 linea = strings.TrimSpace(linea)
30 if linea == "" || strings.HasPrefix(linea, "#") {
31 continue // salta vacias y comentarios
32 }
33
34 campos := strings.Fields(linea) // separa por espacios
35 if len(campos) != 3 {
36 return nil, fmt.Errorf("%s linea %d: esperaba 3 campos, hay %d",
37 ruta, i+1, len(campos))
38 }
39
40 puerto, err := strconv.Atoi(campos[2])
41 if err != nil {
42 return nil, fmt.Errorf("%s linea %d: puerto invalido %q: %w",
43 ruta, i+1, campos[2], err)
44 }
45
46 servicios = append(servicios, Servicio{
47 Nombre: campos[0],
48 URL: campos[1],
49 Puerto: puerto,
50 })
51 }
52 return servicios, nil
53 }
54
55 func main() {
56 servicios, err := Cargar("servicios.txt")
57 if err != nil {
58 fmt.Fprintln(os.Stderr, "error:", err)
59 os.Exit(1)
60 }
61
62 fmt.Printf("cargados %d servicios:\n", len(servicios))
63 for _, s := range servicios {
64 fmt.Printf(" %-12s %-38s :%d\n", s.Nombre, s.URL, s.Puerto)
65 }
66 }
$ go run fig04_11.go
cargados 3 servicios:
catalogo https://catalogo.example.com :443
pagos https://pagos.example.com :443
inventario https://inventario.example.com :8080
And let's try what happens when something goes wrong:
$ go run fig04_11.go # con el archivo renombrado
error: leyendo servicios.txt: open servicios.txt: no such file or directory
$ echo $?
1
Line 20: os.ReadFile. Reads the whole file and returns its bytes. For configuration files it is
perfect; for a file of several gigabytes there are other ways, because this loads everything into memory.
Line 26: string(datos). Converts the bytes to text. strings.TrimSpace removes spaces and line
breaks from the beginning and the end — without it, the last empty line would produce a phantom service.
Line 34: strings.Fields. Splits on spaces, collapsing repeated ones. It is more robust than
strings.Split(linea, " "), which with two consecutive spaces would give you an empty field.
Line 40: strconv.Atoi. Converts text to an integer (ASCII to integer). It returns an error if it is
not a number, and we are checking it.
Lines 58-59: the difference between a script and a program.
The error you will see
This lesson leaves two panic messages etched in your memory, because they are the two most frequent ones
of the whole course after the nil pointer:
panic: runtime error: index out of range [N] with length M (Fig. 4.3). You asked for a position N
that doesn't exist in a collection of length M. What it means: the highest valid index is M-1, not
M. How to fix it: check len(coleccion) before indexing, above all if the index comes from outside
(a file, an argument, a request) and you didn't write it yourself in the code.
panic: assignment to entry in nil map (Fig. 4.8). You tried to write to a map that was never created
— var m map[string]Estado leaves m as nil, and a nil map can be read but not written. What
it means: the make(map[K]V) or the map[K]V{} that reserves the table inside is missing. How to fix
it: create the map before using it as the target of an assignment; if you are only going to read it,
nil causes no problem at all.
And there is a third case that does not blow up, and that is why it is the most dangerous of the three: modifying a shared slice (Fig. 4.4 and Fig. 4.6) doesn't produce any error message. The program keeps running with incorrect data. That is exactly the reason section 4.3 exists.
What goes wrong
- Assuming that
copia := originalcopies a slice's data. It copies the pointer, the length, and the capacity — not the data. Both variables end up sharing memory (Fig. 4.4). - Storing a slice you received as a parameter without copying it first. The behavior of
appenddepends on the capacity the slice brings, which whoever receives it almost never controls (Fig. 4.6). If you are going to store someone else's slice, copy it; if you are only going to read it, there is no need. - Trusting that a
for rangeover a map will always come out in the same order. It was never guaranteed, and Go randomizes it on purpose (Fig. 4.9) so the error shows up today and not the day the database grows in production. - Reading
m[llave]without checkingokwhen the zero value is ambiguous. Amap[string]intthat stores response times can't distinguish "responded in 0 ms" from "was never queried" if you don't use the two-value form. - Indexing something that comes from outside —a file,
os.Args, a network response— without checkinglenfirst. It is the most common cause of this lesson'sindex out of range, and it is avoided with one line. - Writing to a map before creating it with
makeor with{}. The compiler doesn't detect it becausevar m map[K]Vis valid code; thepanicdoesn't appear until the program runs and tries to write.
Exercises
Questions
4.1 What is the difference between [3]int and []int?
4.2 Why are [3]int and [4]int different types, and what practical consequence does that have?
4.3 What does this print and why?
a := []int{1, 2, 3}
b := a
b[0] = 99
fmt.Println(a[0])
4.4 Name the two ways to really copy a slice.
4.5 Why does the same append sometimes share memory with the original and sometimes not?
4.6 What does m["no-existe"] return in a map[string]int, and why is it dangerous?
4.7 What is the asymmetry between a nil slice and a nil map?
4.8 Why does iterating over a map come out unordered, and how do you get a fixed order?
4.9 In for a, b := range x, what is a if x is a slice? And if it is a map?
4.10 What does strings.Fields do that strings.Split(s, " ") doesn't?
4.11 Why do errors go to os.Stderr and not to the normal output?
4.12 This program has two problems. Find them.
func main() {
var m map[string]int
m["catalogo"] = 443
lista := []int{1, 2, 3}
fmt.Println(lista[3])
}
Code exercises
4.13 Write func Contar(estados map[string]Estado) (ok, fallas int) that counts how many states have a
200-299 code and how many don't. Go returns several values: use that.
4.14 Write func Nombres(servicios []Servicio) []string that returns only the names, sorted.
4.15 (The trap) Write a function func Guardar(s []int) that stores the slice in a global variable and
then prints it. Call it like this:
datos := make([]int, 3, 10)
datos[0], datos[1], datos[2] = 1, 2, 3
Guardar(datos)
datos = append(datos, 4)
datos[0] = 999
// ahora imprime lo que guardó Guardar
Predict what it is going to print before running it. Then run it. If you got it right, you understood section 4.3; if not, read it again — it is the one that costs the most.
4.16 Extend Cargar from Fig. 4.11 so it accepts an optional fourth field with the timeout in
milliseconds. If it is not there, use 5000. Make sure a file with three fields keeps working.
4.17 Write func Agrupar(servicios []Servicio) map[int][]Servicio that groups by port. Test that a
port with three services has all of them.
4.18 (Find the error) Say what is wrong in each one:
// (a)
var m map[string]Estado
m["catalogo"] = Estado{}
// (b)
append(servicios, nuevo)
// (c)
for i := range servicios {
fmt.Println(servicios[i+1].Nombre)
}
// (d)
ms := estados["catalogo"].Ms
if ms == 0 {
fmt.Println("respondio instantaneo")
}
4.19 (Course project) Take your program to the next step:
Cargar(ruta string) ([]Servicio, error)that reads the file, like Fig. 4.11.RevisarTodosthat returnsmap[string]Estado.Reporte(estados map[string]Estado) stringthat produces the table sorted alphabetically.- A final count: "3 of 5 responded".
- Make the program accept the file path as an argument:
os.Args[1]. 🔴 Checklen(os.Args)before reading it, or you will get anindex out of rangewhen someone runs it without arguments. - Make it exit with code 0 if everything responded and 1 if something failed.
Solutions
4.1 [3]int is an array: fixed size, part of the type, and it is copied when assigned. []int is
a slice: variable size, and when assigned it shares the data.
4.2 Because the size is part of the type. The consequence: a function that receives [3]int doesn't
accept a [4]int, which makes arrays impractical. That is why slices are used.
4.3 It prints 99. b := a doesn't copy the data: both variables point to the same memory.
4.4 make + copy, or append([]T(nil), original...).
4.5 Because it depends on the capacity. If the slice has spare room, append writes right there
and keeps sharing memory; if it doesn't fit, it moves to new memory and stops sharing. Since you almost
never control the capacity, the same code can behave differently.
4.6 It returns 0, the zero value, without an error. It is dangerous because 0 can be legitimate
data, so you can't distinguish "it is zero" from "it isn't there". It is solved with v, ok := m[k].
4.7 A nil slice accepts append without problem. A nil map blows up when you write to it
(assignment to entry in nil map), although it can be read. Maps have to be created with make.
4.8 Because its order was never guaranteed, and Go randomizes it on purpose so you don't write
programs that depend on an accidental order. For a fixed order: extract the keys into a slice, sort them
with sort.Strings, and iterate over the slice.
4.9 If x is a slice, a is the index. If it is a map, a is the key.
4.10 Fields splits on spaces collapsing repeated ones, so two consecutive spaces don't produce an
empty field. It also treats tabs as a separator.
4.11 So that the normal output stays clean and can be used in a pipe, and because whoever calls the program expects to find the errors there. Together with the nonzero exit code, that is what makes a program automatable.
4.12 Both: (1) m is a nil map and writing to it causes panic: assignment to entry in nil map —
m := make(map[string]int) is missing; (2) lista[3] goes out of range, because the valid indexes are 0,
1, and 2 — panic: index out of range [3] with length 3.
Code exercises (4.13 to 4.19): their verified solutions —compiled and run— will be added in the
delivery in which the chapter of exercises with solutions for the whole course is closed; they are not
published without having run go build on each one.
How I know I got it
- You can explain, without looking at the text, why
copia := originaldoesn't copy a slice's data — and draw it (pointer, length, capacity). - You correctly predicted what exercise 4.15 prints before running it. If you didn't get it right, you went over section 4.3 again until you did.
- Your
Cargar(exercise 4.16) keeps working with three-field files and accepts the optional fourth one. - Your
Reporteproduces the same output, in the same order, in ten runs in a row. - Your program doesn't blow up with
index out of rangeif you run it without arguments: you checkedlen(os.Args)before readingos.Args[1]. - You wrote down in
bitacora.mdthe result of exercise 4.15: what you predicted and what really happened. That is what separates understanding slices from believing you understand them.
Summary
- An array (
[3]int) has a fixed size, the size is part of the type, and it is copied when assigned. It is hardly ever used directly. - A slice (
[]int) has a variable size and is what is always used. - A slice stores pointer, length, and capacity: it doesn't contain the data, it points to it.
- 🔴 Assigning a slice does NOT copy the data: both variables share memory. To really copy,
make+copyorappend([]T(nil), s...). - 🔴
appendshares memory or not depending on the available capacity, so the same code can behave differently. If you are going to store someone else's slice, copy it. - A
nilslice works withappend,len, andrange. Anilmap blows up when you write to it. - Reading a nonexistent key from a map returns the zero value without an error. Use
v, ok := m[k]when absence matters. - Iterating over a map is random on purpose. For a fixed order: keys into a slice,
sort, and iterate over the slice. - In
range, the first variable is the index in slices and the key in maps._discards. os.ReadFilereads a whole file;strings.Fieldssplits on spaces collapsing repeated ones;strconv.Atoiconverts text to an integer and returns an error.- Errors go to
os.Stderrand the program exits with a nonzero exit code.
Further reading
- Go Slices: usage and internals — the official blog of the Go team, explains pointer/length/capacity with drawings. Start here.
- Go maps in action — official blog, covers the random order and the
nilmap in more detail than fits in this lesson. - Go by Example: Slices and Maps — minimal code, good as a quick reference.
- Effective Go — Slices — the specific section on the
two-step pattern (
make+copy) and whyappendbehaves the way it does.
Terms from this lesson
append |
adds elements to a slice and returns the result |
| array | fixed-size list; the size is part of the type |
capacity (cap) |
how many elements fit in a slice before it moves |
copy |
copies elements between slices |
delete |
removes a key from a map |
index out of range |
panic from asking for a position that doesn't exist |
length (len) |
how many elements it has now |
| map | collection of key→value pairs |
make |
creates slices, maps, and channels with reserved space |
nil map |
map not created; it can be read but not written |
os.Stderr |
error output, separate from the normal one |
| slice | variable-size list; a window over the data |
sort.Strings |
sorts a slice of text |
strings.Fields |
splits a text on spaces, collapsing repeated ones |
strconv.Atoi |
converts text to an integer |
two-result value (v, ok) |
way of reading a map that distinguishes absence |
Prefer email? Write to us at hola@habil.mx