Table des matières du cours
Leçon 4 — Collections : slices et maps
Par Dorian Chávez · fondateur de Hábil et architecte d'intégration ·
Durée : 90 minutes (ou 2 séances de 45).
À la fin, tu seras capable de :
- Distinguer un tableau d'un slice et dire pourquoi tu utiliseras toujours le second.
- Expliquer pourquoi copier un slice ne copie pas les données, et quand cela va te mordre.
- Utiliser une map et savoir pourquoi son parcours sort en désordre exprès.
- Parcourir des collections avec
for rangeet savoir ce que signifie le_. - Trier des résultats pour que ton programme produise toujours la même sortie.
- Lire un fichier et le convertir en une liste de données.
Pourquoi c'est important
C'est la leçon où presque tout le monde trébuche. Et pas parce qu'elle est difficile, mais parce qu'il y a un piège qu'aucun cours n'explique avant qu'il t'ait déjà mordu : en Go, copier un slice ne copie pas ses données, et ce comportement ne produit aucune erreur — il corrompt les données en silence.
Le revisor a besoin de collections pour deux choses très concrètes : une liste de services que tu vas
interroger (ce sont des slices) et un rapport qui associe chaque service à son état (ce sont des maps).
Sans slices ni maps, il n'y a pas de programme : tu ne peux pas avoir « plusieurs services » ni « l'état de
chacun » avec ce que tu as vu jusqu'à la Leçon 3.
Voici le plan de la leçon :
| 4.1 | Les tableaux : ceux que tu n'utiliseras presque pas |
| 4.2 | Les slices : ceux que tu utiliseras toujours |
| 4.3 | Le piège de la mémoire partagée |
| 4.4 | Les maps |
| 4.5 | L'ordre aléatoire, et pourquoi il est délibéré |
| 4.6 | Lire un vrai fichier |
Les concepts
4.1 Les tableaux : ceux que tu n'utiliseras presque pas
Un tableau est une liste de taille fixe. La taille fait partie du type.
Fig. 4.1 | Les tableaux, pour que tu les reconnaisses.
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]
Remarque deux choses :
Ligne 8. Le tableau naît avec les valeurs zéro, comme tout en Go. Il n'y a pas de déchets.
Lignes 19-22. Quand on affecte un tableau à une autre variable, il est copié en entier : modifier la copie ne touche pas l'original. Retiens cela, parce qu'avec les slices ce n'est pas pareil, et c'est là qu'est le piège de cette leçon.
4.2 Les slices : ceux que tu utiliseras toujours
Un slice est une liste de taille variable. C'est ce que, dans d'autres langages, tu appellerais une liste ou un tableau dynamique.
Fig. 4.2 | Créer et faire grandir un 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
Ligne 15. Un slice non initialisé vaut nil et sa longueur est 0. Mais remarque quelque chose
d'important :
Ligne 17 : nombres = append(nombres, "catalogo"). Remarque qu'il faut réaffecter. append ne
modifie pas le slice : il en renvoie un nouveau, et si tu ne gardes pas le résultat, tu le perds.
Ligne 31 : servicios[len(servicios)-1]. En Go, il n'y a pas d'indice négatif comme en Python : pour le
dernier élément, on le calcule. Et si tu dépasses la fin, le programme plante :
Fig. 4.3 | Sortir de l'intervalle.
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 Le piège de la mémoire partagée
C'est la section la plus importante de la leçon. C'est un comportement qui surprend tout le monde, qui ne produit aucune erreur, et qui peut corrompre des données en silence.
4.3.1 Le problème
Fig. 4.4 | Copier un slice ne copie pas les données.
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]
Tu as modifié copia et original a changé. Il n'y a pas eu d'erreur, pas d'avertissement, et les deux
slices affichent la même chose.
Compare-le avec la Fig. 4.1, où le même code avec un tableau copiait bel et bien. La différence est de fond.
4.3.2 Pourquoi cela arrive
Un slice ne contient pas les données : c'est une fenêtre qui pointe vers elles. À l'intérieur, il stocke trois choses :
| un pointeur | vers l'endroit où se trouvent vraiment les données |
la longueur (len) |
combien d'éléments il a maintenant |
la capacité (cap) |
combien en tiennent avant de devoir déménager |
Quand tu écris copia := original, Go copie ces trois choses — pas les données. Les deux variables
pointent vers le même endroit.
C'est comme te donner l'adresse d'une maison au lieu de t'en construire une identique : si tu peins « ta » maison, la mienne change, parce que c'est la même.
4.3.3 La solution
Fig. 4.5 | Copier pour de vrai.
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]
Maintenant, ils sont bien indépendants.
Ligne 11 : make([]int, len(original)). make crée un slice avec de l'espace réservé. C'est la façon de
dire « je veux un slice de cette longueur, avec ses valeurs zéro ».
Ligne 16 : original.... Ces trois points signifient « étale les éléments un par un ». Sans eux, tu
essaierais d'ajouter le slice complet comme un seul élément, et cela ne compile pas.
4.3.4 Et la partie vraiment traîtresse : append
Fig. 4.6 | Le même code se comporte différemment selon la capacité.
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ó
Lis cette sortie deux fois. C'est le même code —append puis modifier— et le résultat est différent
selon la capacité restante.
4.4 Les maps
Une map stocke des paires clé-valeur. C'est ce que, dans d'autres langages, on appelle un dictionnaire ou un tableau associatif.
Fig. 4.7 | Créer, écrire et lire une 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
Lignes 27-28 : voici ce qu'il faut comprendre. Lire une clé qui n'existe pas ne donne pas d'erreur :
cela renvoie la valeur zéro du type. Pour un int, c'est 0, qui pourrait être une valeur légitime.
4.4.1 La map nil : la seule qui mord
Fig. 4.8 | Une map nil peut se lire mais pas s'écrire.
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 L'ordre aléatoire, et pourquoi il est délibéré
Fig. 4.9 | Le même programme, deux exécutions, deux ordres.
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
Trois exécutions, trois ordres différents. Et ce n'est pas un défaut : Go le fait exprès, en rendant aléatoire le point de départ de chaque parcours.
4.5.1 Trier pour que la sortie soit stable
Si tu as besoin d'un ordre, il faut le demander explicitement.
Fig. 4.10 | Parcourir une map dans l'ordre alphabétique.
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
Et maintenant, oui : la même sortie, toujours. Lance le programme dix fois et elle ne change pas.
Ligne 24 : make([]string, 0, len(estados)). Longueur 0, capacité len(estados). Tu sais combien de
clés tu vas ajouter, donc on réserve l'espace d'un coup.
Ligne 25 : for nombre := range estados. Sur une map, range donne la clé dans la première variable.
Sur un slice, il donne l'indice. C'est une différence qu'il vaut mieux garder en tête :
| Collection | première variable | deuxième variable |
|---|---|---|
| slice | l'indice (0, 1, 2…) | l'élément |
| map | la clé | la valeur |
Ligne 35 : for _, nombre := range nombres. Ici nombres est un slice, donc la première variable est
l'indice, et je n'en ai pas besoin : d'où le _.
4.6 Lire un vrai fichier
Jusqu'ici, les données étaient écrites dans le programme. Cela ne sert à rien : chaque changement exige de recompiler.
Fig. 4.11 | Lire des services depuis un fichier texte.
D'abord le fichier de données, servicios.txt :
catalogo https://catalogo.example.com 443
pagos https://pagos.example.com 443
inventario https://inventario.example.com 8080
Et le programme :
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
Et voyons ce qui se passe quand quelque chose tourne mal :
$ go run fig04_11.go # con el archivo renombrado
error: leyendo servicios.txt: open servicios.txt: no such file or directory
$ echo $?
1
Ligne 20 : os.ReadFile. Lit le fichier en entier et renvoie ses octets. Pour des fichiers de
configuration, c'est parfait ; pour un fichier de plusieurs gigaoctets, il existe d'autres façons, parce que
ceci charge tout en mémoire.
Ligne 26 : string(datos). Convertit les octets en texte. strings.TrimSpace retire les espaces et les
sauts de ligne du début et de la fin — sans cela, la dernière ligne vide produirait un service fantôme.
Ligne 34 : strings.Fields. Sépare par les espaces, en fusionnant ceux qui se répètent. C'est plus
robuste que strings.Split(linea, " "), qui, avec deux espaces consécutifs, te donnerait un champ vide.
Ligne 40 : strconv.Atoi. Convertit du texte en entier (ASCII to integer). Renvoie une erreur si ce
n'est pas un nombre, et nous la vérifions.
Lignes 58-59 : la différence entre un script et un programme.
L'erreur que tu vas voir
Cette leçon grave deux messages de panic dans la mémoire, parce que ce sont les deux plus fréquents de
tout le cours après le pointeur nul :
panic: runtime error: index out of range [N] with length M (Fig. 4.3). Tu as demandé une position N
qui n'existe pas dans une collection de longueur M. Ce que cela signifie : l'indice valide le plus élevé
est M-1, pas M. Comment le corriger : vérifie len(coleccion) avant d'indexer, surtout si l'indice
vient de l'extérieur (un fichier, un argument, une requête) et que tu ne l'as pas écrit toi-même dans le code.
panic: assignment to entry in nil map (Fig. 4.8). Tu as essayé d'écrire dans une map qui n'a jamais été
créée — var m map[string]Estado laisse m à nil, et une map nil peut se lire mais pas s'écrire.
Ce que cela signifie : il manque le make(map[K]V) ou le map[K]V{} qui réserve la table en interne.
Comment le corriger : crée la map avant de l'utiliser comme destination d'une affectation ; si tu vas
seulement la lire, nil ne pose aucun problème.
Et il y a un troisième cas qui ne plante pas, et c'est pour cela qu'il est le plus dangereux des trois : modifier un slice partagé (Fig. 4.4 et Fig. 4.6) ne produit aucun message d'erreur. Le programme continue de tourner avec des données incorrectes. C'est exactement la raison d'être de la section 4.3.
Ce qu'on fait mal
- Supposer que
copia := originalcopie les données d'un slice. Cela copie le pointeur, la longueur et la capacité — pas les données. Les deux variables finissent par partager la mémoire (Fig. 4.4). - Conserver un slice reçu en paramètre sans le copier d'abord. Le comportement d'
appenddépend de la capacité qu'apporte le slice, que celui qui le reçoit ne contrôle presque jamais (Fig. 4.6). Si tu vas conserver un slice qui n'est pas à toi, copie-le ; si tu vas seulement le lire, ce n'est pas nécessaire. - Compter sur le fait qu'un
for rangesur une map sortira toujours dans le même ordre. Cela n'a jamais été garanti, et Go le rend aléatoire exprès (Fig. 4.9) pour que l'erreur apparaisse aujourd'hui et non le jour où la base de données grandira en production. - Lire
m[llave]sans vérifierokquand la valeur zéro est ambiguë. Unemap[string]intqui stocke des temps de réponse ne peut pas distinguer « a répondu en 0 ms » de « n'a jamais été interrogé » si tu n'utilises pas la forme à deux valeurs. - Indexer quelque chose qui vient de l'extérieur —un fichier,
os.Args, une réponse réseau— sans vérifierlend'abord. C'est la cause la plus courante duindex out of rangede cette leçon, et cela s'évite avec une ligne. - Écrire dans une map avant de la créer avec
makeou avec{}. Le compilateur ne le détecte pas parce quevar m map[K]Vest du code valide ; lepanicn'apparaît que lorsque le programme tourne et essaie d'écrire.
Exercices
Questions
4.1 Quelle est la différence entre [3]int et []int ?
4.2 Pourquoi [3]int et [4]int sont-ils des types différents, et quelle conséquence pratique cela
a-t-il ?
4.3 Qu'affiche ceci et pourquoi ?
a := []int{1, 2, 3}
b := a
b[0] = 99
fmt.Println(a[0])
4.4 Nomme les deux façons de copier un slice pour de vrai.
4.5 Pourquoi le même append partage-t-il parfois la mémoire avec l'original et parfois non ?
4.6 Que renvoie m["no-existe"] dans une map[string]int, et pourquoi est-ce dangereux ?
4.7 Quelle est l'asymétrie entre un slice nil et une map nil ?
4.8 Pourquoi le parcours d'une map sort-il en désordre, et comment obtiens-tu un ordre fixe ?
4.9 Dans for a, b := range x, qu'est-ce que a si x est un slice ? Et si c'est une map ?
4.10 Que fait strings.Fields que strings.Split(s, " ") ne fait pas ?
4.11 Pourquoi les erreurs vont-elles vers os.Stderr et pas vers la sortie normale ?
4.12 Ce programme a deux problèmes. Trouve-les.
func main() {
var m map[string]int
m["catalogo"] = 443
lista := []int{1, 2, 3}
fmt.Println(lista[3])
}
Exercices de code
4.13 Écris func Contar(estados map[string]Estado) (ok, fallas int) qui compte combien d'états ont un
code 200-299 et combien non. Go renvoie plusieurs valeurs : sers-t'en.
4.14 Écris func Nombres(servicios []Servicio) []string qui renvoie uniquement les noms, triés.
4.15 (Le piège) Écris une fonction func Guardar(s []int) qui stocke le slice dans une variable globale
puis l'affiche. Appelle-la ainsi :
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
Prédis ce qu'elle va afficher avant de l'exécuter. Ensuite, exécute-la. Si tu as vu juste, tu as compris la section 4.3 ; sinon, relis-la — c'est celle qui coûte le plus cher.
4.16 Étends Cargar de la Fig. 4.11 pour qu'il accepte un quatrième champ facultatif avec le délai
d'expiration en millisecondes. S'il n'est pas fourni, utilise 5000. Veille à ce qu'un fichier à trois
champs continue de fonctionner.
4.17 Écris func Agrupar(servicios []Servicio) map[int][]Servicio qui regroupe par port. Vérifie qu'un
port avec trois services les contient tous.
4.18 (Trouve l'erreur) Dis ce qui ne va pas dans chacun :
// (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 (Projet du cours) Fais passer ton programme à l'étape suivante :
Cargar(ruta string) ([]Servicio, error)qui lit le fichier, comme la Fig. 4.11.RevisarTodosqui renvoiemap[string]Estado.Reporte(estados map[string]Estado) stringqui produit le tableau trié par ordre alphabétique.- Un décompte final : « 3 de 5 respondieron ».
- Que le programme accepte le chemin du fichier comme argument :
os.Args[1]. 🔴 Vérifielen(os.Args)avant de le lire, sinon tu auras unindex out of rangequand quelqu'un le lancera sans arguments. - Fais en sorte qu'il se termine avec le code 0 si tout a répondu et 1 si quelque chose a échoué.
Corrigés
4.1 [3]int est un tableau : taille fixe, qui fait partie du type, et il est copié lors de
l'affectation. []int est un slice : taille variable, et lors de l'affectation il partage les
données.
4.2 Parce que la taille fait partie du type. La conséquence : une fonction qui reçoit [3]int n'accepte
pas un [4]int, ce qui rend les tableaux peu pratiques. C'est pourquoi on utilise des slices.
4.3 Cela affiche 99. b := a ne copie pas les données : les deux variables pointent vers la même
mémoire.
4.4 make + copy, ou append([]T(nil), original...).
4.5 Parce que cela dépend de la capacité. Si le slice a de l'espace en trop, append écrit à cet
endroit même et continue de partager la mémoire ; si cela ne tient pas, il déménage vers une nouvelle mémoire
et cesse de partager. Comme tu ne contrôles presque jamais la capacité, le même code peut se comporter
différemment.
4.6 Cela renvoie 0, la valeur zéro, sans erreur. C'est dangereux parce que 0 peut être une donnée
légitime, donc tu ne peux pas distinguer « vaut zéro » de « n'est pas là ». Cela se résout avec
v, ok := m[k].
4.7 Un slice nil accepte append sans problème. Une map nil plante quand on y écrit
(assignment to entry in nil map), bien qu'on puisse la lire. Les maps doivent être créées avec make.
4.8 Parce que leur ordre n'a jamais été garanti, et Go le rend aléatoire exprès pour que tu n'écrives
pas de programmes qui dépendent d'un ordre accidentel. Pour un ordre fixe : extraire les clés dans un slice,
les trier avec sort.Strings, et parcourir le slice.
4.9 Si x est un slice, a est l'indice. Si c'est une map, a est la clé.
4.10 Fields sépare par les espaces en fusionnant ceux qui se répètent, donc deux espaces consécutifs
ne produisent pas de champ vide. Il traite aussi les tabulations comme séparateur.
4.11 Pour que la sortie normale reste propre et puisse être utilisée dans un tube, et parce que celui qui appelle le programme s'attend à y trouver les erreurs. Avec le code de sortie différent de zéro, c'est ce qui fait qu'un programme peut être automatisé.
4.12 Les deux : (1) m est une map nil et y écrire provoque panic: assignment to entry in nil map —
il manque m := make(map[string]int) ; (2) lista[3] sort de l'intervalle, parce que les indices valides
sont 0, 1 et 2 — panic: index out of range [3] with length 3.
Exercices de code (4.13 à 4.19) : leurs corrigés vérifiés —compilés et exécutés— seront ajoutés lors de
la livraison qui clôturera le chapitre des exercices avec corrigé du cours complet ; ils ne sont pas publiés
sans avoir lancé go build sur chacun.
Comment savoir que j'y suis arrivé
- Tu peux expliquer, sans regarder le texte, pourquoi
copia := originalne copie pas les données d'un slice — et le dessiner (pointeur, longueur, capacité). - Tu as prédit correctement ce qu'affiche l'exercice 4.15 avant de l'exécuter. Si tu ne l'as pas fait, tu as repris la section 4.3 jusqu'à y arriver.
- Ton
Cargar(exercice 4.16) fonctionne toujours avec des fichiers à trois champs et accepte le quatrième facultatif. - Ton
Reporteproduit la même sortie, dans le même ordre, sur dix exécutions consécutives. - Ton programme ne plante pas avec
index out of rangesi tu le lances sans arguments : tu as vérifiélen(os.Args)avant de lireos.Args[1]. - Tu as noté dans
bitacora.mdle résultat de l'exercice 4.15 : ce que tu avais prédit et ce qui s'est réellement passé. C'est ce qui sépare comprendre les slices de croire qu'on les comprend.
Résumé
- Un tableau (
[3]int) a une taille fixe, la taille fait partie du type, et il est copié lors de l'affectation. On ne l'utilise presque pas directement. - Un slice (
[]int) a une taille variable et c'est ce qu'on utilise toujours. - Un slice stocke pointeur, longueur et capacité : il ne contient pas les données, il pointe vers elles.
- 🔴 Affecter un slice NE copie PAS les données : les deux variables partagent la mémoire. Pour copier
pour de vrai,
make+copyouappend([]T(nil), s...). - 🔴
appendpartage ou non la mémoire selon la capacité disponible, donc le même code peut se comporter différemment. Si tu vas conserver un slice qui n'est pas à toi, copie-le. - Un slice
nilfonctionne avecappend,lenetrange. Une mapnilplante quand on y écrit. - Lire une clé inexistante d'une map renvoie la valeur zéro sans erreur. Utilise
v, ok := m[k]quand l'absence compte. - Le parcours d'une map est aléatoire exprès. Pour un ordre fixe : les clés dans un slice,
sort, et parcourir le slice. - Dans
range, la première variable est l'indice pour les slices et la clé pour les maps._écarte. os.ReadFilelit un fichier en entier ;strings.Fieldssépare par les espaces en fusionnant les répétitions ;strconv.Atoiconvertit du texte en entier et renvoie une erreur.- Les erreurs vont vers
os.Stderret le programme se termine avec un code différent de zéro.
Pour aller plus loin
- Go Slices: usage and internals — le blog officiel de l'équipe de Go, explique le pointeur/longueur/capacité avec des dessins. Commence ici.
- Go maps in action — blog officiel, couvre l'ordre aléatoire et la map
nilavec plus de détails que n'en contient cette leçon. - Go by Example: Slices et Maps — code minimal, bon comme référence rapide.
- Effective Go — Slices — la section consacrée au modèle en
deux étapes (
make+copy) et à la raison pour laquelleappendse comporte comme il se comporte.
Termes de cette leçon
append |
ajoute des éléments à un slice et renvoie le résultat |
| tableau | liste de taille fixe ; la taille fait partie du type |
capacité (cap) |
combien d'éléments tiennent dans un slice avant qu'il déménage |
copy |
copie des éléments entre slices |
delete |
retire une clé d'une map |
index out of range |
panic pour avoir demandé une position qui n'existe pas |
longueur (len) |
combien d'éléments il a maintenant |
| map | collection de paires clé→valeur |
make |
crée des slices, des maps et des canaux avec de l'espace réservé |
nil map |
map non créée ; elle peut se lire mais pas s'écrire |
os.Stderr |
sortie des erreurs, séparée de la sortie normale |
| slice | liste de taille variable ; une fenêtre sur les données |
sort.Strings |
trie un slice de textes |
strings.Fields |
sépare un texte par les espaces, en fusionnant les répétitions |
strconv.Atoi |
convertit du texte en entier |
valeur à deux résultats (v, ok) |
façon de lire une map en distinguant l'absence |
Vous préférez l'e-mail ? Écrivez-nous à hola@habil.mx