En esta página
Interfaces
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".
Inicia sesión para guardar tu progreso