En esta página

Interfaces

14 min lectura TextoCap. 2 — Funciones y estructuras

Interfaces en Go: polimorfismo implícito

Las interfaces de Go son una de sus características más elegantes y poderosas. A diferencia de Java o C#, en Go no declares que un tipo implementa una interfaz. Si un tipo tiene los métodos que la interfaz requiere, automáticamente la implementa. Esto se conoce como duck typing estático: si camina como un pato y hace cuac como un pato, es un pato.

Esta aproximación tiene consecuencias profundas: el acoplamiento entre tipos e interfaces es mínimo, el código es más flexible, y puedes crear interfaces en el consumidor (el código que las usa) en lugar del proveedor (el código que las define).

Definiendo una interfaz

// Una interfaz define un conjunto de métodos
type Escritor interface {
    Escribir(datos []byte) (n int, err error)
}

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

// Las interfaces pueden componerse
type LectorEscritor interface {
    Lector
    Escritor
}

La convención en Go es nombrar interfaces de un solo método con el nombre del método más el sufijo -er: Reader, Writer, Stringer, Closer, Handler.

Implementación implícita

type Loggable interface {
    Log() string
}

type Servidor struct {
    Host  string
    Puerto int
}

// Servidor implementa Loggable automáticamente — sin declaración explícita
func (s Servidor) Log() string {
    return fmt.Sprintf("Servidor en %s:%d", s.Host, s.Puerto)
}

type BaseDeDatos struct {
    URL string
}

func (b BaseDeDatos) Log() string {
    return fmt.Sprintf("Base de datos en %s", b.URL)
}

// Función que acepta cualquier Loggable
func registrar(l Loggable) {
    fmt.Println("[LOG]", l.Log())
}

func main() {
    s := Servidor{Host: "localhost", Puerto: 8080}
    db := BaseDeDatos{URL: "postgres://localhost:5432/midb"}

    registrar(s)   // funciona sin que Servidor declare "implements Loggable"
    registrar(db)  // igual para BaseDeDatos
}

Interfaces como tipos: polimorfismo

Una variable de tipo interfaz puede contener cualquier valor concreto que implemente esa interfaz:

type Animal interface {
    Sonido() string
    Nombre() string
}

type Perro struct{ nombre string }
func (p Perro) Sonido() string { return "¡Woof!" }
func (p Perro) Nombre() string { return p.nombre }

type Gato struct{ nombre string }
func (g Gato) Sonido() string { return "¡Miau!" }
func (g Gato) Nombre() string { return g.nombre }

func hacerSonido(a Animal) {
    fmt.Printf("%s dice: %s\n", a.Nombre(), a.Sonido())
}

func main() {
    animales := []Animal{
        Perro{nombre: "Rex"},
        Gato{nombre: "Whiskers"},
        Perro{nombre: "Max"},
    }

    for _, a := range animales {
        hacerSonido(a)
    }
}

La interfaz vacía: `any` (antes `interface{}`)

La interfaz vacía interface{} — ahora con el alias any desde Go 1.18 — puede contener un valor de cualquier tipo. Es el equivalente de Object en Java u object en C#:

// any puede contener cualquier tipo
func imprimir(v any) {
    fmt.Printf("(%T) %v\n", v, v)
}

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

    // Slice de any
    mezcla := []any{1, "dos", 3.0, true}
    for _, v := range mezcla {
        fmt.Println(v)
    }

    // Map con valores de tipo any
    config := map[string]any{
        "host":   "localhost",
        "puerto": 8080,
        "debug":  true,
    }
    fmt.Println(config["host"])
}

El uso de any debe ser moderado. Cuando lo usas, pierdes la seguridad de tipos. Prefiere genéricos o interfaces específicas cuando sea posible.

Type assertions: extraer el tipo concreto

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

var f Forma = Círculo{Radio: 5}

// Forma segura con "comma ok"
c, ok := f.(Círculo)
if ok {
    fmt.Printf("Es un círculo con radio %.1f\n", c.Radio)
} else {
    fmt.Println("No es un círculo")
}

// Forma directa (panic si el tipo es incorrecto)
c2 := f.(Círculo)  // ¡pánico si f no es Círculo!
fmt.Println(c2.Radio)

// Siempre prefiere la forma con "comma ok"

Type switches: ramificar por tipo

El type switch es la forma idiomática de manejar múltiples tipos posibles:

