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

Урок 3 — Structs, методи, грешки и интерфейси

От Dorian Chávez · основател на Hábil и архитект на интеграции ·

Тук Go става Go. Това е най-важният урок от курса.

Продължителност: две сесии по 60 минути. Не го минавай наведнъж.

Като приключиш, ще можеш:

  • Да групираш свързани данни в struct и да обясниш защо това е по-добре от отделни променливи.
  • Да пишеш методи и да решаваш кога да използваш получател по стойност и кога по указател.
  • Да обясниш какво е указател, без да се плашиш.
  • Да обработваш грешките като стойности, да ги обвиваш и да питаш за тях.
  • Да дефинираш интерфейс и да разбереш защо в Go не се декларира, че той се изпълнява.
  • Да пишеш код, който може да се тества, без да се свързва с каквото и да е.

Защо е важно

В урок 2 записа данните на една услуга в три отделни променливи (nombre, url, puerto). Това работи с една услуга. С десет са тридесет разпилени променливи и нищо в кода не казва кои вървят заедно — ако сбъркаш и съчетаеш името на една с порта на друга, програмата пак се компилира и създава грешен отчет. Това е първият проблем, който решава този урок: struct.

Вторият проблем е по-дълбок. revisor ще трябва да проверява истински услуги, а истинските проверки се провалят: мрежата пада, услугата се бави, отговаря с код, който не си очаквал. Програма, която не умее да се справя с това подредено, не става за нищо в продукция. Go решава това по начин, който вероятно не си виждал, ако идваш от друг език: грешките са стойности, а не изключения, които прекъсват потока.

А третият проблем е онзи, който кара двете предишни части да пасват една на друга, без revisor да трябва да знае предварително как ще бъде проверявана всяка услуга (HTTP днес, може би база данни утре). Това го решава интерфейсът, който освен това ще ти позволи в урок 5 да тестваш цялата програма, без да се свързваш с нищо реално.

Structs, грешки и интерфейси са, в този ред, гръбнакът на всичко, което следва в курса.


Понятията

3.1 Structs: групиране на онова, което върви заедно

Това, от което имаш нужда, е да кажеш на езика: „тези три неща са една услуга“.

