Съдържание на курса
Урок 4 — Колекции: slices и maps
От Dorian Chávez · основател на Hábil и архитект на интеграции ·
Продължителност: 90 минути (или 2 сесии по 45).
Като приключиш, ще можеш:
- Да различаваш масив от slice и да кажеш защо винаги ще използваш второто.
- Да обясниш защо копирането на slice не копира данните и кога това ще те ухапе.
- Да използваш map и да знаеш защо обхождането му излиза разбъркано нарочно.
- Да обхождаш колекции с
for rangeи да знаеш какво означава_. - Да подреждаш резултатите, така че програмата ти винаги да дава един и същ изход.
- Да прочетеш файл и да го превърнеш в списък от данни.
Защо е важно
Това е урокът, в който почти всички се спъват. И не защото е труден, а защото има капан, който никой курс не обяснява, докато вече не те е ухапал: в Go копирането на slice не копира данните му и това поведение не предизвиква никаква грешка — то поврежда данни тихо.
revisor има нужда от колекции за две много конкретни неща: списък от услуги, които ще
проверяваш (това са slices), и отчет, който свързва всяка услуга с нейното състояние (това са maps). Без
slices и maps няма програма: не можеш да имаш "няколко услуги", нито "състоянието на всяка", с онова, което
видя до Урок 3.
Това е картата на урока:
| 4.1 | Масиви: онези, които почти няма да използваш |
| 4.2 | Slices: онези, които ще използваш винаги |
| 4.3 | Капанът на споделената памет |
| 4.4 | Maps |
| 4.5 | Случайният ред и защо е умишлен |
| 4.6 | Четене на истински файл |
Понятията
4.1 Масиви: онези, които почти няма да използваш
Масивът е списък с фиксиран размер. Размерът е част от типа.
Фиг. 4.1 | Масиви, за да ги разпознаваш.
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]
Обърни внимание на две неща:
Ред 8. Масивът се ражда с нулевите стойности, както всичко в Go. Няма боклук.
Редове 19-22. Когато присвоиш масив на друга променлива, той се копира целият: промяната на копието не засяга оригинала. Запомни това, защото със slices не става същото и точно там е капанът на този урок.
4.2 Slices: онези, които ще използваш винаги
Slice е списък с променлив размер. Това е, което в други езици би нарекъл списък или динамичен масив.
Фиг. 4.2 | Създаване и разрастване на 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
Ред 15. Неинициализиран slice е nil и дължината му е 0. Но обърни внимание на нещо важно:
Ред 17: nombres = append(nombres, "catalogo"). Забележи, че трябва да се присвои отново. append не
променя slice-а: връща нов и ако не запазиш резултата, го губиш.
Ред 31: servicios[len(servicios)-1]. В Go няма отрицателен индекс като в Python: за последния
елемент той се изчислява. А ако минеш след края, програмата гърми:
Фиг. 4.3 | Излизане извън диапазона.
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 Капанът на споделената памет
Това е най-важният раздел на урока. Става дума за поведение, което изненадва всички, което не предизвиква никаква грешка и което може да поврежда данни тихо.
4.3.1 Проблемът
Фиг. 4.4 | Копирането на slice не копира данните.
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]
Промени copia и се промени original. Нямаше грешка, нямаше предупреждение и двата slices показват едно и
също.
Сравни го с Фиг. 4.1, където същият код с масив наистина копираше. Разликата е дълбока.
4.3.2 Защо се случва
Slice не съдържа данните: той е прозорец, който сочи към тях. Отвътре пази три неща:
| указател | към мястото, където са истинските данни |
дължината (len) |
колко елемента има сега |
капацитетът (cap) |
колко се побират, преди да се наложи да се премести |
Когато напишеш copia := original, Go копира тези три неща — не данните. Двете променливи остават
да сочат към едно и също място.
Все едно да ти дам адреса на една къща, вместо да ти построя същата: ако боядисаш „твоята“ къща, моята се променя, защото е същата.
4.3.3 Решението
Фиг. 4.5 | Истинско копиране.
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]
Сега вече са независими.
Ред 11: make([]int, len(original)). make създава slice със запазено място. Това е начинът да
кажеш „искам slice с тази дължина, с нулевите му стойности“.
Ред 16: original.... Тези три точки означават „разпръсни елементите един по един“. Без тях
би се опитал да добавиш целия slice като един-единствен елемент, а това не се компилира.
4.3.4 И наистина коварната част: append
Фиг. 4.6 | Един и същ код се държи различно според капацитета.
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ó
Прочети този изход два пъти. Това е един и същ код — append и после промяна — а резултатът е
различен според това колко свободен капацитет е имало.
4.4 Maps
Map пази двойки ключ и стойност. Това е, което в други езици се нарича речник или асоциативна таблица.
Фиг. 4.7 | Създаване, писане и четене на 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
Редове 27-28: ето какво трябва да се разбере. Четенето на ключ, който не съществува, не дава грешка:
връща нулевата стойност на типа. За int това е 0, което може да е легитимна стойност.
4.4.1 Map nil: единственият, който хапе
Фиг. 4.8 | Map nil може да се чете, но не и да се пише в него.
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 Случайният ред и защо е умишлен
Фиг. 4.9 | Една и съща програма, две изпълнения, два реда.
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
Три изпълнения, три различни реда. И това не е дефект: Go го прави нарочно, като прави случайна началната точка при всяко обхождане.
4.5.1 Подреждане, за да е стабилен изходът
Ако ти трябва ред, той се иска изрично.
Фиг. 4.10 | Обхождане на map по азбучен ред.
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
И сега вече: един и същ изход, винаги. Пусни програмата десет пъти и той не се променя.
Ред 24: make([]string, 0, len(estados)). Дължина 0, капацитет len(estados). Знаеш колко ключа
ще добавиш, така че мястото се запазва наведнъж.
Ред 25: for nombre := range estados. Върху map range дава ключа в първата променлива.
Върху slice дава индекса. Това е разлика, която си струва да имаш предвид:
| Колекция | първа променлива | втора променлива |
|---|---|---|
| slice | индексът (0, 1, 2…) | елементът |
| map | ключът | стойността |
Ред 35: for _, nombre := range nombres. Тук nombres е slice, така че първата променлива е
индексът, а той не ми трябва: затова _.
4.6 Четене на истински файл
Досега данните бяха записани в програмата. Това не върши работа: всяка промяна изисква прекомпилиране.
Фиг. 4.11 | Четене на услуги от текстов файл.
Първо файлът с данни, servicios.txt:
catalogo https://catalogo.example.com 443
pagos https://pagos.example.com 443
inventario https://inventario.example.com 8080
И програмата:
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
И да пробваме какво става, когато нещо се обърка:
$ go run fig04_11.go # con el archivo renombrado
error: leyendo servicios.txt: open servicios.txt: no such file or directory
$ echo $?
1
Ред 20: os.ReadFile. Чете целия файл и връща байтовете му. За конфигурационни файлове
е идеално; за файл от няколко гигабайта има други начини, защото това зарежда всичко в
паметта.
Ред 26: string(datos). Превръща байтовете в текст. strings.TrimSpace маха интервалите и новите
редове от началото и края — без това последният празен ред би създал призрачна услуга.
Ред 34: strings.Fields. Разделя по интервали, като слива повтарящите се. По-надеждно е от
strings.Split(linea, " "), което при два поредни интервала би ти дало празно поле.
Ред 40: strconv.Atoi. Превръща текст в цяло число (ASCII to integer). Връща грешка, ако не е
число, и ние я проверяваме.
Редове 58-59: разликата между скрипт и програма.
Грешката, която ще видиш
Този урок оставя две съобщения за panic, запечатани в паметта, защото те са двете най-чести в
целия курс след нулевия указател:
panic: runtime error: index out of range [N] with length M (Фиг. 4.3). Поискал си позиция N, която
не съществува в колекция с дължина M. Какво означава: най-високият валиден индекс е M-1, а не M.
Как се поправя: провери len(coleccion), преди да индексираш, особено ако индексът идва отвън
(файл, аргумент, заявка) и не си го написал ти самият в кода.
panic: assignment to entry in nil map (Фиг. 4.8). Опитал си се да пишеш в map, който никога не е бил създаден —
var m map[string]Estado оставя m в nil, а map nil може да се чете, но не и да се пише в него. Какво
означава: липсва make(map[K]V) или map[K]V{}, което запазва таблицата отвътре. Как се
поправя: създай map-а, преди да го използваш като цел на присвояване; ако само ще го четеш, nil не създава
никакъв проблем.
И има трети случай, който не гърми, и затова е най-опасният от трите: промяната на споделен slice (Фиг. 4.4 и Фиг. 4.6) не предизвиква никакво съобщение за грешка. Програмата продължава да работи с невярни данни. Точно това е причината да съществува раздел 4.3.
Какво се прави погрешно
- Да приемеш, че
copia := originalкопира данните на slice. Копира указателя, дължината и капацитета — не данните. Двете променливи накрая споделят памет (Фиг. 4.4). - Да запазиш slice, който си получил като параметър, без първо да го копираш. Поведението на
appendзависи от капацитета, който носи slice-ът, а него почти никога не го контролира онзи, който го получава (Фиг. 4.6). Ако ще запазваш чужд slice, копирай го; ако само ще го четеш, не е нужно. - Да разчиташ, че
for rangeвърху map винаги ще излиза в един и същ ред. Това никога не е било гарантирано и Go го прави случаен нарочно (Фиг. 4.9), за да излезе грешката днес, а не в деня, когато базата данни в продукция порасне. - Да четеш
m[llave], без да проверявашok, когато нулевата стойност е двусмислена.map[string]int, който пази времена за отговор, не може да различи „отговори за 0 ms“ от „никога не е проверявана“, ако не използваш формата с две стойности. - Да индексираш нещо, което идва отвън — файл,
os.Args, мрежов отговор — без първо да проверишlen. Това е най-честата причина заindex out of rangeв този урок и се избягва с един ред. - Да пишеш в map, преди да си го създал с
makeили с{}. Компилаторът не го открива, защотоvar m map[K]Vе валиден код;panicсе появява едва когато програмата се изпълнява и опитва да пише.
Упражнения
Въпроси
4.1 Каква е разликата между [3]int и []int?
4.2 Защо [3]int и [4]int са различни типове и каква практическа последица има това?
4.3 Какво отпечатва това и защо?
a := []int{1, 2, 3}
b := a
b[0] = 99
fmt.Println(a[0])
4.4 Назови двата начина да се копира наистина един slice.
4.5 Защо един и същ append понякога споделя памет с оригинала, а понякога не?
4.6 Какво връща m["no-existe"] в map[string]int и защо е опасно?
4.7 Каква е асиметрията между slice nil и map nil?
4.8 Защо обхождането на map излиза разбъркано и как получаваш фиксиран ред?
4.9 Във for a, b := range x какво е a, ако x е slice? А ако е map?
4.10 Какво прави strings.Fields, което strings.Split(s, " ") не прави?
4.11 Защо грешките отиват в os.Stderr, а не в нормалния изход?
4.12 Тази програма има два проблема. Намери ги.
func main() {
var m map[string]int
m["catalogo"] = 443
lista := []int{1, 2, 3}
fmt.Println(lista[3])
}
Упражнения с код
4.13 Напиши func Contar(estados map[string]Estado) (ok, fallas int), която брои колко състояния
имат код 200-299 и колко не. Go връща няколко стойности: възползвай се от това.
4.14 Напиши func Nombres(servicios []Servicio) []string, която връща само имената, подредени.
4.15 (Капанът) Напиши функция func Guardar(s []int), която запазва slice-а в глобална променлива
и после го отпечатва. Извикай я така:
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
Предскажи какво ще отпечата, преди да го пуснеш. После го пусни. Ако си познал, разбрал си раздел 4.3; ако не — прочети го отново: той е онзи, който излиза най-скъпо.
4.16 Разшири Cargar от Фиг. 4.11, така че да приема четвърто незадължително поле с времевото ограничение (timeout) в
милисекунди. Ако го няма, използвай 5000. Внимавай файл с три полета да продължи да работи.
4.17 Напиши func Agrupar(servicios []Servicio) map[int][]Servicio, която групира по порт. Провери, че
порт с три услуги ги съдържа всичките.
4.18 (Намери грешката) Кажи какво не е наред във всеки от тях:
// (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 (Проект на курса) Изведи програмата си до следващата стъпка:
Cargar(ruta string) ([]Servicio, error), която чете файла, като Фиг. 4.11.RevisarTodos, която връщаmap[string]Estado.Reporte(estados map[string]Estado) string, която създава таблицата подредена по азбучен ред.- Окончателен брой: „3 от 5 отговориха“.
- Програмата да приема пътя на файла като аргумент:
os.Args[1]. 🔴 Провериlen(os.Args), преди да го прочетеш, иначе ще получишindex out of range, когато някой я пусне без аргументи. - Да излиза с код 0, ако всичко е отговорило, и 1, ако нещо се е провалило.
Решения
4.1 [3]int е масив: фиксиран размер, част от типа, и се копира при присвояване. []int е
slice: променлив размер, и при присвояване споделя данните.
4.2 Защото размерът е част от типа. Последицата: функция, която получава [3]int, не приема
[4]int, което прави масивите непрактични. Затова се използват slices.
4.3 Отпечатва 99. b := a не копира данните: двете променливи сочат към една и съща памет.
4.4 make + copy или append([]T(nil), original...).
4.5 Защото зависи от капацитета. Ако slice-ът има свободно място, append пише точно там и
продължава да споделя памет; ако не се побира, се мести в нова памет и престава да споделя. Тъй като почти никога
не контролираш капацитета, един и същ код може да се държи различно.
4.6 Връща 0, нулевата стойност, без грешка. Опасно е, защото 0 може да е легитимна стойност, така
че не можеш да различиш „равно е на нула“ от „го няма“. Решава се с v, ok := m[k].
4.7 Slice nil приема append без проблем. Map nil гърми, когато пишеш в него
(assignment to entry in nil map), макар че може да се чете. Maps трябва да се създават с make.
4.8 Защото редът му никога не е бил гарантиран и Go го прави случаен нарочно, за да не пишеш
програми, които зависят от случаен ред. За фиксиран ред: извади ключовете в slice, подреди ги със
sort.Strings и обходи slice-а.
4.9 Ако x е slice, a е индексът. Ако е map, a е ключът.
4.10 Fields разделя по интервали, като слива повтарящите се, така че два поредни интервала не
създават празно поле. Освен това третира табулациите като разделител.
4.11 За да остане нормалният изход чист и да може да се използва в конвейер, и защото онзи, който извиква програмата, очаква да намери грешките там. Заедно с кода на изход, различен от нула, това е, което прави една програма автоматизируема.
4.12 Двата: (1) m е map nil и писането в него предизвиква panic: assignment to entry in nil map —
липсва m := make(map[string]int); (2) lista[3] излиза извън диапазона, защото валидните индекси са 0, 1 и
2 — panic: index out of range [3] with length 3.
Упражнения с код (4.13 до 4.19): проверените им решения — компилирани и изпълнени — ще бъдат добавени във
версията, в която се затвори главата с решени упражнения за целия курс; не се публикуват,
без да е пуснат go build върху всяко от тях.
Как разбирам, че съм успял
- Можеш да обясниш, без да гледаш текста, защо
copia := originalне копира данните на slice — и да го нарисуваш (указател, дължина, капацитет). - Предсказа правилно какво отпечатва упражнение 4.15, преди да го пуснеш. Ако не позна, повтори раздел 4.3, докато не познаеш.
- Твоята
Cargar(упражнение 4.16) продължава да работи с файлове с три полета и приема четвъртото незадължително. - Твоят
Reporteдава един и същ изход, в един и същ ред, при десет поредни пускания. - Програмата ти не гърми с
index out of range, ако я пуснеш без аргументи: провериlen(os.Args), преди да прочетешos.Args[1]. - Записа в
bitacora.mdрезултата от упражнение 4.15: какво предсказа и какво се случи в действителност. Това е, което отличава разбирането на slices от вярата, че ги разбираш.
Резюме
- Масивът (
[3]int) има фиксиран размер, размерът е част от типа и се копира при присвояване. Почти не се използва директно. - Slice (
[]int) има променлив размер и е това, което се използва винаги. - Slice пази указател, дължина и капацитет: не съдържа данните, сочи към тях.
- 🔴 Присвояването на slice НЕ копира данните: двете променливи споделят памет. За истинско копиране —
make+copyилиappend([]T(nil), s...). - 🔴
appendсподеля или не памет според наличния капацитет, така че един и същ код може да се държи различно. Ако ще запазваш чужд slice, копирай го. - Slice
nilработи сappend,lenиrange. Mapnilгърми, когато пишеш в него. - Четенето на несъществуващ ключ от map връща нулевата стойност без грешка. Използвай
v, ok := m[k], когато липсата има значение. - Обхождането на map е случайно нарочно. За фиксиран ред: ключовете в slice,
sortи обхождане на slice-а. - В
rangeпървата променлива е индексът при slices и ключът при maps._изхвърля. os.ReadFileчете цял файл;strings.Fieldsразделя по интервали, като слива повтарящите се;strconv.Atoiпревръща текст в цяло число и връща грешка.- Грешките отиват в
os.Stderrи програмата излиза с код на изход, различен от нула.
За допълнително четене
- Go Slices: usage and internals — официалният блог на екипа на Go, обяснява указателя/дължината/капацитета с рисунки. Започни оттук.
- Go maps in action — официален блог, разглежда случайния ред и map
nilпо-подробно, отколкото се побира в този урок. - Go by Example: Slices и Maps — минимален код, добър за бърза справка.
- Effective Go — Slices — конкретният раздел за
шаблона от две стъпки (
make+copy) и защоappendсе държи така, както се държи.
Термини от този урок
append |
добавя елементи към slice и връща резултата |
| масив | списък с фиксиран размер; размерът е част от типа |
капацитет (cap) |
колко елемента се побират в slice, преди да се премести |
copy |
копира елементи между slices |
delete |
маха ключ от map |
index out of range |
panic заради искане на позиция, която не съществува |
дължина (len) |
колко елемента има сега |
| map | колекция от двойки ключ→стойност |
make |
създава slices, maps и канали със запазено място |
nil map |
несъздаден map; може да се чете, но не и да се пише в него |
os.Stderr |
изход за грешки, отделен от нормалния |
| slice | списък с променлив размер; прозорец към данните |
sort.Strings |
подрежда slice от текст |
strings.Fields |
разделя текст по интервали, като слива повтарящите се |
strconv.Atoi |
превръща текст в цяло число |
стойност с два резултата (v, ok) |
начин за четене от map, който различава липсата |
Предпочитате имейл? Пишете ни на hola@habil.mx