La palabra clave interface declara un conjunto de métodos. Cualquier tipo que tenga esos métodos satisface la interfaz automáticamente. No existe una palabra clave implements ni una lista de interfaces en el tipo, así que un struct escrito años antes de que existiera una interfaz puede satisfacerla igualmente. Desde Go 1.18, una interfaz también puede listar tipos además de métodos, lo que la convierte en una restricción para código genérico (especificación de Go).
Resumen rápido
type Notifier interface { Notify(...) error }declara una interfaz. Cualquier tipo con un métodoNotifyque coincida es unNotifier.- La satisfacción es implícita.
var _ Notifier = (*SlackNotifier)(nil)hace que el compilador lo compruebe al compilar. - Un método con receptor de puntero pertenece a
*T, no aT. Asignar unTfalla conmethod Notify has pointer receiver. - Mantén las interfaces pequeñas y decláralas en el paquete que las usa. Acepta interfaces y devuelve tipos concretos.
anyes un alias deinterface{}y guarda un valor de cualquier tipo. Recupera el valor conv, ok := x.(T)o con un switch de tipos.- Una interfaz solo es
nilcuando tanto su tipo como su valor están sin asignar. Devolver un*ValidationErrornil comoerrorte da unerr != nilque valetrue. - Las interfaces con listas de tipos como
~int64 | ~float64son restricciones. Funcionan en listas de parámetros de tipo y en ningún otro sitio.
¿Cómo se declara una interfaz en Go?
Escribe type, un nombre e interface seguido de las firmas de los métodos. Aquí tienes un sistema de alertas que envía el mismo mensaje a Slack y por email:
example.gogopackage main import ( "context" "fmt" ) type Notifier interface { Notify(ctx context.Context, msg string) error } type SlackNotifier struct { Channel string } func (s SlackNotifier) Notify(ctx context.Context, msg string) error { fmt.Printf("slack #%s: %s\n", s.Channel, msg) return nil } type EmailNotifier struct { To string } func (e EmailNotifier) Notify(ctx context.Context, msg string) error { fmt.Printf("email %s: %s\n", e.To, msg) return nil } func alertAll(ctx context.Context, notifiers []Notifier, msg string) { for _, n := range notifiers { if err := n.Notify(ctx, msg); err != nil { fmt.Println("notify failed:", err) } } } func main() { notifiers := []Notifier{ SlackNotifier{Channel: "oncall"}, EmailNotifier{To: "[email protected]"}, } alertAll(context.Background(), notifiers, "payment API p99 above 2s") }
example.texttextslack #oncall: payment API p99 above 2s email [email protected]: payment API p99 above 2s
alertAll solo conoce el método Notify. Le da igual si está hablando con Slack, con el email o con un cliente de PagerDuty que alguien añada el mes que viene. Cada valor del slice lleva consigo su tipo concreto, y la llamada va al método de ese tipo en tiempo de ejecución.
Una interfaz puede incrustar otras interfaces. Los conjuntos de métodos incrustados se combinan, y así es como la biblioteca estándar construye io.ReadWriter a partir de io.Reader e io.Writer:
example.gogotype ReadWriter interface { Reader Writer }
Un tipo satisface io.ReadWriter cuando tiene tanto Read como Write. *os.File y net.Conn los tienen, y *strings.Reader no, porque no tiene método Write.
¿Cómo implementa un tipo una interfaz en Go?
Teniendo en su conjunto de métodos todos los métodos de la interfaz, con exactamente la misma firma. Go lo llama satisfacción implícita, y es la principal diferencia con Java o C#, donde una clase tiene que nombrar cada interfaz que implementa.
Así los paquetes quedan desacoplados. SlackNotifier no importa el paquete que declara Notifier, y ni siquiera tiene que saber que Notifier existe. Puedes escribir hoy una interfaz que ya satisfacen tipos de la biblioteca estándar o de un paquete de terceros.
Cuando falta un método, el compilador te avisa en la línea donde usas el valor como interfaz:
example.gogotype PagerDutyNotifier struct { RoutingKey string } func (p PagerDutyNotifier) Send(ctx context.Context, msg string) error { return nil } var _ Notifier = PagerDutyNotifier{}
example.texttext./main.go:20:18: cannot use PagerDutyNotifier{} (value of struct type PagerDutyNotifier) as Notifier value in variable declaration: PagerDutyNotifier does not implement Notifier (missing method Notify)
Si el método existe pero la firma es distinta, el error muestra las dos versiones una al lado de la otra:
example.texttext./main.go:16:18: cannot use PagerDutyNotifier{} (value of struct type PagerDutyNotifier) as Notifier value in variable declaration: PagerDutyNotifier does not implement Notifier (wrong type for method Notify) have Notify(string) error want Notify(context.Context, string) error
¿Cómo se comprueba en tiempo de compilación que un tipo implementa una interfaz?
Declara una variable a nivel de paquete con el identificador en blanco:
example.gogovar _ Notifier = (*SlackNotifier)(nil)
La línea no reserva memoria y nunca ejecuta código. Solo le pregunta al compilador si un *SlackNotifier se puede asignar a un Notifier, así que la compilación falla en cuanto alguien renombra o cambia Notify. Sin ella, solo te enteras donde el tipo se usa por primera vez como Notifier, que puede estar en otro paquete. La palabra clave var en Go explica el mismo truco con http.Handler.
¿Por qué un receptor de puntero rompe la satisfacción de una interfaz?
Porque el método pertenece al tipo puntero. Todo tipo tiene un conjunto de métodos, y las reglas son estas (especificación de Go):
- El conjunto de métodos de
Tcontiene los métodos declarados con un receptor de valor(t T). - El conjunto de métodos de
*Tcontiene esos más los métodos declarados con un receptor de puntero(t *T).
Un notificador que agrupa mensajes en lotes tiene que modificarse a sí mismo, así que usa un receptor de puntero:
example.gogotype BatchNotifier struct { mu sync.Mutex pending []string } func (b *BatchNotifier) Notify(ctx context.Context, msg string) error { b.mu.Lock() defer b.mu.Unlock() b.pending = append(b.pending, msg) return nil } func main() { var n Notifier = BatchNotifier{} fmt.Println(n) }
example.texttext./main.go:26:19: cannot use BatchNotifier{} (value of struct type BatchNotifier) as Notifier value in variable declaration: BatchNotifier does not implement Notifier (method Notify has pointer receiver)
La solución es var n Notifier = &BatchNotifier{}. Go rechaza la versión con valor a propósito. Una interfaz que guardara un BatchNotifier guardaría una copia, y Notify añadiría elementos al slice pending de la copia mientras el original nunca cambiaría. Cambiar Notify a un receptor de valor tampoco ayuda. Copiaría el sync.Mutex en cada llamada, y go vet lo señala como Notify passes lock by value.
Un tipo con métodos de receptor de puntero tiene que comprobarse a través de su puntero, como en var _ Notifier = (*BatchNotifier)(nil). La forma con puntero también funciona para tipos con receptor de valor como SlackNotifier, porque un *T tiene todos los métodos de T. Por eso (*T)(nil) es la forma habitual de escribir la comprobación.
¿Cómo se diseñan las interfaces en Go?
El proverbio de Go dice «cuanto más grande es la interfaz, más débil es la abstracción» (Go Proverbs). Las interfaces más usadas de la biblioteca estándar tienen uno o dos métodos:
| Interfaz | Métodos | Implementada por |
|---|---|---|
io.Reader | Read | archivos, conexiones de red, cuerpos HTTP, lectores gzip, bytes.Buffer |
io.Writer | Write | archivos, http.ResponseWriter, hashes, bytes.Buffer |
error | Error | *fs.PathError, *json.SyntaxError, tus propios tipos de error |
fmt.Stringer | String | cualquier cosa que controle cómo la imprime fmt |
http.Handler | ServeHTTP | routers, middleware, http.HandlerFunc |
Una interfaz de un solo método es fácil de satisfacer, así que muchos tipos la satisfacen. io.Copy funciona con cualquier origen y destino porque pide muy poco. Una interfaz Storage de doce métodos suele tener una única implementación real, más un mock que hay que cambiar cada vez que cambia la interfaz. http.Handler funciona igual. Un middleware recibe uno y devuelve otro, así que el middleware en Go de cualquier paquete se apila alrededor de cualquier router.
fmt.Stringer muestra lo poco que necesita hacer un tipo para encajar en la biblioteca estándar. Dale a un enum un método String y todos los verbos de fmt lo usarán:
example.gogotype OrderStatus int const ( Pending OrderStatus = iota Paid Refunded ) func (s OrderStatus) String() string { switch s { case Pending: return "pending" case Paid: return "paid" case Refunded: return "refunded" } return fmt.Sprintf("OrderStatus(%d)", int(s)) } func main() { fmt.Println(Paid) fmt.Printf("order 1001 is %v\n", Refunded) }
example.texttextpaid order 1001 is refunded
¿Dónde se debe declarar una interfaz en Go?
En el paquete que la usa, no en el paquete que la implementa. Como la satisfacción es implícita, el consumidor puede listar solo los métodos que llama.
Un servicio de reembolsos solo necesita cargar un pedido. Declara una interfaz de un solo método para eso y la recibe en su constructor:
example.gogotype orderGetter interface { GetOrder(ctx context.Context, id int64) (Order, error) } type RefundService struct { orders orderGetter } func NewRefundService(orders orderGetter) *RefundService { return &RefundService{orders: orders} } func (s *RefundService) CanRefund(ctx context.Context, id int64) (bool, error) { o, err := s.orders.GetOrder(ctx, id) if err != nil { return false, fmt.Errorf("load order %d: %w", id, err) } return o.Status == "paid", nil }
En producción le pasas el store de Postgres, que tiene otros veinte métodos que el servicio de reembolsos nunca ve. En un test, basta con un map:
example.gogotype fakeOrders map[int64]Order func (f fakeOrders) GetOrder(ctx context.Context, id int64) (Order, error) { o, ok := f[id] if !ok { return Order{}, ErrNotFound } return o, nil } func main() { svc := NewRefundService(fakeOrders{1001: {ID: 1001, Status: "paid"}}) fmt.Println(svc.CanRefund(context.Background(), 1001)) fmt.Println(svc.CanRefund(context.Background(), 1002)) }
example.texttexttrue <nil> false load order 1002: order not found
fakeOrders es un tipo map definido con un método (consulta la palabra clave type en Go), así que el test no necesita ninguna biblioteca de mocks.
¿Qué significa «acepta interfaces, devuelve structs»?
Es la otra mitad de la misma idea. Las funciones reciben interfaces como parámetros, así que quien las llama puede pasar cualquier cosa que encaje. Los constructores devuelven tipos concretos como *RefundService o *PostgresStore, así que quien los llama obtiene todos los métodos y todos los campos exportados, y puede decidir por su cuenta qué interfaz pequeña necesita.
Devolver una interfaz desde un constructor oculta los métodos que la interfaz no lista, y obliga a todos los llamadores a usar la única abstracción que tú elegiste. La principal excepción es error. Las funciones devuelven error y no *ValidationError, y la sección sobre nil de más abajo explica por qué.
¿Qué es any en Go?
any es un alias de interface{}, la interfaz sin métodos. Todo tipo tiene al menos cero métodos, así que una variable de tipo any puede guardar cualquier valor. any se añadió en Go 1.18, y las dos formas de escribirlo son intercambiables (especificación de Go).
any aparece donde el tipo no se conoce hasta el tiempo de ejecución. fmt.Println(a ...any) recibe argumentos de cualquier tipo, y json.Unmarshal sobre un map[string]any produce valores cuyo tipo depende del JSON.
El coste es que el compilador deja de comprobar. Cada lectura de un any necesita una aserción de tipo, y una suposición equivocada se convierte en un error en tiempo de ejecución en lugar de un error de compilación. Cuando una función trabaja con varios tipos pero las mismas operaciones valen para todos, usa un parámetro de tipo. func Sum[T Amount](values []T) T, que se explica en la sección de restricciones más abajo, mantiene la comprobación de tipos completa, mientras que func Sum(values []any) any lleva todas las comprobaciones al tiempo de ejecución.
¿Cómo funcionan las aserciones de tipo y los switches de tipos?
Una aserción de tipo recupera el valor concreto de dentro de una interfaz. La forma con dos valores nunca provoca un panic:
example.gogovar payload map[string]any body := `{"user_id": 42, "email": "[email protected]", "roles": ["admin", "billing"], "manager": null}` if err := json.Unmarshal([]byte(body), &payload); err != nil { log.Fatal(err) } email, ok := payload["email"].(string) fmt.Println(email, ok) id, ok := payload["user_id"].(int) fmt.Println(id, ok) fmt.Printf("%T\n", payload["user_id"])
example.texttext[email protected] true 0 false float64
La segunda aserción falla porque encoding/json decodifica todos los números JSON como float64 cuando el destino es any. Con ok, el fallo solo te da false y el valor cero. La forma con un solo valor, payload["user_id"].(int), provoca un panic:
example.texttextpanic: interface conversion: interface {} is float64, not int
Las aserciones también funcionan con interfaces. r.(io.Writer) pregunta si el valor dentro de r tiene un método Write, y la biblioteca estándar lo usa para buscar comportamiento opcional. io.Copy comprueba si el origen es un io.WriterTo y, si lo es, usa su camino más rápido.
Un switch de tipos ejecuta varias aserciones en una sola sentencia. Viene bien para el mismo payload JSON, donde un valor puede ser de unos pocos tipos:
example.gogofunc describe(v any) string { switch v := v.(type) { case nil: return "null" case float64: return fmt.Sprintf("number %g", v) case string: return fmt.Sprintf("string of %d bytes", len(v)) case []any: return fmt.Sprintf("array of %d items", len(v)) case map[string]any: return fmt.Sprintf("object with %d keys", len(v)) default: return fmt.Sprintf("other %T", v) } } func main() { // payload decoded as above for _, key := range []string{"user_id", "email", "roles", "manager"} { fmt.Println(key+":", describe(payload[key])) } }
example.texttextuser_id: number 42 email: string of 15 bytes roles: array of 2 items manager: null
Dentro de cada caso, v tiene el tipo de ese caso, así que len(v) compila para el string, el slice y el map. Los switches de tipos sobre errores funcionan igual, pero solo ven el error más externo. La palabra clave type en Go muestra ese caso y por qué errors.As suele ser la mejor herramienta para errores envueltos.
¿Por qué mi error nil no es nil en Go?
Un valor de interfaz tiene dos partes, un tipo dinámico y un valor dinámico. Solo es igual a nil cuando las dos están sin asignar. Si guardas un puntero nil en una interfaz, la parte del tipo queda asignada, así que la interfaz no es nil (FAQ de Go).
El problema aparece sobre todo con los tipos de error propios. Aquí la función guarda una variable *ValidationError y la devuelve, se haya asignado o no:
example.gogopackage main import ( "fmt" "strings" ) type ValidationError struct { Field string } func (e *ValidationError) Error() string { return "invalid " + e.Field } func validate(email string) error { var verr *ValidationError if !strings.Contains(email, "@") { verr = &ValidationError{Field: "email"} } return verr } func main() { err := validate("[email protected]") if err != nil { fmt.Printf("validation failed: %v (%T)\n", err, err) return } fmt.Println("ok") }
example.texttextvalidation failed: <nil> (*main.ValidationError)
El email es válido y verr es nil, pero el error devuelto guarda el tipo *main.ValidationError con un puntero nil dentro. err != nil compara la interfaz entera con una interfaz sin tipo, así que vale true. Quien llama rechaza un registro válido, y la línea del log dice <nil>, lo que hace que el bug sea difícil de ver.
La solución es devolver un nil literal en el camino de éxito y no pasar nunca un puntero con tipo por un retorno error:
example.gogofunc validate(email string) error { if !strings.Contains(email, "@") { return &ValidationError{Field: "email"} } return nil }
Declara el tipo de retorno como error, no como *ValidationError, y devuelve nil directamente. Quien necesite el campo lo sigue obteniendo con errors.As, que también encuentra el error después de que se haya envuelto:
example.gogoerr := fmt.Errorf("signup: %w", validate("ana.example.com")) var verr *ValidationError if errors.As(err, &verr) { fmt.Println("bad field:", verr.Field) }
Esto imprime bad field: email. La misma trampa existe con cualquier interfaz. Un var store *PostgresStore que nunca se asigna, pasado donde se espera un orderGetter, también produce una interfaz que no es nil.
El caso contrario es una interfaz nil de verdad, como un campo de struct de tipo Notifier que nadie asignó. Llamar a un método sobre ella provoca un panic con invalid memory address or nil pointer dereference, porque no hay ningún tipo en el que buscar el método. Un constructor que exige la dependencia, como NewRefundService(orders), lo evita.
¿Cómo se usan las interfaces como restricciones genéricas?
Desde Go 1.18, una interfaz puede listar tipos además de métodos. La lista se llama conjunto de tipos, y ~T significa «cualquier tipo cuyo tipo subyacente sea T»:
example.gogotype Cents int64 type Amount interface { ~int64 | ~float64 } func Sum[T Amount](values []T) T { var total T for _, v := range values { total += v } return total } func Dedupe[T comparable](ids []T) []T { seen := make(map[T]struct{}, len(ids)) out := make([]T, 0, len(ids)) for _, id := range ids { if _, ok := seen[id]; ok { continue } seen[id] = struct{}{} out = append(out, id) } return out } func main() { fmt.Println(Sum([]Cents{4999, 1250, 300})) fmt.Println(Dedupe([]string{"u_42", "u_7", "u_42"})) }
example.texttext6549 [u_42 u_7]
Amount permite + porque todos los tipos de su conjunto lo admiten. comparable es una restricción predeclarada para los tipos que funcionan con ==, que es lo que necesita una clave de map. El compilador rechaza un tipo que está fuera del conjunto y nombra el problema:
example.texttext./main.go:27:17: string does not satisfy Amount (string missing in ~int64 | ~float64)
Una interfaz con una lista de tipos solo se puede usar como restricción. Declarar una variable de ese tipo falla:
example.texttext./main.go:10:8: cannot use type Amount outside a type constraint: interface contains type constraints
Así que en Go hay dos clases de interfaces con la misma palabra clave. Las interfaces que solo tienen métodos funcionan en todas partes, como tipos de variables, parámetros y restricciones. Las interfaces con listas de tipos solo aparecen entre corchetes. La introducción a los genéricos del blog de Go describe cómo los conjuntos de tipos unifican las dos.
Dónde encaja LevelUpGo
LevelUpGo enseña Go con ejercicios que ejecutan código Go real en el navegador. Interfaces & Polymorphism trata la satisfacción implícita, los conjuntos de métodos, any, las aserciones de tipo y la composición de interfaces. Interface Design trata las interfaces pequeñas, cómo declararlas donde se usan y cómo evitar interfaces que solo existen para hacer mocks. Go Generics Masterclass profundiza en las restricciones y los conjuntos de tipos. El Training Ground tiene ejercicios cortos e independientes sobre interfaces para practicar fuera de un curso. Para las otras 24 palabras reservadas, consulta Palabras clave de Go: las 25 explicadas.
Preguntas frecuentes
¿Es interface una palabra clave en Go?
Sí. interface es una de las 25 palabras clave reservadas de Go, así que no puedes usarla como nombre de variable ni de función. Aparece en declaraciones de tipos interfaz como type Notifier interface { ... }, en el literal interface{} y en restricciones en línea como [T interface{ ~int | ~string }].
¿Tiene Go una palabra clave implements?
No. Un tipo satisface una interfaz simplemente por tener todos sus métodos con firmas que coinciden. Para que el compilador compruebe que un tipo satisface una interfaz, añade var _ Notifier = (*SlackNotifier)(nil) junto al tipo.
¿Qué diferencia hay entre any e interface{} en Go?
Ninguna. any es un alias predeclarado de interface{}, añadido en Go 1.18, así que los dos son el mismo tipo. El código más reciente usa any porque es más corto, y gofmt -r 'interface{} -> any' puede reescribir el código antiguo.
¿Por qué Go dice "method has pointer receiver"?
El método está declarado sobre *T, así que solo un *T lo tiene. Intentaste usar un valor T como la interfaz. Pasa un puntero, como en &BatchNotifier{}, o cambia el método a un receptor de valor si no modifica el receptor.
¿Por qué mi error no es nil si devolví un puntero nil?
Una interfaz solo es nil cuando tanto su tipo como su valor están sin asignar. Devolver un *ValidationError nil desde una función cuyo tipo de retorno es error asigna el tipo *ValidationError, así que err != nil vale true. Devuelve un nil literal en el camino de éxito en lugar de una variable puntero con tipo.
¿Pueden tener campos las interfaces en Go?
No. Las interfaces solo describen métodos, o conjuntos de tipos cuando se usan como restricciones. Si quien llama necesita un valor, añade un método getter como ID() string, o acepta el struct concreto en lugar de una interfaz.
Fuentes
- The Go Programming Language Specification, Interface types: https://go.dev/ref/spec#Interface_types
- The Go Programming Language Specification, Method sets: https://go.dev/ref/spec#Method_sets
- The Go Programming Language Specification, Type assertions: https://go.dev/ref/spec#Type_assertions
- The Go Programming Language Specification, Type switches: https://go.dev/ref/spec#Type_switches
- Go FAQ, Why is my nil error value not equal to nil?: https://go.dev/doc/faq#nil_error
- The Go Blog, An Introduction To Generics: https://go.dev/blog/intro-generics
- Effective Go, Interfaces and other types: https://go.dev/doc/effective_go#interfaces_and_types
- Go Proverbs: https://go-proverbs.github.io/
- Paquete io: https://pkg.go.dev/io
