Съдържание на курса

Урок 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 (Проект на курса) Изведи програмата си до следващата стъпка:

  1. Cargar(ruta string) ([]Servicio, error), която чете файла, като Фиг. 4.11.
  2. RevisarTodos, която връща map[string]Estado.
  3. Reporte(estados map[string]Estado) string, която създава таблицата подредена по азбучен ред.
  4. Окончателен брой: „3 от 5 отговориха“.
  5. Програмата да приема пътя на файла като аргумент: os.Args[1]. 🔴 Провери len(os.Args), преди да го прочетеш, иначе ще получиш index out of range, когато някой я пусне без аргументи.
  6. Да излиза с код 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. Map nil гърми, когато пишеш в него.
  • Четенето на несъществуващ ключ от map връща нулевата стойност без грешка. Използвай v, ok := m[k], когато липсата има значение.
  • Обхождането на map е случайно нарочно. За фиксиран ред: ключовете в slice, sort и обхождане на slice-а.
  • В range първата променлива е индексът при slices и ключът при maps. _ изхвърля.
  • os.ReadFile чете цял файл; strings.Fields разделя по интервали, като слива повтарящите се; strconv.Atoi превръща текст в цяло число и връща грешка.
  • Грешките отиват в os.Stderr и програмата излиза с код на изход, различен от нула.

За допълнително четене

  1. Go Slices: usage and internals — официалният блог на екипа на Go, обяснява указателя/дължината/капацитета с рисунки. Започни оттук.
  2. Go maps in action — официален блог, разглежда случайния ред и map nil по-подробно, отколкото се побира в този урок.
  3. Go by Example: Slices и Maps — минимален код, добър за бърза справка.
  4. 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