← Volver al listado de tecnologías

Interfaces

Por: Artiko
gointerfacespolimorfismoduck-typing

Las interfaces en Go son implicitas: un tipo las satisface automaticamente si implementa todos sus metodos. No hay implements ni extends. Esto habilita un polimorfismo flexible conocido como duck typing.

Interfaces Implicitas

Si camina como pato y hace quack como pato, es un pato:

type Forma interface {
    Area() float64
}

type Circulo struct {
    Radio float64
}

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

type Rectangulo struct {
    Ancho, Alto float64
}

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

func imprimirArea(f Forma) {
    fmt.Printf("Area: %.2f\n", f.Area())
}

func main() {
    imprimirArea(Circulo{Radio: 5})        // Area: 78.54
    imprimirArea(Rectangulo{Ancho: 3, Alto: 4}) // Area: 12.00
}

Ni Circulo ni Rectangulo declaran que implementan Forma. Simplemente tienen el metodo Area() y eso basta.

La Interfaz Vacia: interface{} y any

interface{} (alias any desde Go 1.18) no tiene metodos, por lo que todo tipo la satisface:

func describir(v any) {
    fmt.Printf("Valor: %v, Tipo: %T\n", v, v)
}

func main() {
    describir(42)       // Valor: 42, Tipo: int
    describir("hola")   // Valor: hola, Tipo: string
    describir(true)     // Valor: true, Tipo: bool
    describir([]int{1}) // Valor: [1], Tipo: []int
}

Usa any con moderacion. Pierdes seguridad de tipos y necesitas type assertions para operar.

Interfaces Comunes: Stringer y error

fmt.Stringer

Controla como se imprime tu tipo:

type Moneda struct {
    Cantidad float64
    Divisa   string
}

func (m Moneda) String() string {
    return fmt.Sprintf("%.2f %s", m.Cantidad, m.Divisa)
}

func main() {
    precio := Moneda{Cantidad: 49.99, Divisa: "USD"}
    fmt.Println(precio) // 49.99 USD
}

error

La interfaz mas usada de Go:

type ErrorValidacion struct {
    Campo   string
    Mensaje string
}

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

func validarEdad(edad int) error {
    if edad < 0 {
        return &ErrorValidacion{
            Campo:   "edad",
            Mensaje: "no puede ser negativa",
        }
    }
    return nil
}

Type Assertion

Extrae el tipo concreto de una interfaz:

func procesar(v any) {
    // Forma segura con ok
    s, ok := v.(string)
    if ok {
        fmt.Println("Es string:", s)
        return
    }

    n, ok := v.(int)
    if ok {
        fmt.Println("Es int:", n)
        return
    }

    fmt.Println("Tipo desconocido")
}

func main() {
    procesar("hola") // Es string: hola
    procesar(42)     // Es int: 42
    procesar(3.14)   // Tipo desconocido
}

Sin el segundo valor ok, una assertion fallida causa panic:

var v any = "texto"
n := v.(int) // PANIC: interface conversion error

Type Switch

Mas elegante que multiples type assertions:

func clasificar(v any) string {
    switch val := v.(type) {
    case int:
        return fmt.Sprintf("entero: %d", val)
    case string:
        return fmt.Sprintf("texto: %q (len=%d)", val, len(val))
    case bool:
        if val {
            return "verdadero"
        }
        return "falso"
    case []int:
        return fmt.Sprintf("slice de %d elementos", len(val))
    default:
        return fmt.Sprintf("tipo no manejado: %T", val)
    }
}

func main() {
    fmt.Println(clasificar(42))          // entero: 42
    fmt.Println(clasificar("Go"))        // texto: "Go" (len=2)
    fmt.Println(clasificar([]int{1, 2})) // slice de 2 elementos
}

Composicion de Interfaces

Las interfaces se componen embebiendo otras interfaces:

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

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

type LectorEscritor interface {
    Lector
    Escritor
}

Esto es exactamente lo que hace la stdlib de Go:

// En el paquete io
type Reader interface {
    Read(p []byte) (n int, err error)
}

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

type ReadWriter interface {
    Reader
    Writer
}

Interfaces Comunes de la Stdlib

InterfazPaqueteMetodoUso
ReaderioRead([]byte) (int, error)Leer datos
WriterioWrite([]byte) (int, error)Escribir datos
CloserioClose() errorLiberar recursos
StringerfmtString() stringRepresentacion textual
errorbuiltinError() stringManejo de errores
Handlernet/httpServeHTTP(w, r)Manejar HTTP
InterfacesortLen, Less, SwapOrdenar colecciones

Ejemplo con sort.Interface

type Personas []Persona

type Persona struct {
    Nombre string
    Edad   int
}

func (p Personas) Len() int           { return len(p) }
func (p Personas) Less(i, j int) bool { return p[i].Edad < p[j].Edad }
func (p Personas) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }

func main() {
    grupo := Personas{
        {"Carlos", 30},
        {"Ana", 25},
        {"Luis", 35},
    }
    sort.Sort(grupo)
    for _, p := range grupo {
        fmt.Printf("%s (%d)\n", p.Nombre, p.Edad)
    }
    // Ana (25)
    // Carlos (30)
    // Luis (35)
}

Ejemplo con io.Reader

func contarPalabras(r io.Reader) (int, error) {
    scanner := bufio.NewScanner(r)
    scanner.Split(bufio.ScanWords)
    count := 0
    for scanner.Scan() {
        count++
    }
    return count, scanner.Err()
}

func main() {
    // Funciona con strings
    r := strings.NewReader("hola mundo desde Go")
    n, _ := contarPalabras(r)
    fmt.Println(n) // 4

    // Funciona con archivos, HTTP bodies, buffers...
    // Cualquier cosa que implemente io.Reader
}

Patron: Accept Interfaces, Return Structs

Este es uno de los principios mas importantes de diseno en Go:

// BIEN: acepta interfaz, cualquier Reader funciona
func ProcesarDatos(r io.Reader) (*Resultado, error) {
    data, err := io.ReadAll(r)
    if err != nil {
        return nil, err
    }
    return &Resultado{Datos: data}, nil
}

// MAL: acepta tipo concreto, limita la flexibilidad
func ProcesarDatos(f *os.File) (*Resultado, error) { ... }

Beneficios:

Verificar Implementacion en Compilacion

Puedes forzar que un tipo implemente una interfaz:

type Almacen interface {
    Guardar(datos []byte) error
    Obtener(id string) ([]byte, error)
}

type AlmacenMemoria struct {
    datos map[string][]byte
}

// Verificacion en tiempo de compilacion
var _ Almacen = (*AlmacenMemoria)(nil)

func (a *AlmacenMemoria) Guardar(datos []byte) error {
    // implementacion...
    return nil
}

func (a *AlmacenMemoria) Obtener(id string) ([]byte, error) {
    // implementacion...
    return nil, nil
}

La linea var _ Almacen = (*AlmacenMemoria)(nil) genera error de compilacion si AlmacenMemoria no satisface Almacen.

Resumen

ConceptoDescripcion
ImplicitasNo se declara implements
any / interface{}Acepta cualquier tipo
Type assertionv.(T) extrae tipo concreto
Type switchswitch v.(type) para multiples tipos
ComposicionInterfaces embeben otras interfaces
Patron claveAcepta interfaces, retorna structs

← Capitulo 10: Metodos | Capitulo 12: Manejo de Errores →