En esta página

Funciones y múltiples retornos

14 min lectura TextoCap. 2 — Funciones y estructuras

Funciones en Go: flexibles y explícitas

Las funciones son los bloques fundamentales de todo programa Go. A diferencia de lenguajes como Python o JavaScript, las funciones de Go son más predecibles: el sistema de tipos estático garantiza que siempre sabrás exactamente qué recibe y qué devuelve cada función.

La característica más importante y diferenciadora de las funciones en Go es la capacidad de retornar múltiples valores. Esta característica está en el corazón del diseño del lenguaje y es fundamental para entender el manejo de errores idiomático.

Declaración básica de funciones

// Función simple
func saludar(nombre string) string {
    return "¡Hola, " + nombre + "!"
}

// Sin parámetros ni retorno
func limpiar() {
    fmt.Println("Limpiando...")
}

// Parámetros del mismo tipo — forma abreviada
func sumar(a, b int) int {
    return a + b
}

// Múltiples tipos de parámetros
func formatear(nombre string, edad int, activo bool) string {
    return fmt.Sprintf("%s (%d años, activo: %v)", nombre, edad, activo)
}

Múltiples valores de retorno

Esta es la característica que hace que Go sea tan expresivo para el manejo de errores. Una función puede retornar cualquier número de valores:

// Retorna dos valores: resultado y error
func dividir(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("no se puede dividir por cero")
    }
    return a / b, nil
}

// Retorna tres valores
func minMax(nums []int) (int, int, error) {
    if len(nums) == 0 {
        return 0, 0, errors.New("slice vacío")
    }
    min, max := nums[0], nums[0]
    for _, n := range nums[1:] {
        if n < min {
            min = n
        }
        if n > max {
            max = n
        }
    }
    return min, max, nil
}

func main() {
    // Debes manejar todos los valores de retorno
    resultado, err := dividir(10, 3)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Printf("Resultado: %.2f\n", resultado)

    // Ignorar el error con _ (generalmente no recomendado)
    min, max, _ := minMax([]int{5, 2, 8, 1, 9, 3})
    fmt.Println("Min:", min, "Max:", max)
}

Retornos nombrados

Los retornos nombrados te permiten darle nombres a los valores de retorno. Esto tiene dos ventajas: documenta el significado de cada valor y permite el "naked return":

// Los nombres min, max, promedio son documentación Y variables
func analizar(datos []float64) (min, max, promedio float64, err error) {
    if len(datos) == 0 {
        err = errors.New("se necesitan datos")
        return  // naked return: retorna el estado actual de min, max, promedio, err
    }

    min, max = datos[0], datos[0]
    suma := 0.0

    for _, d := range datos {
        suma += d
        if d < min {
            min = d
        }
        if d > max {
            max = d
        }
    }

    promedio = suma / float64(len(datos))
    return  // naked return: retorna los valores actuales
}

Los retornos nombrados son útiles para funciones que calculan múltiples resultados relacionados, pero evita el abuso de naked returns en funciones largas — pueden reducir la legibilidad.

Funciones variádicas

Las funciones variádicas aceptan un número variable de argumentos del mismo tipo usando ...:

