Cristhian Villegas
Backend10 min read1 views

Curso Go #4: Funciones y Manejo de Errores en Go

Curso Go #4: Funciones y Manejo de Errores en Go

Introduccion: funciones y errores en Go

Bienvenido a la Parte 4 del curso de Go para principiantes. En el articulo anterior aprendiste a controlar el flujo de tu programa con if, switch y for. Ahora vamos a aprender a organizar tu codigo en funciones reutilizables y a manejar errores de la forma idiomatica de Go.

Logo de Go

Las funciones son los bloques fundamentales de cualquier programa en Go. Pero lo que hace a Go realmente especial es su enfoque unico para manejar errores: en lugar de excepciones, Go usa retornos multiples y el patron if err != nil. Este enfoque es explicito, predecible y te obliga a pensar en que puede salir mal en cada paso.

Sintaxis basica de funciones

Una funcion en Go se declara con la palabra clave func, seguida del nombre, los parametros entre parentesis y el tipo de retorno.

go
1package main
2
3import "fmt"
4
5// Funcion sin retorno
6func saludar(nombre string) {
7    fmt.Println("Hola,", nombre)
8}
9
10// Funcion con retorno
11func sumar(a int, b int) int {
12    return a + b
13}
14
15// Parametros del mismo tipo se pueden abreviar
16func multiplicar(a, b int) int {
17    return a * b
18}
19
20func main() {
21    saludar("Carlos")
22    resultado := sumar(5, 3)
23    fmt.Println("5 + 3 =", resultado)
24    fmt.Println("4 * 7 =", multiplicar(4, 7))
25}
Nota: En Go, las funciones que empiezan con mayuscula son exportadas (visibles desde otros paquetes). Las que empiezan con minuscula son privadas al paquete. Esto aplica tambien a variables, tipos y constantes.

Retornos multiples: la firma de Go

Una de las caracteristicas mas distintivas de Go es que las funciones pueden retornar multiples valores. Esto es fundamental para el manejo de errores.