func procesar(v any) {
    switch x := v.(type) {
    case nil:
        fmt.Println("nil")
    case int:
        fmt.Printf("entero: %d (doble: %d)\n", x, x*2)
    case int64:
        fmt.Printf("int64: %d\n", x)
    case float64:
        fmt.Printf("float64: %.2f\n", x)
    case string:
        fmt.Printf("string: %q (longitud: %d)\n", x, len(x))
    case bool:
        if x {
            fmt.Println("verdadero")
        } else {
            fmt.Println("falso")
        }
    case []int:
        fmt.Printf("slice de int con %d elementos\n", len(x))
    case error:
        fmt.Printf("error: %v\n", x)
    default:
        fmt.Printf("tipo desconocido: %T = %v\n", x, x)
    }
}

Interfaces importantes de la biblioteca estándar

`fmt.Stringer`

type Stringer interface {
    String() string
}

Si tu tipo implementa String() string, fmt.Println, fmt.Printf y similares usarán ese método automáticamente:

type Punto struct{ X, Y int }

func (p Punto) String() string {
    return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}

p := Punto{3, 4}
fmt.Println(p)       // (3, 4) — usa String()
fmt.Printf("%v\n", p) // (3, 4)
fmt.Printf("%s\n", p) // (3, 4)

`error`

type error interface {
    Error() string
}

Cualquier tipo con un método Error() string es un error en Go:

type ErrorValidación struct {
    Campo   string
    Mensaje string
}

func (e *ErrorValidación) Error() string {
    return fmt.Sprintf("campo %q: %s", e.Campo, e.Mensaje)
}

func validarEmail(email string) error {
    if !strings.Contains(email, "@") {
        return &ErrorValidación{Campo: "email", Mensaje: "formato inválido"}
    }
    return nil
}

`io.Reader` e `io.Writer`

Estas dos interfaces son las más usadas en toda la biblioteca estándar:

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

Archivos, conexiones de red, buffers en memoria, compresión gzip, cifrado — todos implementan estas interfaces. Esto permite que el código funcione con cualquier fuente o destino:

import (
    "bytes"
    "io"
    "os"
    "strings"
)

// copiar acepta cualquier Reader y cualquier Writer
func copiar(dst io.Writer, src io.Reader) (int64, error) {
    return io.Copy(dst, src)
}

func main() {
    // copiar un string a stdout
    copiar(os.Stdout, strings.NewReader("¡Hola, interfaces!\n"))

    // copiar un archivo a un buffer en memoria
    f, _ := os.Open("datos.txt")
    defer f.Close()
    var buf bytes.Buffer
    copiar(&buf, f)
}

Interfaces y nil

Una variable de tipo interfaz tiene dos componentes: tipo y valor. Una interfaz es nil solo cuando ambos son nil:

var r io.Reader  // nil — tipo nil, valor nil

var f *os.File    // nil pointer
var r2 io.Reader = f  // ¡NO es nil! tipo=*os.File, valor=nil

fmt.Println(r == nil)   // true
fmt.Println(r2 == nil)  // false — ¡trampa común!

Esta sutileza es importante al retornar errores:

// INCORRECTO: retorna una interfaz no-nil aunque p sea nil
func obtenerError() error {
    var p *MiError = nil
    return p  // la interfaz tendrá tipo=*MiError, valor=nil
}

// CORRECTO: retorna nil explícito de tipo interfaz
func obtenerError() error {
    return nil
}

Con las interfaces dominadas, en la próxima lección aprenderemos las colecciones fundamentales de Go: slices y maps — sus diferencias, operaciones y el idiom "comma ok".

Las interfaces de Go son implícitas: no se declara implements
En Go, un tipo implementa una interfaz automáticamente si tiene todos los métodos requeridos. No existe la palabra clave implements como en Java o C#. Esto permite que tipos de paquetes externos implementen tus interfaces sin modificarlos, y que tus tipos implementen interfaces de la biblioteca estándar automáticamente.
Prefiere interfaces pequeñas con uno o dos métodos
La filosofía de Go favorece interfaces pequeñas. La interfaz io.Reader solo tiene un método Read(). La interfaz fmt.Stringer solo tiene String(). Las interfaces pequeñas son más fáciles de implementar, combinar y probar. Sigue el principio de segregación de interfaces (ISP).