Índice del curso
Lección 3 — Structs, métodos, errores e interfaces
Aquí Go se vuelve Go. Ésta es la lección más importante del curso.
Duración: dos sesiones de 60 minutos. No la hagas de corrido.
Al terminar vas a poder:
- Agrupar datos relacionados en un struct y explicar por qué es mejor que variables sueltas.
- Escribir métodos y decidir cuándo usar receptor de valor y cuándo de puntero.
- Explicar qué es un puntero sin asustarte.
- Manejar errores como valores, envolverlos y preguntar por ellos.
- Definir una interfaz y entender por qué en Go no se declara que se cumple.
- Escribir código que se puede probar sin conectarse a nada.
Por qué importa
En la lección 2 guardaste los datos de un servicio en tres variables separadas (nombre, url, puerto).
Funciona con un servicio. Con diez son treinta variables sueltas, y nada en el código dice cuáles van
juntas — si te equivocas y combinas el nombre de uno con el puerto de otro, el programa compila igual y
produce un reporte incorrecto. Ese es el primer problema que resuelve esta lección: el struct.
El segundo problema es más de fondo. El revisor va a tener que consultar servicios de verdad, y las
consultas de verdad fallan: se cae la red, el servicio tarda, responde con un código que no esperabas.
Un programa que no sabe manejar eso con orden no sirve para nada en producción. Go resuelve esto de una
forma que probablemente no hayas visto si vienes de otro lenguaje: los errores son valores, no
excepciones que interrumpen el flujo.
Y el tercer problema es el que hace que las dos piezas anteriores encajen entre sí sin que el revisor
tenga que saber, de antemano, cómo se va a revisar cada servicio (HTTP hoy, quizá una base de datos
mañana). Eso lo resuelve la interfaz, que es también lo que te va a permitir, en la lección 5, probar
todo el programa sin conectarte a nada real.
Structs, errores e interfaces son, en ese orden, la columna vertebral de todo lo que sigue en el curso.
Los conceptos
3.1 Structs: agrupar lo que va junto
Lo que necesitas es decirle al lenguaje: «estas tres cosas son un servicio».
Fig. 3.1 | Definir y usar un 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}
Línea 8: type Servicio struct {. Se lee: «define un tipo nuevo llamado Servicio, que es una
estructura». Acabas de crear un tipo que no venía con el lenguaje, y desde ahora se usa igual que
int o string.
Líneas 15-19. Crea un valor de ese tipo. Los nombres de campo con dos puntos son opcionales —podrías escribir solo los valores en orden— pero ponlos siempre:
Línea 21: s.Nombre. El punto accede a un campo. Se lee «el Nombre de s».
3.1.1 La mayúscula es permiso, no estética
Fíjate que los campos empiezan con mayúscula: Nombre, URL, Puerto. En Go eso no es un estilo
elegido: es el control de acceso del lenguaje.
Nombre (mayúscula) |
exportado: visible desde otros paquetes |
nombre (minúscula) |
no exportado: solo visible dentro de su propio paquete |
No hay public, private ni protected. La letra inicial es la regla completa.
3.1.2 El valor cero de un struct
Fig. 3.2 | Un struct sin inicializar.
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
Cada campo recibe su valor cero: los textos quedan en "" y los números en 0. No hay basura ni
«indefinido», y por eso un struct recién creado ya se puede usar sin miedo.
Y aquí aparece %+v, que es muy útil para depurar: imprime el struct con los nombres de los
campos. Compáralo con %v, que imprime solo los valores.
3.2 Métodos
Un método es una función que pertenece a un tipo. En vez de escribir etiqueta(s), escribes
s.Etiqueta(), y la función queda ligada al tipo al que corresponde.
Fig. 3.3 | Métodos sobre un 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
Línea 14: func (s Servicio) Etiqueta() string. Ese (s Servicio) entre func y el nombre se llama
receptor, y es lo que convierte una función en un método. Se lee: «esta función pertenece al tipo
Servicio, y dentro me voy a referir al valor como s».
3.3 Punteros, en diez minutos
Los punteros tienen fama de difíciles. En Go son mucho más simples que en C, y necesitas entender una sola idea para lo que viene.
3.3.1 El problema
Fig. 3.4 | Un método que no funciona como esperarías.
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
No cambió nada. El método se ejecutó, no hubo error, y el valor sigue igual.
¿Por qué? Porque cuando el receptor es (s Servicio), Go le entrega al método una copia del
struct. El método modifica la copia, la copia se descarta al terminar, y el original nunca se enteró. Es
un antipatrón tan frecuente que tiene su propia entrada en «Lo que se hace mal», más abajo.
3.3.2 La solución: el receptor de puntero
Un puntero es una variable que guarda la dirección de otra, en vez de una copia de su contenido.
Piensa en la diferencia entre darte una fotocopia de un documento y darte la dirección del archivero donde está el original. Con la fotocopia puedes escribir todo lo que quieras: el original no cambia. Con la dirección, puedes ir y modificar el original.
Servicioes la fotocopia.*Servicioes la dirección del original.
Fig. 3.5 | El mismo método, ahora con receptor de puntero.
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
La única diferencia entre la Fig. 3.4 y la 3.5 es un asterisco en la línea 13. Eso es todo.
Y fíjate en la línea 21: escribiste x.CambiarPuerto(8443) igual que antes, sin & ni nada raro. Go
se da cuenta de que el método necesita un puntero y lo toma solo. Ésa es la razón por la que los punteros
de Go dan mucho menos trabajo que los de C.
3.3.3 El único puntero peligroso: nil
Todavía hay una tercera situación con punteros, y ésta sí produce un mensaje de error real —tan real que
tiene su propia sección dedicada: «El error que vas a ver», más abajo en esta lección. Adelantamos la
idea: el valor cero de un puntero es nil («no apunto a nada»), y leer un campo a través de un
puntero nulo hace que el programa truene en vez de comportarse mal en silencio como en la Fig. 3.4.
3.4 Los errores son valores
Ya viste en la lección 2 que una función de Go puede devolver (resultado, error). Ahora vamos al fondo,
porque esto es lo que más distingue a Go de lo que probablemente hayas visto.
3.4.1 error es una interfaz, no algo mágico
En Go, error es simplemente un tipo con un método:
type error interface {
Error() string
}
Eso significa: «cualquier cosa que tenga un método Error() que devuelva texto, es un error». No hay
jerarquía de clases de excepción, no hay throw, no hay pila de llamadas que se desenrolla. Un error es
un valor común que viaja como cualquier otro.
3.4.2 Crear errores
Fig. 3.7 | Las tres formas de crear un error.
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
Línea 11: el error centinela. Se declara una vez, a nivel de paquete, con el prefijo Err. Sirve
para que quien llame a tu función pueda preguntar «¿fue este error en particular?», como verás en 3.5.
Línea 30: fmt.Errorf. Como Printf, pero produce un error en vez de imprimir. Úsalo cuando el
mensaje necesita datos.
Línea 44: if err := Validar(s); err != nil. Este patrón declara err dentro del if, así que
solo existe ahí. Es muy idiomático en Go y mantiene el código limpio.
3.5 Envolver errores: %w, errors.Is y errors.As
Un error sin contexto es poco útil. Si tu programa dice connection refused, no sabes qué servicio
falló.
Envolver un error es agregarle contexto sin perder el original.
Fig. 3.8 | Envolver errores y preguntar por ellos.
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
Línea 23: %w. Éste es el verbo clave. Se ve igual que %v al imprimir, pero conserva el error
original adentro para que se pueda preguntar por él después.
Línea 35: errors.Is. Pregunta «¿en algún punto de esta cadena está ese error?». Funciona aunque
haya cinco capas de envoltura.
3.5.1 errors.As, cuando necesitas los datos del error
errors.Is responde «¿es este error?». errors.As responde «¿es de este tipo? dámelo para leer sus
campos».
Fig. 3.9 | Un error propio con datos, recuperado con 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
Línea 17. Al escribir un método Error() string, tu tipo ya es un error. No declaraste nada: el
tipo cumple la interfaz porque tiene el método. Eso es lo que explica la sección siguiente.
Línea 42. Y aquí está el valor real: el programa puede decidir según el tipo de fallo. Un 503 se reintenta; un 404, no. Con errores de solo texto eso sería imposible sin comparar cadenas, que es frágil.
3.6 Interfaces: el corazón de Go
Una interfaz es una lista de métodos. Cualquier tipo que tenga esos métodos la cumple, y no hay que declararlo en ninguna parte.
Fig. 3.10 | Una interfaz y dos implementaciones.
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
Léelo despacio, porque aquí está la idea que sostiene todo Go.
Línea 22-24: la interfaz. Dice: «un Revisor es cualquier cosa que tenga un método Revisar que
reciba un Servicio y devuelva un Estado».
Líneas 29 y 39. RevisorHTTP y RevisorFalso no declaran en ninguna parte que cumplen Revisor.
No hay implements, no hay : Revisor, nada. Cumplen la interfaz porque tienen el método, y eso lo
verifica el compilador solo.
Línea 45: func RevisarTodos(r Revisor, ...). Esta función no sabe con qué está trabajando. Solo
sabe que puede llamar .Revisar(). Y por eso las líneas 60 y 68 le pasan dos cosas completamente
distintas sin cambiar una sola línea de RevisarTodos.
3.7 Por qué esto hace tu código probable
Mira otra vez la línea 65 de la Fig. 3.10. Acabas de probar RevisarTodos simulando un servicio
caído, sin apagar nada, sin red, y en un milisegundo.
Eso es lo que la gente quiere decir cuando habla de «código probable», y en Go se consigue sin bibliotecas de mocks, sin anotaciones y sin frameworks. Solo con una interfaz pequeña.
El error que vas a ver
El error más frecuente de esta lección es el nil pointer dereference, y a diferencia del antipatrón
de la Fig. 3.4 (que falla en silencio), éste sí te avisa — con un mensaje que al principio asusta más de
lo que debería.
Fig. 3.6 | Un programa que compila perfecto y truena al ejecutarse.
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
El valor cero de un puntero es nil: «no apunto a nada». Intentar leer un campo a través de un
puntero nulo provoca un panic, que es la forma en que Go aborta un programa cuando pasa algo
irrecuperable.
Cómo leer este mensaje, línea por línea:
panic: runtime error: invalid memory address or nil pointer dereference— el nombre del problema. Cuando lo veas, la causa casi siempre es un puntero, un mapa o una interfaz ennilque intentaste usar como si tuviera algo adentro.goroutine 1 [running]:— qué hilo de ejecución de Go estaba corriendo cuando reventó. Por ahora, en un programa sin concurrencia, siempre va a ser el 1 (la funciónmain); en la lección 6, con varias goroutines, este número empieza a importar de verdad.main.main() … fig03_06.go:11— el archivo y la línea exactos donde pasó. Empieza siempre por ahí, no por el mensaje de arriba: el mensaje te dice qué tipo de error fue, esta línea te dice dónde.exit status 2— el programa terminó con error (unexit status 0es éxito).
Lo que se hace mal
1. Escribir un método que debería modificar el struct, con receptor de valor. Es el error de la Fig. 3.4 (sección 3.3.1): compila, se ejecuta, y no hace absolutamente nada — sin ningún mensaje. Es uno de los tropiezos más desconcertantes para quien empieza, precisamente porque no hay síntoma alguno. La regla que lo evita está en la Buena práctica 3.2: si el método modifica, receptor de puntero.
2. Envolver un error con %v en vez de %w. Es el error de la sección 3.5: el mensaje impreso queda
idéntico, así que nada avisa, pero errors.Is deja de reconocer el error original y el código que decidía
según el tipo de fallo empieza a tomar el camino equivocado. De los dos antipatrones de esta lección, éste
es el más caro, porque nadie nota el síntoma hasta que el programa ya tomó una mala decisión con él.
3. Escribir interfaces grandes, con muchos métodos, copiando el hábito de otro lenguaje. En Go las interfaces se hacen pequeñas —uno o dos métodos, como viste en la Buena práctica 3.4— y se definen del lado de quien las consume, no de quien las implementa. Una interfaz de diez métodos casi nunca es necesaria: normalmente basta con el o los métodos que la función que la recibe realmente llama.
Ejercicios
Ejercicios de repaso
3.1 ¿Qué ventaja tiene un struct sobre tres variables sueltas?
3.2 ¿Qué significa que un campo empiece con mayúscula?
3.3 ¿Qué imprime fmt.Printf("%+v\n", Servicio{}) si el struct tiene Nombre string y Puerto int?
3.4 ¿Cuál es la diferencia entre func (s Servicio) X() y func (s *Servicio) X(), y cuándo se usa
cada uno?
3.5 Este método compila, se ejecuta y no hace nada. ¿Por qué?
func (s Servicio) Renombrar(nuevo string) {
s.Nombre = nuevo
}
3.6 ¿Qué es nil para un puntero y qué pasa si lees un campo a través de uno?
3.7 ¿Qué tiene que tener un tipo para ser un error?
3.8 ¿Cuál es la diferencia entre %w y %v al envolver un error, y por qué es peligrosa?
3.9 ¿Cuándo usas errors.Is y cuándo errors.As?
3.10 ¿Qué hay que escribir para que un tipo cumpla una interfaz en Go?
3.11 ¿Qué imprime este programa?
type Estado struct{ Codigo int }
func (e Estado) OK() bool { return e.Codigo == 200 }
func main() {
var e Estado
fmt.Println(e.OK())
}
Ejercicios de código
3.12 Agrega al struct Servicio un campo TimeoutMs int y un método Timeout() time.Duration que lo
convierta. Pista: time.Duration(s.TimeoutMs) * time.Millisecond.
3.13 Escribe un método func (s *Servicio) Normalizar() que: quite espacios del nombre con
strings.TrimSpace, lo pase a minúsculas con strings.ToLower, y si el puerto es 0 lo ponga en 443.
Explícate por qué este método necesita receptor de puntero.
3.14 Define un tipo Estado con los campos Servicio, Codigo, Duracion y Err. Escríbele un
método OK() bool que devuelva verdadero solo si no hay error y el código está entre 200 y 299.
Pruébalo con cinco casos, incluido el Estado{} vacío.
3.15 Crea un error centinela ErrTimeout y una función que lo devuelva envuelto con contexto.
Comprueba con errors.Is que se detecta. Después cambia el %w por %v y comprueba que errors.Is
devuelve false. Anota en la bitácora que el mensaje impreso no cambió.
3.16 Define un error propio ErrorValidacion con los campos Campo string y Motivo string, hazlo
cumplir la interfaz error, y recupéralo con errors.As para imprimir qué campo falló.
3.17 Define la interfaz Notificador con un método Notificar(mensaje string) error. Implementa
NotificadorConsola (que imprime) y NotificadorFalso (que guarda los mensajes en un slice para poder
revisarlos). Escribe una función que reciba un Notificador y úsala con las dos.
3.18 (Encuentra el error) Di qué está mal en cada uno sin compilar, y luego compila para confirmar:
// (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 (Proyecto del curso) Reorganiza tu programa de la lección 2 usando lo de esta lección:
- Los structs
ServicioyEstado. - Los métodos
Etiqueta(),EsSeguro()yOK(). - La interfaz
RevisorconRevisorHTTP(que por ahora devuelve datos inventados) yRevisorFalso. - La función
RevisarTodos(r Revisor, servicios []Servicio) []Estado. - Un
mainque imprima el reporte usando el falso, con un servicio que falle.
🔑 Cuando termines, fíjate en algo: tu programa ya se puede probar completo sin conectarse a nada, y todavía no has escrito una sola prueba. Eso es lo que acabas de ganar en esta lección.
Soluciones
Ejercicios de repaso (3.1 a 3.11):
3.1 Agrupa los datos que van juntos en una sola variable, así que el lenguaje —y quien lea el código— sabe que pertenecen a la misma cosa. Y no se pueden combinar por error datos de dos servicios distintos.
3.2 Que está exportado: es visible desde otros paquetes. Con minúscula solo se ve dentro de su paquete. Es todo el control de acceso que tiene Go.
3.3 {Nombre: Puerto:0} — los valores cero, con los nombres de los campos porque es %+v.
3.4 El primero es receptor de valor: recibe una copia y no puede modificar el original. El segundo es receptor de puntero: recibe la dirección y sí puede. Se usa puntero cuando el método modifica, o cuando el struct es grande y copiarlo cuesta.
3.5 Porque el receptor es de valor: el método modifica una copia que se descarta al terminar. Hay
que cambiarlo a func (s *Servicio) Renombrar(...).
3.6 nil es el valor cero de un puntero y significa «no apunto a nada». Leer un campo a través de un
puntero nil provoca un panic (nil pointer dereference) y el programa aborta.
3.7 Un método Error() string. Nada más: error es una interfaz con ese único método.
3.8 %w envuelve el error original y lo conserva dentro; %v solo lo convierte a texto. Es
peligrosa porque el mensaje impreso es idéntico, así que no hay síntoma — pero errors.Is deja de
encontrar el error original y el código que decidía según el tipo de fallo empieza a equivocarse.
3.9 errors.Is para preguntar «¿es este error en particular?». errors.As para preguntar «¿es de
este tipo? dámelo», cuando necesitas leer los datos que el error lleva adentro.
3.10 Nada. Basta con tener los métodos que la interfaz pide; el compilador lo verifica solo. No
existe implements.
3.11 false — el valor cero de Estado tiene Codigo: 0, que no es 200. Nota que el método funciona
perfectamente sobre un struct vacío: eso es el valor cero siendo útil.
Ejercicios de código (3.12 a 3.19): sus soluciones verificadas —compiladas y ejecutadas— se agregan en
la entrega en la que se cierre el capítulo de ejercicios con solución del curso completo; no se publican
sin haber corrido go build sobre cada una.
Cómo sé que lo logré
- Compilaste y corriste las figuras 3.1 a 3.10 y tu salida coincide con la mostrada.
- Puedes explicar, sin ver el texto, qué diferencia hay entre receptor de valor y receptor de puntero, y dar un ejemplo de cuándo usar cada uno.
- Hiciste el experimento del ejercicio 3.15 (cambiar
%wpor%v) y viste con tus propios ojos queerrors.Isdeja de encontrar el error sin que el mensaje impreso cambie. - Puedes explicarle a alguien más, sin tecnicismos, por qué en Go «cumplir una interfaz» no se declara en ningún lado.
- Terminaste el ejercicio 3.19: tu programa del
revisorya usa structs, métodos, la interfazRevisory se puede correr con elRevisorFalsosin tocar la red.
Antes de cerrar: en bitacora.md anota qué te costó más entre punteros, errores e
interfaces, y el resultado del ejercicio 3.15 —el del %w contra %v—. Ese experimento es el que más se
olvida y el que más caro sale en un programa real.
Resumen
- Un struct agrupa datos relacionados en un tipo propio. Se define con
type Nombre struct { … }. - Los campos con mayúscula son exportados; con minúscula, privados al paquete.
- El valor cero de un struct llena cada campo con su propio valor cero: nunca hay basura.
%+vimprime un struct con los nombres de sus campos: la mejor herramienta para depurar.- Un método es una función con receptor:
func (s Servicio) X(). - Receptor de valor recibe una copia y no puede modificar el original; receptor de puntero
(
*T) sí puede. - Un puntero guarda la dirección de otra variable. Go inserta el
&y el*por ti al llamar métodos. - El valor cero de un puntero es
nil; leer a través de él provoca un panic. errores una interfaz con un solo métodoError() string. Un error es un valor común.- Se crean con
errors.New(simples), como centinelas (var ErrX = errors.New(...)) o confmt.Errorf(con datos). %wenvuelve un error conservando el original;%vlo aplasta y rompeerrors.Issin avisar.errors.Ispregunta si un error está en la cadena;errors.Asrecupera el error de un tipo concreto para leer sus datos.- Una interfaz es una lista de métodos. Un tipo la cumple por tener los métodos, sin declararlo.
- Las interfaces de Go se hacen pequeñas y se definen donde se usan.
- Una interfaz pequeña permite probar sin el mundo exterior: se sustituye la implementación real por una falsa.
Para leer más
- A Tour of Go — Methods — oficial e interactivo: receptores, punteros e interfaces con ejercicios en el navegador.
- Effective Go — Interfaces and other types — la explicación oficial de por qué la satisfacción de interfaces es implícita.
- Error handling and Go — la entrada del blog oficial de Go sobre por qué los errores son valores y cómo se manejan idiomáticamente.
- Paquete
errors— documentación oficial — la referencia exacta deerrors.Is,errors.Asyerrors.Unwrap.
Términos de esta lección
| campo | cada dato que contiene un struct |
| envolver (un error) | agregarle contexto conservando el original, con %w |
| error centinela | error declarado una vez a nivel de paquete, para comparar con errors.Is |
| exportado / no exportado | visible fuera del paquete (mayúscula) o no (minúscula) |
| interfaz | lista de métodos; un tipo la cumple al tener esos métodos |
| método | función asociada a un tipo mediante un receptor |
nil |
ausencia de valor; el valor cero de punteros, interfaces y errores |
nil pointer dereference |
panic por leer a través de un puntero nulo |
| panic | aborto del programa por un error irrecuperable |
| puntero | variable que guarda la dirección de otra; su tipo se escribe *T |
| receptor | el (s Servicio) que liga una función a un tipo |
| satisfacción implícita | cumplir una interfaz sin declararlo |
| struct | tipo que agrupa varios campos |
Anterior: Lección 2 — Variables, funciones y tipos · Siguiente: Lección 4 — Colecciones
¿Prefiere correo? Escríbanos a hola@habil.mx