go
1package main
2
3import (
4    "errors"
5    "fmt"
6)
7
8// Retorna dos valores: resultado y error
9func dividir(a, b float64) (float64, error) {
10    if b == 0 {
11        return 0, errors.New("no se puede dividir entre cero")
12    }
13    return a / b, nil
14}
15
16func main() {
17    resultado, err := dividir(10, 3)
18    if err != nil {
19        fmt.Println("Error:", err)
20        return
21    }
22    fmt.Printf("10 / 3 = %.2f
23", resultado)
24
25    resultado, err = dividir(10, 0)
26    if err != nil {
27        fmt.Println("Error:", err)
28        return
29    }
30    fmt.Printf("10 / 0 = %.2f
31", resultado)
32}

Otro ejemplo practico: una funcion que busca un elemento en un slice y retorna su posicion.

go
1package main
2
3import "fmt"
4
5func buscar(nombres []string, objetivo string) (int, bool) {
6    for i, nombre := range nombres {
7        if nombre == objetivo {
8            return i, true
9        }
10    }
11    return -1, false
12}
13
14func main() {
15    lista := []string{"Ana", "Pedro", "Maria", "Luis"}
16
17    if pos, encontrado := buscar(lista, "Maria"); encontrado {
18        fmt.Printf("Maria esta en la posicion %d
19", pos)
20    } else {
21        fmt.Println("Maria no fue encontrada")
22    }
23}
Tip: Si no necesitas uno de los valores retornados, usa el identificador en blanco _ para descartarlo: _, err := dividir(10, 0).

Retornos con nombre

Go permite nombrar los valores de retorno en la firma de la funcion. Esto los declara como variables locales y permite usar return sin argumentos (naked return).

go
1package main
2
3import "fmt"
4
5func calcularEstadisticas(numeros []float64) (suma, promedio float64, cantidad int) {
6    cantidad = len(numeros)
7    if cantidad == 0 {
8        return // retorna los valores cero de cada tipo
9    }
10
11    for _, n := range numeros {
12        suma += n
13    }
14    promedio = suma / float64(cantidad)
15    return // retorna suma, promedio, cantidad
16}
17
18func main() {
19    datos := []float64{85.5, 92.0, 78.3, 95.1, 88.7}
20    suma, promedio, cantidad := calcularEstadisticas(datos)
21
22    fmt.Printf("Cantidad: %d
23", cantidad)
24    fmt.Printf("Suma: %.2f
25", suma)
26    fmt.Printf("Promedio: %.2f
27", promedio)
28}
Cuidado: Los retornos con nombre son utiles en funciones cortas, pero en funciones largas pueden dificultar la lectura. El equipo de Go recomienda usarlos principalmente para documentar el significado de los valores de retorno.

Funciones variadicas

Una funcion variadica acepta un numero variable de argumentos del mismo tipo. Se declara con ... antes del tipo del ultimo parametro.

go
1package main
2
3import "fmt"
4
5func sumarTodos(numeros ...int) int {
6    total := 0
7    for _, n := range numeros {
8        total += n
9    }
10    return total
11}
12
13func imprimirLinea(separador string, valores ...string) {
14    for i, v := range valores {
15        if i > 0 {
16            fmt.Print(separador)
17        }
18        fmt.Print(v)
19    }
20    fmt.Println()
21}
22
23func main() {
24    fmt.Println(sumarTodos(1, 2, 3))          // 6
25    fmt.Println(sumarTodos(10, 20, 30, 40))   // 100
26
27    // Pasar un slice a una funcion variadica
28    nums := []int{5, 10, 15}
29    fmt.Println(sumarTodos(nums...))           // 30
30
31    imprimirLinea(" - ", "Go", "es", "genial") // Go - es - genial
32}
Nota: La funcion fmt.Println que has usado a lo largo del curso es, de hecho, una funcion variadica. Su firma es func Println(a ...any) (n int, err error).

Funciones anonimas y closures

Go soporta funciones anonimas (funciones sin nombre) que pueden asignarse a variables o ejecutarse inmediatamente. Cuando una funcion anonima captura variables de su entorno, se convierte en un closure.

go
1package main
2
3import "fmt"
4
5func main() {
6    // Funcion anonima asignada a una variable
7    doble := func(n int) int {
8        return n * 2
9    }
10    fmt.Println(doble(5)) // 10
11
12    // Funcion anonima ejecutada inmediatamente (IIFE)
13    resultado := func(a, b int) int {
14        return a + b
15    }(3, 7)
16    fmt.Println(resultado) // 10
17
18    // Closure: captura la variable contador
19    contador := 0
20    incrementar := func() int {
21        contador++
22        return contador
23    }
24
25    fmt.Println(incrementar()) // 1
26    fmt.Println(incrementar()) // 2
27    fmt.Println(incrementar()) // 3
28    fmt.Println("Contador final:", contador) // 3
29}

Un ejemplo practico de closures: un generador de IDs.

go
1package main
2
3import "fmt"
4
5func crearGeneradorID(prefijo string) func() string {
6    id := 0
7    return func() string {
8        id++
9        return fmt.Sprintf("%s-%04d", prefijo, id)
10    }
11}
12
13func main() {
14    genUsuario := crearGeneradorID("USR")
15    genPedido := crearGeneradorID("ORD")
16
17    fmt.Println(genUsuario()) // USR-0001
18    fmt.Println(genUsuario()) // USR-0002
19    fmt.Println(genPedido())  // ORD-0001
20    fmt.Println(genUsuario()) // USR-0003
21    fmt.Println(genPedido())  // ORD-0002
22}

defer, panic y recover

Go tiene tres mecanismos especiales para controlar el flujo en situaciones excepcionales.

defer pospone la ejecucion de una funcion hasta que la funcion que la contiene termine. Es ideal para liberar recursos.

go
1package main
2
3import (
4    "fmt"
5    "os"
6)
7
8func leerArchivo(nombre string) {
9    archivo, err := os.Open(nombre)
10    if err != nil {
11        fmt.Println("Error al abrir:", err)
12        return
13    }
14    defer archivo.Close() // Se ejecuta al salir de la funcion
15
16    fmt.Println("Archivo abierto correctamente")
17    // ... procesar el archivo ...
18    // archivo.Close() se llama automaticamente al final
19}
20
21func main() {
22    // Los defers se ejecutan en orden LIFO (ultimo en entrar, primero en salir)
23    defer fmt.Println("1 - Primero en defer, ultimo en ejecutar")
24    defer fmt.Println("2 - Segundo en defer")
25    defer fmt.Println("3 - Tercero en defer, primero en ejecutar")
26
27    fmt.Println("Codigo principal")
28}
bash
1Codigo principal
23 - Tercero en defer, primero en ejecutar
32 - Segundo en defer
41 - Primero en defer, ultimo en ejecutar

panic detiene la ejecucion normal del programa. recover permite recuperarse de un panic dentro de un defer.

go
1package main
2
3import "fmt"
4
5func operacionRiesgosa() {
6    defer func() {
7        if r := recover(); r != nil {
8            fmt.Println("Recuperado del panic:", r)
9        }
10    }()
11
12    fmt.Println("Iniciando operacion...")
13    panic("algo salio terriblemente mal")
14    fmt.Println("Esta linea nunca se ejecuta")
15}
16
17func main() {
18    operacionRiesgosa()
19    fmt.Println("El programa continua despues del panic recuperado")
20}
Cuidado: En Go, panic y recover NO son el equivalente de try/catch de otros lenguajes. Solo debes usar panic para errores verdaderamente irrecuperables (como un estado corrupto del programa). Para errores normales, siempre usa el patron de retorno de errores.

El patron if err != nil: manejo de errores en Go

El manejo de errores en Go es explicito y se basa en retornar valores de tipo error. Este es el patron mas importante que debes dominar.

go
1package main
2
3import (
4    "errors"
5    "fmt"
6    "strconv"
7)
8
9func convertirAEdad(texto string) (int, error) {
10    edad, err := strconv.Atoi(texto)
11    if err != nil {
12        return 0, fmt.Errorf("valor invalido '%s': %w", texto, err)
13    }
14    if edad < 0 || edad > 150 {
15        return 0, fmt.Errorf("edad fuera de rango: %d", edad)
16    }
17    return edad, nil
18}
19
20func main() {
21    entradas := []string{"25", "abc", "-5", "200", "42"}
22
23    for _, entrada := range entradas {
24        edad, err := convertirAEdad(entrada)
25        if err != nil {
26            fmt.Printf("Error con '%s': %s
27", entrada, err)
28            continue
29        }
30        fmt.Printf("Edad valida: %d
31", edad)
32    }
33}

errors.New crea errores simples. fmt.Errorf crea errores con formato. El verbo %w permite envolver (wrap) el error original para mantener la cadena de errores.

go
1package main
2
3import (
4    "errors"
5    "fmt"
6)
7
8// Errores personalizados como variables de paquete
9var (
10    ErrNoEncontrado  = errors.New("recurso no encontrado")
11    ErrNoAutorizado  = errors.New("no autorizado")
12    ErrSinConexion   = errors.New("sin conexion a la base de datos")
13)
14
15func buscarUsuario(id int) (string, error) {
16    if id <= 0 {
17        return "", fmt.Errorf("buscarUsuario: id invalido %d: %w", id, ErrNoEncontrado)
18    }
19    // Simulacion: solo existe el usuario con id 1
20    if id != 1 {
21        return "", fmt.Errorf("buscarUsuario: id %d: %w", id, ErrNoEncontrado)
22    }
23    return "Carlos Gomez", nil
24}
25
26func main() {
27    nombre, err := buscarUsuario(99)
28    if err != nil {
29        // errors.Is verifica si el error (o alguno envuelto) es el esperado
30        if errors.Is(err, ErrNoEncontrado) {
31            fmt.Println("El usuario no existe:", err)
32        } else {
33            fmt.Println("Error inesperado:", err)
34        }
35        return
36    }
37    fmt.Println("Usuario encontrado:", nombre)
38}

Tipos de error personalizados

Para errores mas complejos, puedes crear tipos que implementen la interfaz error (que solo requiere el metodo Error() string).

go
1package main
2
3import (
4    "errors"
5    "fmt"
6)
7
8// Tipo de error personalizado
9type ErrorValidacion struct {
10    Campo   string
11    Mensaje string
12}
13
14func (e *ErrorValidacion) Error() string {
15    return fmt.Sprintf("validacion fallida en '%s': %s", e.Campo, e.Mensaje)
16}
17
18func validarUsuario(nombre string, edad int) error {
19    if nombre == "" {
20        return &ErrorValidacion{Campo: "nombre", Mensaje: "no puede estar vacio"}
21    }
22    if edad < 0 || edad > 150 {
23        return &ErrorValidacion{Campo: "edad", Mensaje: "debe estar entre 0 y 150"}
24    }
25    return nil
26}
27
28func main() {
29    err := validarUsuario("", 25)
30    if err != nil {
31        // errors.As extrae el tipo concreto del error
32        var errVal *ErrorValidacion
33        if errors.As(err, &errVal) {
34            fmt.Printf("Campo con error: %s
35", errVal.Campo)
36            fmt.Printf("Detalle: %s
37", errVal.Mensaje)
38        }
39        return
40    }
41
42    err = validarUsuario("Ana", -5)
43    if err != nil {
44        fmt.Println(err)
45    }
46}
Tip: Usa errors.Is para comparar errores por valor (como los errores centinela ErrNoEncontrado) y errors.As para comparar por tipo (como *ErrorValidacion). Ambas funciones recorren la cadena de errores envueltos automaticamente.

Buenas practicas para manejo de errores

El manejo de errores en Go tiene convenciones claras que debes seguir:

  • Siempre verifica los errores: nunca ignores un valor de retorno de tipo error.
  • Agrega contexto: usa fmt.Errorf con %w para envolver errores con informacion adicional.
  • Errores centinela: define errores conocidos como variables de paquete para que los consumidores puedan usar errors.Is.
  • No uses panic para errores normales: reservalo para situaciones verdaderamente irrecuperables.
  • Retorna errores, no los imprimas: la funcion que llama decide que hacer con el error.
go
1// MAL: imprimir y continuar
2func procesar(datos string) int {
3    resultado, err := convertir(datos)
4    if err != nil {
5        fmt.Println("Error:", err) // Quien llama no sabe que fallo
6        return 0
7    }
8    return resultado
9}
10
11// BIEN: retornar el error con contexto
12func procesar(datos string) (int, error) {
13    resultado, err := convertir(datos)
14    if err != nil {
15        return 0, fmt.Errorf("procesar '%s': %w", datos, err)
16    }
17    return resultado, nil
18}

Resumen y proximo articulo

En este articulo aprendiste los pilares de la programacion en Go:

  • Funciones: sintaxis basica, parametros abreviados y exportacion con mayusculas
  • Retornos multiples: la caracteristica mas distintiva de Go, esencial para el manejo de errores
  • Retornos con nombre: utiles para documentar y en funciones cortas
  • Funciones variadicas: aceptan un numero variable de argumentos con ...
  • Funciones anonimas y closures: capturan variables del entorno para crear generadores y callbacks
  • defer, panic, recover: para liberar recursos y manejar situaciones criticas
  • Patron if err != nil: el manejo explicito de errores que define a Go
  • Errores personalizados: tipos que implementan la interfaz error con errors.Is y errors.As

En el proximo articulo (Parte 5) aprenderemos sobre structs e interfaces: como definir tipos compuestos, asociarles metodos y usar interfaces para escribir codigo flexible y desacoplado.

Share:
CV

Cristhian Villegas

Software Engineer specializing in Java, Spring Boot, Angular & AWS. Building scalable distributed systems with clean architecture.

Comments

Sign in to leave a comment

No comments yet. Be the first!

Related Articles