Funciones variádicas: sintaxis, expansión y el uso de any

Una función variádica es aquella que puede aceptar un número indeterminado de argumentos de un mismo tipo. Lo que ocurre por debajo es que el compilador toma todos esos argumentos y los empaqueta en un slice del tipo especificado. Por eso, dentro del cuerpo de la función, el parámetro variádico se comporta exactamente como un slice normal.

Este diseño permite que la API sea extremadamente flexible: puedes llamar a la función con tres argumentos, con cien, o incluso sin ninguno. Es el mecanismo fundamental que utiliza fmt.Printf para permitir que le pases una combinación de valores de cualquier tipo; en su caso, utiliza un parámetro variádico de tipo any [disponible desde Go 1.18]. Sin embargo, esta flexibilidad tiene reglas estrictas: la función solo puede tener un parámetro variádico y, por diseño, este debe ser siempre el último de la lista de parámetros.

Si intentas pasar un slice directamente a un parámetro variádico sin usar el operador de expansión ..., el compilador fallará porque espera recibir elementos individuales del tipo esperado, no el contenedor completo.

package main

import (
	"fmt"
)

// Sumar recibe cualquier cantidad de enteros y devuelve su suma.
// El parámetro 'nums' se trata como un []int dentro de la función.
func Sumar(nums ...int) int {
	total := 0
	for _, n := range nums {
		total += n
	}
	return total
}

// FormatearLog permite imprimir un prefijo seguido de cualquier cantidad
// de valores de cualquier tipo (heterogéneos).
func FormatearLog(prefijo string, elementos ...any) {
	fmt.Printf("[%s] ", prefijo)
	// Usamos el operador de expansión %v para imprimir el valor real
	fmt.Printf("Valores: %v\n", elementos)
}

func main() {
	// 1. Paso de argumentos individuales (Packing)
	// El compilador crea un slice con [10, 20, 30]
	resultado1 := Sumar(10, 20, 30)
	fmt.Printf("Suma 1: %d\n", resultado1)

	// 2. Uso de un slice existente (Unpacking/Expansión)
	// Debemos usar '...' para "desempaquetar" el slice en argumentos individuales
	numeros := []int{1, 2, 3, 4, 5}
	resultado2 := Sumar(numeros...)
	fmt.Printf("Suma 2: %d\n", resultado2)

	// 3. Llamada sin argumentos
	// 'nums' será un slice de longitud 0
	resultado3 := Sumar()
	fmt.Printf("Suma 3 (vacía): %d\n", resultado3)

	// 4. Mix de tipos con 'any'
	// Similar a fmt.Printf, permite mezclar tipos gracias a 'any'
	FormatearLog("INFO", "Conexión establecida", 200, true, 0.75)

	// 5. Diferencia sutil entre nil y slice vacío
	// f(nil...) pasará un slice nil, f([]int{}...) pasará un slice con len 0
	verificarNil(nil)
	verificarVacio([]int{})
}

func verificarNil(n ...int) {
	fmt.Printf("Es nil: %v, len: %d\n", n == nil, len(n))
}

func verificarVacio(n ...int) {
	fmt.Printf("Es nil: %v, len: %d\n", n == nil, len(n))
}

En el ejemplo anterior, Sumar demuestra cómo el compilador realiza el “packing”: cuando llamas a Sumar(10, 20, 30), los valores se agrupan en un []int. Por el contrario, al usar Sumar(numeros...), estamos haciendo “unpacking”; le decimos al compilador que no envíe el slice como un solo bloque, sino que extienda sus elementos como argumentos individuales.

La función FormatearLog es la clave para entender por qué fmt.Printf es tan potente. Al usar ...any, la función puede aceptar una lista de elementos de tipos distintos (un string, un int, un bool). Sin este patrón, tendrías que definir una función diferente para cada combinación de tipos posible.

Un detalle de nivel avanzado es la distinción entre un slice nil y uno vacío en funciones variádicas. En verificarNil, al pasar nil explícitamente con la expansión nil..., el parámetro n dentro de la función será efectivamente nil. En verificarVacio, al pasar un slice inicializado pero vacío []int{}, el parámetro n no será nil, pero su longitud será 0. Esto es vital cuando la lógica de tu función depende de la distinción entre “ausencia de datos” (nil) y “colección sin elementos” (len == 0).

El error frecuente

El error más común ocurre al intentar pasar un slice a una función que espera argumentos variádicos, olvidando el operador de expansión.

func Procesar(puntos ...int) {
    // ...
}

func main() {
    lista := []int{1, 2, 3}
    
    // ERROR DE COMPILACIÓN:
    // cannot use lista (type []int) as type int in argument to Procesar
    Procesar(lista) 

    // FORMA CORRECTA:
    Procesar(lista...)
}

El compilador no es capaz de adivinar que quieres desempaquetar el slice; para él, estás intentando pasar un objeto de tipo []int donde se requiere un int.

42

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio