En Go, una interface no es una jerarquía de tipos, sino un conjunto de firmas de métodos que definen un comportamiento. A diferencia de lenguajes como Java o C#, donde debes declarar explícitamente que una clase implementa una interfaz mediante la palabra clave implements, en Go la implementación es implícita. Este mecanismo se conoce como tipado estructural o static duck typing: si un tipo tiene los métodos que una interface exige, con la misma firma, entonces se considera que la satisface automáticamente.
Esta decisión de diseño permite un desacoplamiento total entre el productor y el consumidor. En la mayoría de los lenguajes, el productor (la clase) debe conocer la interfaz para implementarla. En Go, es el consumidor quien define la interfaz que necesita para trabajar. Si tú escribes una función que requiere una capacidad de lectura, defines una interfaz con un método Read y listo; cualquier tipo que tenga ese método —ya sea un archivo, un socket o un buffer en memoria— será aceptable, sin que el creador de ese tipo haya tenido que saber siquiera que tu interfaz existe. Úsalo siempre que quieras diseñar componentes que no dependan de implementaciones concretas, permitiendo que el código sea extensible y testeable mediante mocks. Si fallas al definir la firma de un método, como cambiar un parámetro de []byte a string, el compilador detectará que el contrato se ha roto y el tipo dejará de ser compatible con la interfaz en el momento de la asignación.
package main
import (
"fmt"
"io"
"strings"
)
// Processor es nuestro "consumidor". No le importa el tipo concreto,
// solo le importa que lo que reciba cumpla con el contrato de io.Reader.
type Processor struct{}
// Consume define la lógica de negocio que depende de una abstracción.
func (p Processor) Consume(r io.Reader) error {
// io.ReadAll es una función de la librería estándar que acepta cualquier io.Reader.
data, err := io.ReadAll(r)
if err != nil {
return err
}
fmt.Printf("Contenido procesado: %s\n", string(data))
return nil
}
// CustomBuffer es un "productor" creado por nosotros.
// No implementa ninguna interfaz de forma explícita, pero la satisface.
type CustomBuffer struct {
content string
offset int
}
// Read implementa el método requerido por io.Reader.
// El tipado estructural hace que CustomBuffer sea un io.Reader automáticamente.
func (b *CustomBuffer) Read(p []byte) (n int, err error) {
if b.offset >= len(b.content) {
return 0, io.EOF // Fin del archivo
}
n = copy(p, b.content[b.offset:])
b.offset += n
return n, nil
}
func main() {
proc := Processor{}
// Caso 1: Usamos un tipo de la librería estándar.
// strings.Reader ya implementa io.Reader internamente.
fmt.Println("--- Caso 1: Standard Library ---")
stdReader := strings.NewReader("Hola desde strings.NewReader")
if err := proc.Consume(stdReader); err != nil {
fmt.Printf("Error: %v\n", err)
}
// Caso 2: Usamos nuestro propio tipo personalizado.
// Aunque CustomBuffer no "sabe" qué es un io.Reader, lo es.
fmt.Println("\n--- Caso 2: Nuestro tipo personalizado ---")
myBuf := &CustomBuffer{content: "Datos desde un buffer custom"}
if err := proc.Consume(myBuf); err != nil {
fmt.Printf("Error: %v\n", err)
}
}
Análisis del ejemplo
En el código anterior, la estructura Processor es la que dicta las reglas. Su método Consume no pide un *CustomBuffer ni un *strings.Reader; pide un io.Reader. Esto es la esencia del desacoplamiento.
Fíjate en CustomBuffer. Para que el compilador la acepte en proc.Consume(myBuf), hemos implementado el método Read(p []byte) (int, error). Si hubiéramos escrito Read(p []byte) int (olvidando el error), el compilador habría arrojado un error en la línea proc.Consume(myBuf), indicando que *CustomBuffer no implementa io.Reader porque la firma no coincide.
Además, es vital notar que CustomBuffer implementa el método con un pointer receiver (*CustomBuffer). Esto significa que el contrato se satisface para el puntero del tipo, no para el valor. En main, por eso pasamos myBuf (que es un puntero *CustomBuffer) y no CustomBuffer{}.
El runtime de Go gestiona esto mediante tablas de métodos internas (itab), que asocian el tipo concreto con la estructura de la interfaz, permitiendo que la llamada a Read sea eficiente a pesar de la abstracción.
El error frecuente
Un error clásico para quienes vienen de otros lenguajes es confundir el receptor del método (valor vs. puntero) al intentar satisfacer una interfaz.
type WrongBuffer struct {
data string
}
// Implementamos el método con un receptor de puntero.
func (b *WrongBuffer) Read(p []byte) (int, error) {
return 0, io.EOF
}
func main() {
// ERROR: WrongBuffer no implementa io.Reader.
// Solo *WrongBuffer implementa io.Reader.
// El compilador dirá: "WrongBuffer does not implement io.Reader (method Read has pointer receiver)"
var _ io.Reader = WrongBuffer{}
}
Al definir el método con (b *WrongBuffer), le estás diciendo al compilador que el método pertenece a la dirección de memoria del objeto. Por lo tanto, un valor directo WrongBuffer{} no tiene ese método disponible en su conjunto de métodos de valor, y la asignación falla.
N° 72