func suma(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

func log(nivel string, partes ...interface{}) {
    fmt.Printf("[%s] ", nivel)
    fmt.Println(partes...)
}

func main() {
    fmt.Println(suma())           // 0
    fmt.Println(suma(1))          // 1
    fmt.Println(suma(1, 2, 3))    // 6
    fmt.Println(suma(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))  // 55

    // Pasar un slice a una función variádica con ...
    números := []int{10, 20, 30, 40}
    fmt.Println(suma(números...))  // 100

    log("INFO", "Servidor", "iniciado", "en puerto", 8080)
}

Dentro de la función, el parámetro variádico se comporta como un slice. El operador ... al llamar la función expande un slice en argumentos individuales.

Funciones como valores de primera clase

En Go, las funciones son valores. Puedes asignarlas a variables, pasarlas como argumentos y retornarlas:

// Tipo función
type Transformador func(int) int

// Función que acepta y retorna funciones (higher-order function)
func aplicarDosVeces(f Transformador, x int) int {
    return f(f(x))
}

func duplicar(n int) int {
    return n * 2
}

// Función anónima asignada a variable
triplicar := func(n int) int {
    return n * 3
}

func main() {
    fmt.Println(aplicarDosVeces(duplicar, 3))    // 12
    fmt.Println(aplicarDosVeces(triplicar, 2))   // 18

    // Función anónima llamada inmediatamente (IIFE)
    resultado := func(a, b int) int {
        return a + b
    }(5, 3)
    fmt.Println(resultado)  // 8
}

Closures: funciones con estado

Un closure es una función que captura variables de su scope exterior. La función "cierra sobre" esas variables, manteniéndolas vivas incluso después de que el scope exterior haya terminado:

// Factory de contadores — cada llamada crea un contador independiente
func nuevoContador() func() int {
    count := 0  // esta variable vive mientras exista la closure
    return func() int {
        count++
        return count
    }
}

// Closure para memoización
func memoize(f func(int) int) func(int) int {
    cache := make(map[int]int)
    return func(n int) int {
        if resultado, ok := cache[n]; ok {
            return resultado
        }
        resultado := f(n)
        cache[n] = resultado
        return resultado
    }
}

func fibonacci(n int) int {
    if n <= 1 {
        return n
    }
    return fibonacci(n-1) + fibonacci(n-2)
}

func main() {
    c1 := nuevoContador()
    c2 := nuevoContador()  // contador independiente

    fmt.Println(c1(), c1(), c1())  // 1 2 3
    fmt.Println(c2(), c2())        // 1 2 (independiente de c1)

    fibMemo := memoize(fibonacci)
    fmt.Println(fibMemo(40))  // rápido gracias al cache
}

`defer`: garantizar limpieza de recursos

defer posterga la ejecución de una función hasta que la función que lo contiene retorne. Es la forma idiomática de garantizar que los recursos se liberen correctamente, independientemente de cómo salga la función (retorno normal, error, o panic):

import "os"

func leerArchivo(nombre string) (string, error) {
    f, err := os.Open(nombre)
    if err != nil {
        return "", err
    }
    defer f.Close()  // garantizado: se ejecuta al salir, sin importar qué pase

    // leer contenido...
    datos := make([]byte, 1024)
    n, err := f.Read(datos)
    if err != nil {
        return "", err  // f.Close() se ejecutará igualmente
    }

    return string(datos[:n]), nil
}

Orden LIFO de defer

func demostrarDefer() {
    defer fmt.Println("tercero")   // se ejecuta primero (LIFO)
    defer fmt.Println("segundo")   // se ejecuta segundo
    defer fmt.Println("primero")   // se ejecuta último (fue el último defer registrado, se ejecuta primero)
    fmt.Println("función en ejecución")
}
// Output:
// función en ejecución
// primero
// segundo
// tercero

Defer con funciones que capturan variables

func contarTiempo(operación string) func() {
    inicio := time.Now()
    return func() {
        fmt.Printf("%s tomó %v\n", operación, time.Since(inicio))
    }
}

func operaciónCostosa() {
    defer contarTiempo("operaciónCostosa")()
    // ... lógica de la función
    time.Sleep(100 * time.Millisecond)
}

Recursión

Go soporta recursión con el mismo comportamiento que cualquier lenguaje:

func factorial(n int) int {
    if n <= 1 {
        return 1
    }
    return n * factorial(n-1)
}

func fibonacci(n int) int {
    if n <= 1 {
        return n
    }
    return fibonacci(n-1) + fibonacci(n-2)
}

Para recursión profunda, considera usar iteración o técnicas como la acumulación de cola (tail recursion), ya que Go no optimiza tail calls automáticamente.

Con un dominio sólido de las funciones de Go, en la siguiente lección aprenderemos los structs — la forma en que Go organiza datos y comportamiento para modelar el mundo real.

El patrón (valor, error) es la firma de Go
Retornar (resultado, error) como par es el patrón más idiomático de Go. El caller siempre debe verificar el error antes de usar el resultado. Este patrón hace el manejo de errores explícito y visible en el código, a diferencia de las excepciones que pueden ignorarse.
defer se ejecuta en orden LIFO
Múltiples defer se ejecutan en orden Last-In First-Out (último en entrar, primero en salir). Si tienes defer A(), defer B(), defer C(), se ejecutarán en orden C, B, A al salir de la función. Úsalo para garantizar limpieza de recursos como cerrar archivos o conexiones.