polimorfismo-interface-any-type-assertions

Polimorfismo en Go con interfaces, any y type switches

  • 5 min

El polimorfismo en Go es la capacidad de tratar valores distintos a través de una interfaz común, siempre que esos valores cumplan los métodos esperados.

Hasta ahora hemos sido muy estrictos: un int es un int y no puede ser un string. Go nos obliga a ser ordenados. Pero, ¿cómo hace la función fmt.Println() para imprimir cualquier cosa que le pasemos? ¿Cómo procesamos un JSON donde un campo puede ser un número o un texto?

Para el polimorfismo basado en comportamiento usamos interfaces con métodos. Cuando de verdad desconocemos el tipo, podemos recurrir a la interfaz vacía, cuyo alias es any.

Vamos a ver primero las interfaces implícitas y después cómo inspeccionar el tipo dinámico guardado en un any.

Interfaces implícitas

En Go no escribimos implements. Un tipo satisface una interfaz si su conjunto de métodos contiene los que esta declara.

type Saludador interface {
    Saludar() string
}

type Persona struct {
    Nombre string
}

func (p Persona) Saludar() string {
    return "Hola, soy " + p.Nombre
}

func Presentar(s Saludador) {
    fmt.Println(s.Saludar())
}
Copied!

Persona puede pasarse a Presentar sin declarar ninguna relación con Saludador. Esta independencia permite que el paquete consumidor defina interfaces pequeñas según el comportamiento que necesita.

La interfaz vacía (interface{}) y el alias any

En Go, una interfaz define un conjunto de métodos. Un tipo cumple la interfaz si implementa esos métodos.

Ahora sigue esta lógica:

  1. Si una interfaz no pide ningún método (está vacía)…
  2. …entonces todos los tipos del universo cumplen esa interfaz (porque todos tienen, al menos, “cero métodos”).

La interfaz vacía se escribía tradicionalmente como interface{}. Desde Go 1.18, tenemos un alias mucho más legible: any.

package main

import "fmt"

// Esta función acepta CUALQUIER tipo de dato
func ImprimirCualquierCosa(v any) {
    fmt.Printf("Valor: %v, Tipo: %T\n", v, v)
}

func main() {
    ImprimirCualquierCosa(42)           // Pasa un int
    ImprimirCualquierCosa("Hola")       // Pasa un string
    ImprimirCualquierCosa(true)         // Pasa un bool
    ImprimirCualquierCosa([]int{1, 2})  // Pasa un slice
}
Copied!

Aunque ahora se prefiere usar any por legibilidad, en código antiguo verás muchísimo interface{}. Son exactamente lo mismo.

El tipo estático de any

Cuando asignamos un valor a una variable any, su tipo dinámico se conserva, pero el compilador solo permite usar las operaciones definidas por su tipo estático, que en este caso no exige ningún método.

var x any = 10

// x = x + 1 // ERROR: invalid operation: x (type any) + 1
Copied!

Para el compilador, x tiene tipo estático any. No podemos sumarlo ni usarlo como índice hasta comprobar su tipo dinámico mediante una aserción de tipo.

Aserciones de tipo

Una aserción de tipo permite obtener el valor con un tipo concreto o comprobar que también satisface otra interfaz.

Sintaxis: valor := variableInterfaz.(TipoConcreto)

func main() {
    var i any = "Hola Mundo"

    // Afirmamos: "Yo sé que dentro de 'i' hay un string. Dámelo".
    s := i.(string)

    fmt.Println(s) // "Hola Mundo"
}
Copied!

El riesgo de panic

¿Qué pasa si te equivocas y dentro de la caja no había lo que tú pensabas?

var i any = "Hola"
n := i.(int) // ¡PANIC!
// El programa se detiene: interface conversion: interface {} is string, not int
Copied!

La forma segura: comma, ok

Al igual que con los Mapas, podemos usar el segundo valor de retorno para verificar si la conversión fue exitosa sin romper el programa.

var i any = "Hola"

if n, ok := i.(int); ok {
    fmt.Println("Es un entero:", n)
} else {
    fmt.Println("No era un entero. Operación segura.")
}
Copied!

Type switches

Si tienes una variable any y quieres comportarte de forma distinta según si es un string, un número o un booleano, podrías encadenar muchos if-else con aserciones. Pero eso es feo.

Go nos ofrece el Type Switch. Es un switch especial donde evaluamos el tipo (.type) en lugar del valor.

func Analizar(v any) {
    // Usamos .(type) dentro del switch
    switch valor := v.(type) {
    case int:
        fmt.Printf("Es un entero. Su cuadrado es %d\n", valor*valor)
    case string:
        fmt.Printf("Es un texto de longitud %d\n", len(valor))
    case bool:
        fmt.Printf("Es un booleano. Valor: %t\n", valor)
    case Persona: // También funciona con nuestros Structs
        fmt.Println("Es una persona llamada", valor.Nombre)
    default:
        fmt.Printf("No sé qué es esto: %T\n", valor)
    }
}
Copied!

Fíjate en switch valor := v.(type). La variable valor dentro de cada case ya tiene el tipo correcto.

  • En el case int, valor es un int.
  • En el case string, valor es un string. ¡Es muy cómodo!

Cuándo usar any

El uso de any es tentador. Podrías pensar: “Genial, voy a declarar todas mis funciones con any y así no me preocupo por los tipos, como en Python”.

¡ERROR! Si haces eso, pierdes todas las ventajas de Go (seguridad en tiempo de compilación, autocompletado, rendimiento).

Cuándo SÍ usar any:

  1. Datos desconocidos: Cuando lees un JSON arbitrario y no sabes qué estructura tiene.
  2. Colecciones realmente heterogéneas: si todos los elementos comparten una operación o un tipo parametrizable, es preferible una interfaz concreta o genéricos.
  3. Funciones de formateo/log: Como fmt.Printf o loggers que deben aceptar cualquier mensaje.

Cuándo evitar any:

  • Cuando sabes el tipo de dato.
  • Como “pereza” para no definir una Interfaz adecuada.