Фиг. 3.1 | Дефиниране и използване на struct.

 1  // fig03_01.go
 2  // Agrupa los datos de un servicio en un solo tipo.
 3  package main
 4
 5  import "fmt"
 6
 7  // Servicio agrupa todo lo que describe a un servicio que vamos a revisar.
 8  type Servicio struct {
 9      Nombre string
10      URL    string
11      Puerto int
12  }
13
14  func main() {
15      s := Servicio{
16          Nombre: "catalogo",
17          URL:    "https://catalogo.example.com",
18          Puerto: 443,
19      }
20
21      fmt.Println("nombre:", s.Nombre)
22      fmt.Println("url:", s.URL)
23      fmt.Println("puerto:", s.Puerto)
24
25      s.Puerto = 8443          // se puede modificar
26      fmt.Println("nuevo puerto:", s.Puerto)
27
28      fmt.Println(s)           // e imprimir completo
29  }
$ go run fig03_01.go
nombre: catalogo
url: https://catalogo.example.com
puerto: 443
nuevo puerto: 8443
{catalogo https://catalogo.example.com 8443}

Ред 8: type Servicio struct {. Чете се: „дефинирай нов тип, наречен Servicio, който е структура“. Току-що създаде тип, който не е идвал с езика, и оттук нататък той се използва също като int или string.

Редове 15-19. Създава стойност от този тип. Имената на полетата с двоеточие не са задължителни — би могъл да напишеш само стойностите по ред — но слагай ги винаги:

Ред 21: s.Nombre. Точката дава достъп до поле. Чете се „Nombre на s“.

3.1.1 Главната буква е разрешение, не естетика

Забележи, че полетата започват с главна буква: Nombre, URL, Puerto. В Go това не е избран стил: това е контролът на достъпа в езика.

Nombre (главна буква) експортиран: видим от други пакети
nombre (малка буква) неекспортиран: видим само в собствения си пакет

Няма public, private, нито protected. Началната буква е цялото правило.

3.1.2 Нулевата стойност на един struct

Фиг. 3.2 | Неинициализиран struct.

 1  // fig03_02.go
 2  package main
 3
 4  import "fmt"
 5
 6  type Servicio struct {
 7      Nombre string
 8      URL    string
 9      Puerto int
10  }
11
12  func main() {
13      var vacio Servicio
14      fmt.Printf("%+v\n", vacio)
15      fmt.Println("¿el nombre está vacío?", vacio.Nombre == "")
16      fmt.Println("puerto:", vacio.Puerto)
17  }
$ go run fig03_02.go
{Nombre: URL: Puerto:0}
¿el nombre está vacío? true
puerto: 0

Всяко поле получава своята нулева стойност: текстовете остават "", а числата 0. Няма боклук, нито „недефинирано“, и затова току-що създаден struct вече може да се използва без страх.

И тук се появява %+v, който е много полезен за отстраняване на грешки: отпечатва struct с имената на полетата. Сравни го с %v, който отпечатва само стойностите.

3.2 Методи

Методът е функция, която принадлежи на тип. Вместо да пишеш etiqueta(s), пишеш s.Etiqueta() и функцията остава свързана с типа, на който съответства.

Фиг. 3.3 | Методи на struct.

 1  // fig03_03.go
 2  // Define metodos que pertenecen al tipo Servicio.
 3  package main
 4
 5  import "fmt"
 6
 7  type Servicio struct {
 8      Nombre string
 9      URL    string
10      Puerto int
11  }
12
13  // Etiqueta devuelve una descripcion legible del servicio.
14  func (s Servicio) Etiqueta() string {
15      return fmt.Sprintf("%s (%s:%d)", s.Nombre, s.URL, s.Puerto)
16  }
17
18  // EsSeguro indica si el servicio usa el puerto de HTTPS.
19  func (s Servicio) EsSeguro() bool {
20      return s.Puerto == 443
21  }
22
23  func main() {
24      a := Servicio{Nombre: "catalogo", URL: "https://catalogo.example.com", Puerto: 443}
25      b := Servicio{Nombre: "local", URL: "http://localhost", Puerto: 8080}
26
27      fmt.Println(a.Etiqueta(), "— seguro:", a.EsSeguro())
28      fmt.Println(b.Etiqueta(), "— seguro:", b.EsSeguro())
29  }
$ go run fig03_03.go
catalogo (https://catalogo.example.com:443) — seguro: true
local (http://localhost:8080) — seguro: false

Ред 14: func (s Servicio) Etiqueta() string. Това (s Servicio) между func и името се нарича получател (receiver) и именно то превръща функцията в метод. Чете се: „тази функция принадлежи на типа Servicio и вътре ще наричам стойността s“.

3.3 Указатели за десет минути

Указателите имат славата на трудни. В Go те са много по-прости, отколкото в C, и трябва да разбереш само една идея за онова, което предстои.

3.3.1 Проблемът

Фиг. 3.4 | Метод, който не работи така, както би очаквал.

 1  // fig03_04.go
 2  // Muestra por que un receptor de VALOR no puede modificar el original.
 3  package main
 4
 5  import "fmt"
 6
 7  type Servicio struct {
 8      Nombre string
 9      Puerto int
10  }
11
12  // CambiarPuerto intenta modificar el servicio... y no lo logra.
13  func (s Servicio) CambiarPuerto(nuevo int) {
14      s.Puerto = nuevo
15  }
16
17  func main() {
18      x := Servicio{Nombre: "catalogo", Puerto: 443}
19      fmt.Println("antes:", x.Puerto)
20
21      x.CambiarPuerto(8443)
22      fmt.Println("despues:", x.Puerto)      // ¿cambió?
23  }
24
$ go run fig03_04.go
antes: 443
despues: 443

Нищо не се промени. Методът се изпълни, нямаше грешка, а стойността си е същата.

Защо? Защото когато получателят е (s Servicio), Go дава на метода копие на struct-а. Методът променя копието, копието се изхвърля накрая, а оригиналът така и не разбира. Това е толкова често срещан антипатърн (antipattern), че има собствен запис в „Какво се прави погрешно“ по-долу.

3.3.2 Решението: получателят по указател

Указателят е променлива, която пази адреса на друга, вместо копие на съдържанието ѝ.

Помисли за разликата между това да ти дадат фотокопие на документ и да ти дадат адреса на шкафа, където е оригиналът. Върху фотокопието можеш да пишеш колкото искаш: оригиналът не се променя. С адреса можеш да отидеш и да промениш оригинала.

  • Servicio е фотокопието.
  • *Servicio е адресът на оригинала.

Фиг. 3.5 | Същият метод, сега с получател по указател.

 1  // fig03_05.go
 2  // Con receptor de PUNTERO, el metodo si modifica el original.
 3  package main
 4
 5  import "fmt"
 6
 7  type Servicio struct {
 8      Nombre string
 9      Puerto int
10  }
11
12  // CambiarPuerto modifica el servicio. El * es la diferencia.
13  func (s *Servicio) CambiarPuerto(nuevo int) {
14      s.Puerto = nuevo
15  }
16
17  func main() {
18      x := Servicio{Nombre: "catalogo", Puerto: 443}
19      fmt.Println("antes:", x.Puerto)
20
21      x.CambiarPuerto(8443)
22      fmt.Println("despues:", x.Puerto)
23  }
$ go run fig03_05.go
antes: 443
despues: 8443

Единствената разлика между Фиг. 3.4 и 3.5 е една звездичка на ред 13. Това е всичко.

И обърни внимание на ред 21: написа x.CambiarPuerto(8443) както преди, без & или нещо странно. Go разбира, че методът има нужда от указател, и го взема сам. Това е причината указателите в Go да създават много по-малко работа от тези в C.

3.3.3 Единственият опасен указател: nil

Има и трета ситуация с указатели и тя вече създава истинско съобщение за грешка — толкова истинско, че има собствен раздел: „Грешката, която ще видиш“, по-надолу в този урок. Ето идеята предварително: нулевата стойност на указател е nil („не соча към нищо“), а четенето на поле през нулев указател кара програмата да гръмне, вместо да се държи тихо погрешно като във Фиг. 3.4.

3.4 Грешките са стойности

Вече видя в урок 2, че функция на Go може да върне (resultado, error). Сега отиваме в дълбочина, защото точно това най-много отличава Go от онова, което вероятно си виждал.

3.4.1 error е интерфейс, а не нещо магическо

В Go error е просто тип с един метод:

type error interface {
    Error() string
}

Това означава: „всяко нещо, което има метод Error(), връщащ текст, е грешка“. Няма йерархия от класове изключения, няма throw, няма стек от извиквания, който да се развива. Грешката е обикновена стойност, която пътува като всяка друга.

3.4.2 Създаване на грешки

Фиг. 3.7 | Трите начина да създадеш грешка.

 1  // fig03_07.go
 2  // Muestra las tres formas de producir un error.
 3  package main
 4
 5  import (
 6      "errors"
 7      "fmt"
 8  )
 9
10  // ErrPuertoInvalido es un error CENTINELA: se declara una vez y se compara.
11  var ErrPuertoInvalido = errors.New("el puerto debe estar entre 1 y 65535")
12
13  type Servicio struct {
14      Nombre string
15      Puerto int
16  }
17
18  // Validar revisa que el servicio tenga sentido.
19  func Validar(s Servicio) error {
20      if s.Nombre == "" {
21          // 1. un error sencillo, creado al momento
22          return errors.New("el nombre no puede estar vacio")
23      }
24      if s.Puerto < 1 || s.Puerto > 65535 {
25          // 2. un error centinela, para poder compararlo despues
26          return ErrPuertoInvalido
27      }
28      if s.Puerto == 80 {
29          // 3. un error con datos dentro
30          return fmt.Errorf("el puerto %d no usa cifrado, usa 443", s.Puerto)
31      }
32      return nil          // nil significa: todo bien
33  }
34
35  func main() {
36      casos := []Servicio{
37          {Nombre: "catalogo", Puerto: 443},
38          {Nombre: "", Puerto: 443},
39          {Nombre: "pagos", Puerto: 99999},
40          {Nombre: "viejo", Puerto: 80},
41      }
42
43      for _, s := range casos {
44          if err := Validar(s); err != nil {
45              fmt.Printf("%-10s ❌ %v\n", s.Nombre, err)
46          } else {
47              fmt.Printf("%-10s ✅ valido\n", s.Nombre)
48          }
49      }
50  }
$ go run fig03_07.go
catalogo   ✅ valido
           ❌ el nombre no puede estar vacio
pagos      ❌ el puerto debe estar entre 1 y 65535
viejo      ❌ el puerto 80 no usa cifrado, usa 443

Ред 11: сигналният (sentinel) error. Декларира се веднъж, на ниво пакет, с префикса Err. Служи на онзи, който извиква твоята функция, да може да пита „тази конкретна грешка ли беше?“, както ще видиш в 3.5.

Ред 30: fmt.Errorf. Като Printf, но създава грешка, вместо да отпечатва. Използвай го, когато съобщението има нужда от данни.

Ред 44: if err := Validar(s); err != nil. Този шаблон декларира err вътре в if, така че тя съществува само там. Много е идиоматичен в Go и поддържа кода чист.

3.5 Обвиване на грешки: %w, errors.Is и errors.As

Грешка без контекст е малко полезна. Ако програмата ти казва connection refused, не знаеш коя услуга се е провалила.

Да обвиеш грешка означава да ѝ добавиш контекст, без да загубиш оригинала.

Фиг. 3.8 | Обвиване на грешки и питане за тях.

 1  // fig03_08.go
 2  // Envuelve un error para agregar contexto sin perder el original.
 3  package main
 4
 5  import (
 6      "errors"
 7      "fmt"
 8  )
 9
10  var ErrNoResponde = errors.New("no responde")
11
12  type Servicio struct{ Nombre string }
13
14  // consultar simula la consulta de bajo nivel.
15  func consultar(s Servicio) error {
16      return ErrNoResponde
17  }
18
19  // Revisar agrega contexto al error de consultar.
20  func Revisar(s Servicio) error {
21      if err := consultar(s); err != nil {
22          // el %w ENVUELVE el error original
23          return fmt.Errorf("revisando %s: %w", s.Nombre, err)
24      }
25      return nil
26  }
27
28  func main() {
29      err := Revisar(Servicio{Nombre: "catalogo"})
30
31      fmt.Println("1. el mensaje completo:")
32      fmt.Println("  ", err)
33
34      fmt.Println("2. ¿es un ErrNoResponde, aunque este envuelto?")
35      fmt.Println("  ", errors.Is(err, ErrNoResponde))
36
37      fmt.Println("3. ¿y si pregunto por otro error?")
38      fmt.Println("  ", errors.Is(err, errors.New("otra cosa")))
39
40      fmt.Println("4. el error original, desenvuelto:")
41      fmt.Println("  ", errors.Unwrap(err))
42  }
$ go run fig03_08.go
1. el mensaje completo:
   revisando catalogo: no responde
2. ¿es un ErrNoResponde, aunque este envuelto?
   true
3. ¿y si pregunto por otro error?
   false
4. el error original, desenvuelto:
   no responde

Ред 23: %w. Това е ключовият глагол. При отпечатване изглежда също като %v, но запазва оригиналната грешка вътре, за да може после да се пита за нея.

Ред 35: errors.Is. Пита „в някоя точка на тази верига има ли я тази грешка?“. Работи дори да има пет слоя обвиване.

3.5.1 errors.As, когато ти трябват данните на грешката

errors.Is отговаря на „тази грешка ли е?“. errors.As отговаря на „от този тип ли е? дай ми я, за да прочета полетата ѝ“.

Фиг. 3.9 | Собствена грешка с данни, извлечена с errors.As.

 1  // fig03_09.go
 2  // Define un tipo de error propio y recupera sus datos.
 3  package main
 4
 5  import (
 6      "errors"
 7      "fmt"
 8  )
 9
10  // ErrorHTTP es un error que lleva datos adentro.
11  type ErrorHTTP struct {
12      Codigo int
13      URL    string
14  }
15
16  // Error hace que ErrorHTTP cumpla la interfaz error.
17  func (e *ErrorHTTP) Error() string {
18      return fmt.Sprintf("el servidor respondio %d", e.Codigo)
19  }
20
21  func consultar(url string) error {
22      return &ErrorHTTP{Codigo: 503, URL: url}
23  }
24
25  func Revisar(nombre, url string) error {
26      if err := consultar(url); err != nil {
27          return fmt.Errorf("revisando %s: %w", nombre, err)
28      }
29      return nil
30  }
31
32  func main() {
33      err := Revisar("catalogo", "https://catalogo.example.com")
34      fmt.Println("mensaje:", err)
35
36      var errHTTP *ErrorHTTP
37      if errors.As(err, &errHTTP) {
38          fmt.Println("es un ErrorHTTP")
39          fmt.Println("  codigo:", errHTTP.Codigo)
40          fmt.Println("  url:   ", errHTTP.URL)
41
42          if errHTTP.Codigo >= 500 {
43              fmt.Println("  → es culpa del servidor, conviene reintentar")
44          }
45      }
46  }
$ go run fig03_09.go
mensaje: revisando catalogo: el servidor respondio 503
es un ErrorHTTP
  codigo: 503
  url:    https://catalogo.example.com
  → es culpa del servidor, conviene reintentar

Ред 17. Като напишеш метод Error() string, твоят тип вече е error. Не си декларирал нищо: типът изпълнява интерфейса, защото има метода. Точно това обяснява следващият раздел.

Ред 42. И ето истинската стойност: програмата може да реши според вида на провала. 503 се опитва отново; 404 — не. С грешки само от текст това би било невъзможно без сравняване на низове, което е крехко.

3.6 Интерфейси: сърцето на Go

Интерфейсът е списък от методи. Всеки тип, който има тези методи, го изпълнява и не е нужно това да се декларира никъде.

Фиг. 3.10 | Един интерфейс и две реализации.

 1  // fig03_10.go
 2  // Una interfaz con dos implementaciones: la real y una de prueba.
 3  package main
 4
 5  import (
 6      "errors"
 7      "fmt"
 8  )
 9
10  type Servicio struct {
11      Nombre string
12      URL    string
13  }
14
15  type Estado struct {
16      Servicio Servicio
17      Codigo   int
18      Err      error
19  }
20
21  // Revisor es una INTERFAZ: cualquier cosa con este metodo la cumple.
22  type Revisor interface {
23      Revisar(s Servicio) Estado
24  }
25
26  // ---- primera implementacion: la de verdad (simplificada) ----
27  type RevisorHTTP struct{}
28
29  func (r RevisorHTTP) Revisar(s Servicio) Estado {
30      // En la leccion 7 esto hara una peticion HTTP real.
31      return Estado{Servicio: s, Codigo: 200}
32  }
33
34  // ---- segunda implementacion: para probar, sin red ----
35  type RevisorFalso struct {
36      Respuesta Estado
37  }
38
39  func (r RevisorFalso) Revisar(s Servicio) Estado {
40      r.Respuesta.Servicio = s
41      return r.Respuesta
42  }
43
44  // RevisarTodos acepta CUALQUIER Revisor. No sabe ni le importa cual.
45  func RevisarTodos(r Revisor, servicios []Servicio) []Estado {
46      var estados []Estado
47      for _, s := range servicios {
48          estados = append(estados, r.Revisar(s))
49      }
50      return estados
51  }
52
53  func main() {
54      servicios := []Servicio{
55          {Nombre: "catalogo", URL: "https://catalogo.example.com"},
56          {Nombre: "pagos", URL: "https://pagos.example.com"},
57      }
58
59      fmt.Println("--- con el revisor real ---")
60      for _, e := range RevisarTodos(RevisorHTTP{}, servicios) {
61          fmt.Printf("  %-10s codigo %d\n", e.Servicio.Nombre, e.Codigo)
62      }
63
64      fmt.Println("--- con el falso, simulando una falla ---")
65      falso := RevisorFalso{
66          Respuesta: Estado{Err: errors.New("no responde")},
67      }
68      for _, e := range RevisarTodos(falso, servicios) {
69          fmt.Printf("  %-10s error: %v\n", e.Servicio.Nombre, e.Err)
70      }
71  }
$ go run fig03_10.go
--- con el revisor real ---
  catalogo   codigo 200
  pagos      codigo 200
--- con el falso, simulando una falla ---
  catalogo   error: no responde
  pagos      error: no responde

Прочети го бавно, защото тук е идеята, която крепи целия Go.

Ред 22-24: интерфейсът. Казва: „Revisor е всяко нещо, което има метод Revisar, който получава Servicio и връща Estado“.

Редове 29 и 39. RevisorHTTP и RevisorFalso не декларират никъде, че изпълняват Revisor. Няма implements, няма : Revisor, нищо. Изпълняват интерфейса, защото имат метода, и това го проверява компилаторът сам.

Ред 45: func RevisarTodos(r Revisor, ...). Тази функция не знае с какво работи. Знае само, че може да извика .Revisar(). И затова редове 60 и 68 ѝ подават две напълно различни неща, без да се промени нито един ред от RevisarTodos.

3.7 Защо това прави кода ти тестваем

Погледни пак ред 65 от Фиг. 3.10. Току-що тества RevisarTodos, симулирайки паднала услуга, без да изключваш нищо, без мрежа и за една милисекунда.

Това имат предвид хората, когато говорят за „тестваем код“, и в Go то се постига без библиотеки за mocks, без анотации и без frameworks. Само с един малък интерфейс.


Грешката, която ще видиш

Най-честата грешка в този урок е nil pointer dereference и за разлика от антипатърна от Фиг. 3.4 (който се проваля тихо), тази те предупреждава — със съобщение, което в началото плаши повече, отколкото би трябвало.

Фиг. 3.6 | Програма, която се компилира перфектно и гърми при изпълнение.

 1  // fig03_06.go — este programa COMPILA pero truena al correr
 2  package main
 3
 4  import "fmt"
 5
 6  type Servicio struct{ Puerto int }
 7
 8  func main() {
 9      var p *Servicio          // un puntero sin apuntar a nada: vale nil
10      fmt.Println(p)           // esto sí funciona
11      fmt.Println(p.Puerto)    // esto truena
12  }
$ go run fig03_06.go
<nil>
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x...]

goroutine 1 [running]:
main.main()
	/home/usuario/fig03_06.go:11 +0x18
exit status 2

Нулевата стойност на указател е nil: „не соча към нищо“. Опитът да се прочете поле през нулев указател предизвиква panic, което е начинът, по който Go прекратява програма, когато се случи нещо непоправимо.

Как да четеш това съобщение, ред по ред:

  • panic: runtime error: invalid memory address or nil pointer dereference — името на проблема. Когато го видиш, причината почти винаги е указател, map или интерфейс в nil, който си опитал да използваш, сякаш има нещо вътре.
  • goroutine 1 [running]: — коя нишка на изпълнение на Go е работела, когато е гръмнало. Засега, в програма без конкурентност, винаги ще е 1 (функцията main); в урок 6, с няколко goroutines, това число започва наистина да има значение.
  • main.main() … fig03_06.go:11 — точният файл и ред, където се е случило. Винаги започвай оттам, а не от съобщението отгоре: съобщението ти казва какъв вид грешка е било, този ред ти казва къде.
  • exit status 2 — програмата е приключила с грешка (exit status 0 е успех).

Какво се прави погрешно

1. Писане на метод, който би трябвало да променя struct-а, с получател по стойност. Това е грешката от Фиг. 3.4 (раздел 3.3.1): компилира се, изпълнява се и не прави абсолютно нищо — без никакво съобщение. Това е едно от най-объркващите спъвания за начинаещия, точно защото няма никакъв симптом. Правилото, което го предотвратява, е в Добра практика 3.2: ако методът променя — получател по указател.

2. Обвиване на грешка с %v вместо %w. Това е грешката от раздел 3.5: отпечатаното съобщение остава идентично, така че нищо не предупреждава, но errors.Is престава да разпознава оригиналната грешка и кодът, който решаваше според вида на провала, започва да поема по грешния път. От двата антипатърна в този урок този е най-скъпият, защото никой не забелязва симптома, докато програмата вече не е взела лошо решение заради него.

3. Писане на големи интерфейси, с много методи, копирайки навика от друг език. В Go интерфейсите се правят малки — един или два метода, както видя в Добра практика 3.4 — и се дефинират от страната на онзи, който ги консумира, а не на онзи, който ги реализира. Интерфейс с десет метода почти никога не е необходим: обикновено стига методът или методите, които функцията, която го получава, наистина извиква.


Упражнения

Упражнения за преговор

3.1 Какво предимство има един struct пред три отделни променливи?

3.2 Какво означава едно поле да започва с главна буква?

3.3 Какво отпечатва fmt.Printf("%+v\n", Servicio{}), ако struct-ът има Nombre string и Puerto int?

3.4 Каква е разликата между func (s Servicio) X() и func (s *Servicio) X() и кога се използва всеки от тях?

3.5 Този метод се компилира, изпълнява се и не прави нищо. Защо?

func (s Servicio) Renombrar(nuevo string) {
    s.Nombre = nuevo
}

3.6 Какво е nil за един указател и какво става, ако прочетеш поле през такъв?

3.7 Какво трябва да има един тип, за да бъде error?

3.8 Каква е разликата между %w и %v при обвиване на грешка и защо е опасна?

3.9 Кога използваш errors.Is и кога errors.As?

3.10 Какво трябва да се напише, за да изпълнява един тип интерфейс в Go?

3.11 Какво отпечатва тази програма?

type Estado struct{ Codigo int }

func (e Estado) OK() bool { return e.Codigo == 200 }

func main() {
    var e Estado
    fmt.Println(e.OK())
}

Упражнения с код

3.12 Добави към struct-а Servicio поле TimeoutMs int и метод Timeout() time.Duration, който да го преобразува. Подсказка: time.Duration(s.TimeoutMs) * time.Millisecond.

3.13 Напиши метод func (s *Servicio) Normalizar(), който: маха интервалите от името със strings.TrimSpace, превръща го в малки букви със strings.ToLower и, ако портът е 0, го прави 443. Обясни си защо този метод има нужда от получател по указател.

3.14 Дефинирай тип Estado с полетата Servicio, Codigo, Duracion и Err. Напиши му метод OK() bool, който връща истина само ако няма грешка и кодът е между 200 и 299. Изпробвай го с пет случая, включително празния Estado{}.

3.15 Създай сигнален error ErrTimeout и функция, която го връща обвит с контекст. Провери с errors.Is, че се открива. После смени %w с %v и провери, че errors.Is връща false. Запиши в дневника, че отпечатаното съобщение не се е променило.

3.16 Дефинирай собствена грешка ErrorValidacion с полетата Campo string и Motivo string, накарай я да изпълнява интерфейса error и я извлечи с errors.As, за да отпечаташ кое поле се е провалило.

3.17 Дефинирай интерфейса Notificador с метод Notificar(mensaje string) error. Реализирай NotificadorConsola (който отпечатва) и NotificadorFalso (който пази съобщенията в slice, за да можеш да ги прегледаш). Напиши функция, която получава Notificador, и я използвай с двата.

3.18 (Намери грешката) Кажи какво не е наред във всеки от тях, без да компилираш, а после компилирай, за да потвърдиш:

// (a)
func (s Servicio) Renombrar(n string) { s.Nombre = n }

// (b)
return fmt.Errorf("fallo al revisar %s: %v", nombre, err)

// (c)
var p *Servicio
fmt.Println(p.Nombre)

// (d)
type Revisor interface {
    Revisar(s Servicio) Estado
}
type MiRevisor struct{}
func (m MiRevisor) revisar(s Servicio) Estado { return Estado{} }
// y luego:  var r Revisor = MiRevisor{}

3.19 (Проект на курса) Преустрой програмата си от урок 2, като използваш наученото в този урок:

  1. Structs Servicio и Estado.
  2. Методите Etiqueta(), EsSeguro() и OK().
  3. Интерфейса Revisor с RevisorHTTP (който засега връща измислени данни) и RevisorFalso.
  4. Функцията RevisarTodos(r Revisor, servicios []Servicio) []Estado.
  5. main, който отпечатва отчета, като използва фалшивия, с една услуга, която се проваля.

🔑 Когато приключиш, забележи нещо: програмата ти вече може да се тества изцяло, без да се свързва с нищо, а още не си написал нито един тест. Това е, което току-що спечели в този урок.

Решения

Упражнения за преговор (3.1 до 3.11):

3.1 Групира данните, които вървят заедно, в една-единствена променлива, така че езикът — и онзи, който чете кода — знае, че те принадлежат на едно и също нещо. И не могат по грешка да се съчетаят данни от две различни услуги.

3.2 Че е експортирано: видимо е от други пакети. С малка буква се вижда само в собствения си пакет. Това е целият контрол на достъпа, който има Go.

3.3 {Nombre: Puerto:0} — нулевите стойности, с имената на полетата, защото е %+v.

3.4 Първият е получател по стойност: получава копие и не може да промени оригинала. Вторият е получател по указател: получава адреса и може. Указател се използва, когато методът променя, или когато struct-ът е голям и копирането му струва скъпо.

3.5 Защото получателят е по стойност: методът променя копие, което се изхвърля накрая. Трябва да се смени на func (s *Servicio) Renombrar(...).

3.6 nil е нулевата стойност на указател и означава „не соча към нищо“. Четенето на поле през указател nil предизвиква panic (nil pointer dereference) и програмата прекратява работа.

3.7 Метод Error() string. Нищо повече: error е интерфейс с този единствен метод.

3.8 %w обвива оригиналната грешка и я запазва вътре; %v само я превръща в текст. Опасна е, защото отпечатаното съобщение е идентично, така че няма симптом — но errors.Is престава да намира оригиналната грешка и кодът, който решаваше според вида на провала, започва да греши.

3.9 errors.Is, за да питаш „тази конкретна грешка ли е?“. errors.As, за да питаш „от този тип ли е? дай ми я“, когато трябва да прочетеш данните, които грешката носи вътре.

3.10 Нищо. Достатъчно е да има методите, които интерфейсът изисква; компилаторът го проверява сам. Не съществува implements.

3.11 false — нулевата стойност на Estado има Codigo: 0, което не е 200. Забележи, че методът работи перфектно върху празен struct: това е нулевата стойност, която е полезна.

Упражнения с код (3.12 до 3.19): проверените им решения — компилирани и изпълнени — ще бъдат добавени във версията, в която се затвори главата с решени упражнения за целия курс; не се публикуват, без да е пуснат go build върху всяко от тях.


Как разбирам, че съм успял

  • Компилира и пусна фигури 3.1 до 3.10 и изходът ти съвпада с показания.
  • Можеш да обясниш, без да гледаш текста, каква е разликата между получател по стойност и получател по указател, и да дадеш пример кога се използва всеки от тях.
  • Направи експеримента от упражнение 3.15 (смяна на %w с %v) и видя със собствените си очи, че errors.Is престава да намира грешката, без отпечатаното съобщение да се промени.
  • Можеш да обясниш на някой друг, без технически термини, защо в Go „изпълнението на интерфейс“ не се декларира никъде.
  • Завърши упражнение 3.19: програмата ти revisor вече използва structs, методи, интерфейса Revisor и може да се пусне с RevisorFalso, без да се докосва мрежата.

Преди да затвориш: в bitacora.md запиши кое ти беше най-трудно между указатели, грешки и интерфейси, и резултата от упражнение 3.15 — онова с %w срещу %v. Този експеримент е онзи, който най-често се забравя, и онзи, който излиза най-скъпо в истинска програма.


Резюме

  • Struct групира свързани данни в собствен тип. Дефинира се с type Nombre struct { … }.
  • Полетата с главна буква са експортирани; с малка — частни за пакета.
  • Нулевата стойност на struct запълва всяко поле със собствената му нулева стойност: никога няма боклук.
  • %+v отпечатва struct с имената на полетата му: най-добрият инструмент за отстраняване на грешки.
  • Методът е функция с получател (receiver): func (s Servicio) X().
  • Получателят по стойност получава копие и не може да промени оригинала; получателят по указател (*T) може.
  • Указателят пази адреса на друга променлива. Go вмъква & и * вместо теб, когато извикваш методи.
  • Нулевата стойност на указател е nil; четенето през него предизвиква panic.
  • error е интерфейс с един-единствен метод Error() string. Грешката е обикновена стойност.
  • Създават се с errors.New (прости), като сигнални (var ErrX = errors.New(...)) или с fmt.Errorf (с данни).
  • %w обвива грешка, като запазва оригинала; %v я сплесква и чупи errors.Is, без да предупреди.
  • errors.Is пита дали една грешка е във веригата; errors.As извлича грешката от конкретен тип, за да прочете данните ѝ.
  • Интерфейсът е списък от методи. Един тип го изпълнява, защото има методите, без да го декларира.
  • Интерфейсите в Go се правят малки и се дефинират там, където се използват.
  • Малкият интерфейс позволява да се тества без външния свят: истинската реализация се заменя с фалшива.

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

  1. A Tour of Go — Methods — официална и интерактивна: получатели, указатели и интерфейси с упражнения в браузъра.
  2. Effective Go — Interfaces and other types — официалното обяснение защо удовлетворяването на интерфейси е имплицитно.
  3. Error handling and Go — публикацията в официалния блог на Go за това защо грешките са стойности и как се обработват идиоматично.
  4. Пакетът errors — официална документация — точният справочник за errors.Is, errors.As и errors.Unwrap.

Термини от този урок

поле всяка от данните, които съдържа един struct
обвиване (на грешка) добавяне на контекст със запазване на оригинала, с %w
сигнален (sentinel) error грешка, декларирана веднъж на ниво пакет, за сравняване с errors.Is
експортиран / неекспортиран видим извън пакета (главна буква) или не (малка буква)
интерфейс списък от методи; един тип го изпълнява, като има тези методи
метод функция, свързана с тип чрез получател
nil липса на стойност; нулевата стойност на указатели, интерфейси и грешки
nil pointer dereference panic заради четене през нулев указател
panic прекратяване на програмата заради непоправима грешка
указател променлива, която пази адреса на друга; типът ѝ се пише *T
получател (receiver) (s Servicio), което свързва функция с тип
имплицитно удовлетворяване изпълняване на интерфейс, без да се декларира
struct тип, който групира няколко полета
Да обсъдим вашия проект

Предпочитате имейл? Пишете ни на hola@habil.mx