Interfaces en Go: duck typing estático e implicit satisfaction

Las interfaces en Go funcionan de forma distinta a la mayoría de lenguajes orientados a objetos: no hace falta declarar que un tipo implementa una interfaz. Si el tipo tiene los métodos correctos, la cumple automáticamente. A esto se le llama satisfacción implícita o duck typing estático, y es una de las decisiones de diseño más elogiadas del lenguaje.

Definir e implementar una interfaz

package main

import (
    "fmt"
    "math"
)

type Forma interface {
    Area() float64
    Perimetro() float64
}

type Circulo struct{ Radio float64 }
type Rectangulo struct{ Ancho, Alto float64 }

func (c Circulo) Area() float64      { return math.Pi * c.Radio * c.Radio }
func (c Circulo) Perimetro() float64 { return 2 * math.Pi * c.Radio }

func (r Rectangulo) Area() float64      { return r.Ancho * r.Alto }
func (r Rectangulo) Perimetro() float64 { return 2 * (r.Ancho + r.Alto) }

func describir(f Forma) {
    fmt.Printf("Área: %.2f, Perímetro: %.2fn", f.Area(), f.Perimetro())
}

func main() {
    describir(Circulo{Radio: 5})
    describir(Rectangulo{Ancho: 4, Alto: 3})
}

Ninguno de los dos tipos menciona la interfaz Forma en su definición. El compilador verifica que ambos tienen los métodos necesarios y permite pasar cualquiera de ellos donde se espere un Forma.

Composición de interfaces

Las interfaces pueden incluir otras interfaces. Esto permite construir contratos más amplios a partir de contratos pequeños sin duplicar métodos:

type Lector interface {
    Leer(p []byte) (n int, err error)
}

type Escritor interface {
    Escribir(p []byte) (n int, err error)
}

type LectorEscritor interface {
    Lector
    Escritor
}

La interfaz vacía: interface{} y any

La interfaz sin métodos la cumple cualquier tipo. Desde Go 1.18 tiene el alias any:

func imprimir(v any) {
    fmt.Printf("tipo: %T, valor: %vn", v, v)
}

func main() {
    imprimir(42)
    imprimir("hola")
    imprimir([]int{1, 2, 3})
}

Úsala con moderación: perder el tipo significa que el compilador ya no puede ayudarte. Prefiere siempre interfaces con métodos concretos.

Type assertion

Cuando tienes un valor de tipo interfaz y necesitas el tipo concreto, usa una type assertion:

var v any = "programacion.net"

s, ok := v.(string)
if ok {
    fmt.Println(strings.ToUpper(s)) // PROGRAMACION.NET
}

// sin ok, un tipo incorrecto produce un pánico:
// n := v.(int) // panic: interface conversion: string is not int

Type switch

Para manejar varios tipos posibles, el type switch es más idiomático que encadenar type assertions:

func describeTipo(v any) string {
    switch t := v.(type) {
    case int:
        return fmt.Sprintf("entero: %d", t)
    case string:
        return fmt.Sprintf("cadena de %d bytes", len(t))
    case bool:
        if t {
            return "verdadero"
        }
        return "falso"
    case []int:
        return fmt.Sprintf("slice de %d enteros", len(t))
    default:
        return fmt.Sprintf("tipo desconocido: %T", t)
    }
}

func main() {
    fmt.Println(describeTipo(42))
    fmt.Println(describeTipo("hola"))
    fmt.Println(describeTipo([]int{1, 2}))
}

Interfaces de un solo método: el convenio de nombre

Por convenio, las interfaces con un único método se nombran añadiendo el sufijo -er al nombre del método: Reader, Writer, Stringer, Closer. Seguir este convenio hace tu código más fácil de entender para cualquier programador Go.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP