Una interfaz es un conjunto de métodos
Un tipo interfaz enumera firmas de métodos. Cualquier tipo que tenga esos métodos cumple la interfaz, sin ninguna declaración que lo diga.
Ni Rect ni Circle mencionan Shape. Eso es la implementación implícita, el rasgo que define a las interfaces de Go. Significa que puedes definir en tu paquete una interfaz que los tipos de otros paquetes ya cumplen, sin tocar su código.
Interfaces pequeñas de la librería estándar
El código Go prefiere interfaces con uno o dos métodos. Las más importantes:
| Interfaz | Método | Quién la usa |
|---|---|---|
fmt.Stringer | String() string | la impresión de fmt |
error | Error() string | cualquier función que pueda fallar |
io.Reader | Read(p []byte) (n int, err error) | archivos, red, gzip, bodies HTTP |
io.Writer | Write(p []byte) (n int, err error) | archivos, buffers, hashes, respuestas HTTP |
sort.Interface | Len, Less, Swap | el paquete sort |
http.Handler | ServeHTTP(w, r) | net/http |
Como io.Reader tiene un solo método, decenas de tipos lo implementan, y cualquier función que recibe un io.Reader funciona con todos ellos:
El proverbio de Go dice "cuanto más grande la interfaz, más débil la abstracción". Las interfaces grandes se construyen combinando pequeñas: io.ReadWriter es Reader más Writer, escrita incrustando una interfaz en otra.
any: la interfaz vacía
interface{} no tiene métodos, así que todos los tipos la cumplen. Go 1.18 añadió any como alias; son idénticas.
Un valor any puede contener cualquier cosa, pero no puedes hacer casi nada con él hasta que recuperas el tipo concreto con una aserción de tipo o un type switch. Prefiere una interfaz real o genéricos cuando el conjunto de tipos es conocido. any encaja con datos realmente dinámicos, como JSON decodificado de forma desconocida, y con la impresión.
Qué contiene un valor de interfaz
Un valor de interfaz es un par: un tipo dinámico y un valor dinámico. var s Shape = Rect{3, 4} guarda el tipo Rect y una copia del valor. Llamar a s.Area() busca el método de Rect en tiempo de ejecución.
Una interfaz es nil solo cuando las dos partes están vacías. Esa regla provoca el bug más desconcertante de Go.
La trampa de la interfaz nil
Un puntero nil guardado en una interfaz produce una interfaz que no es nil.
Salida:
false
*main.MyError true
true
validate(true) devuelve una interfaz error que contiene el tipo *MyError y el valor nil. La interfaz tiene un tipo, así que no es igual a nil, y se ejecuta la rama if err != nil de quien llama. Llamar ahí a err.Error() provocaría entonces un panic al acceder a un campo del receptor nil.
El arreglo es sencillo: declara la variable como error, no como el tipo puntero concreto, o devuelve un nil literal en la ruta de éxito. Nunca devuelvas un tipo puntero de error concreto desde una función cuyo resultado es error. La misma trampa se aplica a cualquier interfaz, no solo a los errores.
Comprobar que un tipo implementa una interfaz
La implementación se comprueba donde un valor se asigna a una interfaz. Si ningún código lo hace todavía, un error en la firma de un método pasa desapercibido. Una asignación vacía a nivel de paquete hace explícita la comprobación:
var _ io.Writer = (*LogWriter)(nil)
var _ fmt.Stringer = Temp(0)
No cuesta nada en tiempo de ejecución. Si *LogWriter tiene Write(p []byte) error en lugar de Write(p []byte) (int, error), la compilación falla:
cannot use (*LogWriter)(nil) (value of type *LogWriter) as io.Writer value in variable declaration: *LogWriter does not implement io.Writer (wrong type for method Write)
have Write([]byte) error
want Write([]byte) (int, error)
Receptores puntero e interfaces
Si un método tiene un receptor puntero, solo el tipo puntero tiene ese método. *Counter cumple una interfaz gracias a él; Counter no. El compilador dice Counter does not implement Incrementer (method Inc has pointer receiver). Guarda &Counter{} en la interfaz. La página de métodos explica los conjuntos de métodos.
Acepta interfaces, devuelve structs
Una pauta habitual en Go: las funciones deberían recibir parámetros de tipo interfaz y devolver tipos concretos.
- Aceptar una interfaz permite a quien llama pasar cualquier cosa que encaje, incluidos los dobles de test. Una función que lee datos debería recibir un
io.Reader, no un*os.File. - Devolver un tipo concreto permite a quien llama usar todos sus métodos y campos, y evita la trampa de la interfaz nil.
os.Opendevuelve*os.File, noio.Reader.
Un hábito relacionado: define las interfaces donde se usan, no donde se implementan. Si tu servicio necesita algo que pueda hacer Get(id) de un usuario, declara una interfaz de un método en el paquete de tu servicio, y deja que el paquete de la base de datos simplemente exporte su struct.
Comparar valores de interfaz
Dos valores de interfaz son iguales cuando sus tipos dinámicos son idénticos y sus valores dinámicos son iguales. Si el tipo dinámico no es comparable (un slice, un map), == compila pero provoca un panic en tiempo de ejecución: runtime error: comparing uncomparable type []int.
Errores comunes
- Devolver un puntero nil con tipo como interfaz. Devuelve un
nilliteral. - Interfaces demasiado pronto. Escribe primero el tipo concreto. Añade una interfaz cuando la necesite una segunda implementación o un test.
- Puntero a interfaz.
*io.Readercasi nunca es correcto. Una interfaz ya contiene un puntero cuando guardas uno en ella. - Interfaces grandes. Las interfaces de diez métodos son difíciles de implementar y de simular. Divídelas.
Preguntas frecuentes
¿Cómo se implementa una interfaz en Go?
Define en tu tipo los métodos que enumera la interfaz, con los mismos nombres y firmas. No existe la palabra clave implements. Si *File tiene Read(p []byte) (int, error), es un io.Reader, automáticamente. El compilador lo comprueba allí donde asignas el valor al tipo interfaz.
¿Qué es la interfaz vacía o any en Go?
interface{} no tiene métodos, así que todos los tipos la cumplen. Desde Go 1.18, any es un alias integrado de interface{}. Un valor de tipo any puede contener cualquier cosa, pero necesitas una aserción de tipo o un type switch para recuperar un tipo concreto.
¿Por qué mi interfaz de Go no es nil si le asigné un puntero nil?
Un valor de interfaz contiene un tipo y un valor. Asignar un *MyError nil a un error da una interfaz cuyo tipo es *MyError y cuyo valor es nil, y esa interfaz no es igual a nil. Devuelve un nil literal en lugar de un puntero nil con tipo cuando no haya error.
¿Cómo compruebo en tiempo de compilación que un tipo implementa una interfaz?
Añade una asignación vacía a nivel de paquete: var _ io.Reader = (*MyReader)(nil). Si a *MyReader le falta un método, la compilación falla con un mensaje que nombra el método que falta.