Interfícies a Go: petites, implícites i més simples del que semblen
Interfícies implícites a Go: composició, testing, desacoblament i errors habituals de disseny. Pensa des del consumidor.

A Java, declares les interfícies per endavant. Crees el contracte abans d’escriure una sola línia d’implementació. A Go, les interfícies es descobreixen després d’escriure el codi. Aquesta diferència no és un detall de sintaxi. És una forma completament diferent de pensar el disseny de programari.
Vinc de treballar amb Kotlin i Java durant anys. Interfícies explícites, implements, contractes formals, injecció de dependències amb frameworks. Tot molt cerimoniós. Quan vaig començar amb Go, les interfícies implícites em van semblar estranyes. Després d’usar-les en producció, em semblen una de les millors decisions de disseny del llenguatge.
Si véns de la JVM o de qualsevol llenguatge amb interfícies explícites, aquest article canviarà la teva forma de pensar el desacoblament. Perquè a Go, la pregunta no és “quin contracte vull imposar”, sinó “quin comportament necessito consumir”.
Satisfacció implícita: no hi ha keyword implements
La primera diferència que notes venint d’altres llenguatges és que a Go no existeix la paraula implements. Un tipus satisfà una interfície automàticament si té tots els mètodes que la interfície defineix. Res més.
type Speaker interface {
Speak() string
}
type Dog struct {
Name string
}
func (d Dog) Speak() string {
return d.Name + \" says woof!\"
}
type Cat struct {
Name string
}
func (c Cat) Speak() string {
return c.Name + \" says meow!\"
}Dog i Cat satisfan Speaker sense declarar-ho en cap lloc. No hi ha anotació, no hi ha registre, no hi ha relació explícita. El compilador ho verifica quan intentes usar un Dog on s’espera un Speaker:
func MakeNoise(s Speaker) {
fmt.Println(s.Speak())
}
func main() {
MakeNoise(Dog{Name: \"Rex\"}) // funciona
MakeNoise(Cat{Name: \"Misi\"}) // funciona
}Això té implicacions profundes. No necessites que l’autor del tipus sàpiga quines interfícies satisfarà. Pots definir una interfície al teu paquet que un tipus d’un altre paquet (o de la biblioteca estàndard) ja satisfà sense haver estat dissenyat per a això. És un desacoblament que no pots aconseguir amb interfícies explícites.
Pensa-ho així: a Java, si vols que una classe implementi la teva interfície, necessites modificar aquella classe o crear un wrapper. A Go, simplement defineixes la interfície amb els mètodes que necessites i qualsevol tipus que ja tingui aquells mètodes la compleix automàticament.
// Això funciona sense que os.File sàpiga res de la teva interfície
type ReadCloser interface {
Read(p []byte) (n int, err error)
Close() error
}
// *os.File ja té Read i Close, de manera que satisfà ReadCloser
var rc ReadCloser = os.StdoutPer què les interfícies petites guanyen
Si mires la biblioteca estàndard de Go, les interfícies més usades són diminutes. Un mètode. Dos com a màxim. Això no és casualitat. És una decisió de disseny que impregna tot l’ecosistema.
io.Reader i io.Writer
Les dues interfícies més importants de Go tenen un sol mètode cadascuna:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}Un mètode. Això és tot. I tanmateix, tot l’ecosistema d’I/O de Go es construeix sobre aquestes dues interfícies. Fitxers, connexions de xarxa, buffers, compressió, xifratge, cossos HTTP… tot implementa io.Reader, io.Writer o tots dos.
Per què funciona tan bé? Perquè una interfície d’un sol mètode és extremadament fàcil de satisfer. Qualsevol tipus que pugui llegir bytes pot ser un Reader. Qualsevol tipus que pugui escriure bytes pot ser un Writer. La barrera d’entrada és mínima i el valor de la composició és màxim.
fmt.Stringer
type Stringer interface {
String() string
}Si el teu tipus té un mètode String() string, pots usar-lo directament amb fmt.Println, fmt.Sprintf i qualsevol funció de formatació. Sense registrar res, sense heretar de cap classe base.
type Money struct {
Amount int
Currency string
}
func (m Money) String() string {
return fmt.Sprintf(\"%d %s\", m.Amount, m.Currency)
}
func main() {
price := Money{Amount: 42, Currency: \"EUR\"}
fmt.Println(price) // \"42 EUR\"
}error
La interfície error és un altre exemple perfecte:
type error interface {
Error() string
}Un mètode. Qualsevol tipus que tingui Error() string és un error a Go. Pots crear errors personalitzats sense heretar d’una classe base, sense implementar deu mètodes que no necessites:
type NotFoundError struct {
Resource string
ID string
}
func (e NotFoundError) Error() string {
return fmt.Sprintf(\"%s with ID %s not found\", e.Resource, e.ID)
}La lliçó és clara: com més petita és la interfície, més tipus la satisfan, més components pots compondre i més flexible és el teu sistema. Rob Pike ho va dir millor: “The bigger the interface, the weaker the abstraction.”
Composició d’interfícies: combinant peces petites
Go no té herència. No pots estendre una interfície com a Java amb extends. El que tens és composició: inserir interfícies dins d’altres per construir contractes més grans a partir de peces petites.
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// Composició: ReadWriter és un Reader I un Writer
type ReadWriter interface {
Reader
Writer
}
// Composició més àmplia
type ReadWriteCloser interface {
Reader
Writer
Closer
}Això és exactament el que fa la biblioteca estàndard al paquet io. Les interfícies grans es construeixen component interfícies petites. Un tipus que satisfà ReadWriteCloser també satisfà Reader, Writer, Closer, ReadWriter i qualsevol altra combinació. Tot implícitament.
L’avantatge sobre l’herència clàssica és que no hi ha jerarquia rígida. Pots crear les combinacions que necessitis sense dependre d’una taxonomia predefinida:
// Al teu paquet, defineixes exactament el que necessites
type ReadFlusher interface {
io.Reader
Flush() error
}Ningú necessitava haver previst aquesta combinació. Si un tipus té Read i Flush, satisfà ReadFlusher. Punt. Això és el que fa que el disseny a Go sigui tan orgànic: els contractes es descobreixen, no s’imposen.
La interfície buida: interface i any
La interfície buida no defineix cap mètode. Per tant, tots els tipus la satisfan. És l’equivalent de Object a Java o Any a Kotlin:
// Abans de Go 1.18
func Print(v interface{}) {
fmt.Println(v)
}
// Des de Go 1.18, any és un àlies de interface{}
func Print(v any) {
fmt.Println(v)
}Pots passar el que vulguis: un int, un string, un struct, un slice. Tot és any.
Quan usar any
Té sentit en casos molt específics:
- Funcions de logging o depuració: que accepten qualsevol cosa
- Serialització/deserialització genèrica: com
json.Marshal(v any) - Contenidors genèrics: abans que existissin els generics
Quan no usar any
La majoria del temps. Si uses any per evitar pensar en el tipus, estàs perdent el principal avantatge de Go: el sistema de tipus estàtic. Cada vegada que uses any, estàs movent errors del compilador al runtime.
// Malament: perds tota la seguretat de tipus
func Sum(a, b any) any {
// Necessites type assertions, gestió d'errors...
return a.(int) + b.(int) // panic si no són int
}
// Bé: amb generics (Go 1.18+)
func Sum[T int | float64](a, b T) T {
return a + b
}Des de Go 1.18 amb generics, la necessitat de any ha disminuït significativament. Si pots expressar el tipus amb generics, fes-ho. El compilador t’ho agrairà amb errors en temps de compilació en lloc de panics en producció.
Cada ús de
anyhauria de fer-te preguntar: “Puc expressar això amb una interfície específica o amb generics?” Si la resposta és sí, usa això en el seu lloc.
Accepta interfícies, retorna structs
Aquest és el principi de disseny més important per a interfícies a Go i el que més costa interioritzar venint de Java o Kotlin.
La idea és simple: les funcions haurien d’acceptar interfícies com a paràmetres i retornar tipus concrets com a resultat.
// Bé: accepta una interfície, retorna un tipus concret
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
// Malament: retorna una interfície
func NewUserService(repo UserRepository) UserServiceInterface {
return &UserService{repo: repo}
}Per què? Perquè acceptar interfícies dóna flexibilitat al que crida: pot passar qualsevol implementació que satisfaci el contracte. Però retornar structs concrets dóna informació al que rep: sap exactament quin tipus té, pot accedir a tots els seus mètodes (no només els de la interfície) i el compilador pot optimitzar millor.
Retornar interfícies amaga informació sense necessitat. El que rep el valor ha de treballar amb un contracte reduït quan podria tenir accés a tot. Només retorna interfícies quan hi hagi una raó real per amagar la implementació, com en factories on la implementació concreta és un detall intern.
Aquest principi s’aplica especialment a constructors i funcions de creació. Si la teva funció NewX retorna una interfície, estàs afegint una capa d’abstracció que probablement ningú necessita.
Interfícies per a testing: mocking sense frameworks
Aquí és on les interfícies implícites de Go brillen amb més força. A Java o Kotlin, per fer mock d’una dependència necessites frameworks com Mockito, o generar proxies dinàmics, o crear implementacions manuals d’interfícies explícites. A Go, defineixes una interfície amb els mètodes que uses i crees un struct que els implementi. Res més.
Imaginem un servei que obté dades d’una API externa:
// Al teu paquet, defineixes la interfície que necessites
type WeatherClient interface {
GetTemperature(city string) (float64, error)
}
// El teu servei depèn de la interfície, no de la implementació
type AlertService struct {
weather WeatherClient
}
func NewAlertService(w WeatherClient) *AlertService {
return &AlertService{weather: w}
}
func (s *AlertService) CheckHeatAlert(city string) (bool, error) {
temp, err := s.weather.GetTemperature(city)
if err != nil {
return false, fmt.Errorf(\"getting temperature for %s: %w\", city, err)
}
return temp > 40.0, nil
}En producció uses el client real que crida a l’API. En els tests, crees un mock trivial:
type mockWeather struct {
temp float64
err error
}
func (m *mockWeather) GetTemperature(city string) (float64, error) {
return m.temp, m.err
}
func TestCheckHeatAlert_HighTemp(t *testing.T) {
mock := &mockWeather{temp: 42.0}
service := NewAlertService(mock)
alert, err := service.CheckHeatAlert(\"Sevilla\")
if err != nil {
t.Fatalf(\"unexpected error: %v\", err)
}
if !alert {
t.Error(\"expected heat alert for 42 degrees\")
}
}
func TestCheckHeatAlert_LowTemp(t *testing.T) {
mock := &mockWeather{temp: 22.0}
service := NewAlertService(mock)
alert, err := service.CheckHeatAlert(\"Santiago\")
if err != nil {
t.Fatalf(\"unexpected error: %v\", err)
}
if alert {
t.Error(\"did not expect heat alert for 22 degrees\")
}
}
func TestCheckHeatAlert_Error(t *testing.T) {
mock := &mockWeather{err: fmt.Errorf(\"API down\")}
service := NewAlertService(mock)
_, err := service.CheckHeatAlert(\"Madrid\")
if err == nil {
t.Error(\"expected error when API fails\")
}
}Sense Mockito. Sense reflexió. Sense generació de codi. Un struct amb els mètodes que necessites. El compilador verifica que el teu mock satisfà la interfície. Si demà la interfície canvia, el teu mock no compila i ho saps immediatament.
La clau és que la interfície la defineix el consumidor, no el proveïdor. El servei AlertService defineix què necessita (WeatherClient) i qualsevol tipus que satisfaci aquells mètodes serveix. El client HTTP real no sap que existeix aquella interfície. No li cal.
Per aprofundir en patrons de test amb interfícies, mira el que explico a testing a Go.
Errors comuns: interfícies prematures, grasses i contaminació
Després de veure molt codi Go d’equips que venen de Java o C#, hi ha tres patrons que es repeteixen i que hauries d’evitar.
1. Interfícies prematures
L’error més freqüent és crear la interfície abans de tenir una segona implementació.
// Malament: defineixes la interfície abans de necessitar-la
type UserRepository interface {
GetByID(id int) (*User, error)
Create(user *User) error
Update(user *User) error
Delete(id int) error
List(filter Filter) ([]*User, error)
Count(filter Filter) (int, error)
}
// I llavors només tens una implementació
type PostgresUserRepository struct { ... }Si només tens una implementació, no necessites la interfície encara. Espera que aparegui la necessitat real: un segon storage, un mock per a tests, una caché que embolcalla el repositori. Llavors extreu la interfície amb els mètodes que realment necessites.
L’excepció: si ja saps que necessitaràs mocks per a testing, defineix la interfície des del principi. Però defineix només els mètodes que el teu consumidor necessita, no tots els que el repositori ofereix.
2. Interfícies grasses (fat interfaces)
Directament relacionat amb l’anterior. Una interfície amb deu mètodes és un senyal d’alerta:
// Malament: interfície massa gran
type DataStore interface {
GetUser(id int) (*User, error)
CreateUser(user *User) error
UpdateUser(user *User) error
DeleteUser(id int) error
GetOrder(id int) (*Order, error)
CreateOrder(order *Order) error
UpdateOrder(order *Order) error
DeleteOrder(id int) error
GetProduct(id int) (*Product, error)
CreateProduct(product *Product) error
// ... i continua
}Això és el que passa quan penses la interfície des del proveïdor (“què pot fer el meu store”) en lloc de des del consumidor (“què necessita aquest handler”).
La solució: interfícies segregades, definides on es consumeixen.
// Bé: cada consumidor defineix el que necessita
type UserGetter interface {
GetUser(id int) (*User, error)
}
type UserCreator interface {
CreateUser(user *User) error
}
// El teu handler només depèn del que usa
type UserHandler struct {
getter UserGetter
creator UserCreator
}Si el teu struct de base de dades implementa GetUser i CreateUser, satisfà ambdues interfícies implícitament. No necessites dividir la implementació. Només divideixes el contracte en el punt de consum.
3. Contaminació d’interfícies (interface pollution)
Això passa quan crees una interfície per a cada struct, “per si de cas”. És un antipatró molt comú en equips que venen de Java on Spring t’obliga a tenir un Service i un ServiceImpl per a tot.
// Malament: una interfície per cada struct, sense justificació
type UserService interface {
GetUser(id int) (*User, error)
}
type userServiceImpl struct { ... }
// I només hi ha una implementació, usada en un sol llocA Go, si no necessites polimorfisme ni testing amb mocks, no necessites la interfície. Usa el tipus concret directament. Pots extreure la interfície més tard sense canviar la implementació, gràcies a la satisfacció implícita.
La regla d’or: no dissenyes interfícies, les descobreixes. Quan tinguis dues implementacions o necessitis un mock, aquell és el moment d’extreure la interfície. No abans.
Exemple real: dissenyant interfícies per a un servei backend
Vegem un cas pràctic. Tens un servei de gestió de tasques amb una API REST. Necessites accés a base de dades, un sistema de notificacions i logging. Anem a dissenyar les interfícies pensant des del consumidor.
El handler defineix el que necessita
package task
import (
\"context\"
\"time\"
)
type Task struct {
ID string
Title string
Description string
Status string
AssignedTo string
CreatedAt time.Time
UpdatedAt time.Time
}
// El handler de creació necessita guardar i notificar
type TaskCreator interface {
Create(ctx context.Context, task *Task) error
}
type Notifier interface {
Notify(ctx context.Context, userID string, message string) error
}El handler
type CreateHandler struct {
store TaskCreator
notifier Notifier
}
func NewCreateHandler(store TaskCreator, notifier Notifier) *CreateHandler {
return &CreateHandler{store: store, notifier: notifier}
}
func (h *CreateHandler) Handle(ctx context.Context, task *Task) error {
if task.Title == \"\" {
return fmt.Errorf(\"task title cannot be empty\")
}
task.ID = generateID()
task.Status = \"pending\"
task.CreatedAt = time.Now()
task.UpdatedAt = task.CreatedAt
if err := h.store.Create(ctx, task); err != nil {
return fmt.Errorf(\"storing task: %w\", err)
}
if task.AssignedTo != \"\" {
msg := fmt.Sprintf(\"New task assigned: %s\", task.Title)
if err := h.notifier.Notify(ctx, task.AssignedTo, msg); err != nil {
// Registrem però no fallem: la tasca ja està creada
log.Printf(\"failed to notify user %s: %v\", task.AssignedTo, err)
}
}
return nil
}Les implementacions reals
// PostgreSQL store - satisfà TaskCreator implícitament
type PostgresStore struct {
db *sql.DB
}
func (s *PostgresStore) Create(ctx context.Context, task *Task) error {
query := `INSERT INTO tasks (id, title, description, status, assigned_to, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7)`
_, err := s.db.ExecContext(ctx, query,
task.ID, task.Title, task.Description, task.Status,
task.AssignedTo, task.CreatedAt, task.UpdatedAt)
return err
}
// Email notifier - satisfà Notifier implícitament
type EmailNotifier struct {
smtpAddr string
}
func (n *EmailNotifier) Notify(ctx context.Context, userID string, message string) error {
// Enviar email real...
return nil
}Els tests
type mockStore struct {
tasks []*Task
err error
}
func (m *mockStore) Create(_ context.Context, task *Task) error {
if m.err != nil {
return m.err
}
m.tasks = append(m.tasks, task)
return nil
}
type mockNotifier struct {
notifications []string
err error
}
func (m *mockNotifier) Notify(_ context.Context, userID string, message string) error {
if m.err != nil {
return m.err
}
m.notifications = append(m.notifications, fmt.Sprintf(\"%s: %s\", userID, message))
return nil
}
func TestCreateHandler_Success(t *testing.T) {
store := &mockStore{}
notifier := &mockNotifier{}
handler := NewCreateHandler(store, notifier)
task := &Task{Title: \"Deploy v2\", AssignedTo: \"user-123\"}
err := handler.Handle(context.Background(), task)
if err != nil {
t.Fatalf(\"unexpected error: %v\", err)
}
if len(store.tasks) != 1 {
t.Errorf(\"expected 1 stored task, got %d\", len(store.tasks))
}
if len(notifier.notifications) != 1 {
t.Errorf(\"expected 1 notification, got %d\", len(notifier.notifications))
}
if task.Status != \"pending\" {
t.Errorf(\"expected status pending, got %s\", task.Status)
}
}
func TestCreateHandler_EmptyTitle(t *testing.T) {
handler := NewCreateHandler(&mockStore{}, &mockNotifier{})
err := handler.Handle(context.Background(), &Task{})
if err == nil {
t.Error(\"expected error for empty title\")
}
}
func TestCreateHandler_StoreError(t *testing.T) {
store := &mockStore{err: fmt.Errorf(\"connection refused\")}
handler := NewCreateHandler(store, &mockNotifier{})
err := handler.Handle(context.Background(), &Task{Title: \"Test\"})
if err == nil {
t.Error(\"expected error when store fails\")
}
}Fixa’t en com cada peça és independent. El handler no sap si el store és PostgreSQL, MongoDB o un map en memòria. No sap si el notifier envia emails, push notifications o missatges a Slack. Només sap quins mètodes necessita. Això és arquitectura neta a Go a la pràctica, i les interfícies implícites ho fan possible sense cerimònies.
Interfícies vs generics: quan usar cadascun
Des de Go 1.18, els generics estan disponibles i hi ha confusió sobre quan usar interfícies i quan generics. La resposta és més simple del que sembla: resolen problemes diferents.
Interfícies: comportament
Usa interfícies quan t’importa què pot fer un tipus:
// M'importa que pugui llegir bytes
type Reader interface {
Read(p []byte) (n int, err error)
}
// M'importa que es pugui guardar
type Saver interface {
Save(ctx context.Context) error
}Generics: tipus
Usa generics quan necessites operar sobre múltiples tipus amb la mateixa lògica:
// Necessito trobar un element en un slice, sigui quin sigui el tipus
func Contains[T comparable](slice []T, target T) bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}
// Necessito un map concurrent que funcioni amb qualsevol clau i valor
type SafeMap[K comparable, V any] struct {
mu sync.RWMutex
m map[K]V
}La línia divisòria
| Situació | Usa |
|---|---|
| Necessites polimorfisme en runtime | Interfícies |
| Necessites la mateixa lògica per a tipus diferents | Generics |
| Necessites desacoblament per a testing | Interfícies |
| Necessites col·leccions type-safe | Generics |
| Necessites injecció de dependències | Interfícies |
| Necessites algorismes genèrics (sort, filter, map) | Generics |
Pots combinar tots dos. Les constraint interfaces de generics són interfícies que defineixen tant mètodes com tipus permesos:
type Number interface {
~int | ~int64 | ~float64
}
func Sum[T Number](values []T) T {
var total T
for _, v := range values {
total += v
}
return total
}El consell pràctic: comença amb tipus concrets. Si necessites polimorfisme o desacoblament, extreu una interfície. Si necessites reutilitzar lògica per a tipus diferents, usa generics. No comencis per l’abstracció. Per a més detalls sobre generics, mira generics a Go.
Comparació amb interfícies a Java i Kotlin
Si véns de Java o Kotlin, hi ha diferències conceptuals que canvien la forma de dissenyar.
Java: contractes explícits i rígids
// Java: la interfície es defineix primer
public interface UserRepository {
User findById(int id);
void save(User user);
void delete(int id);
List<User> findAll();
}
// La implementació declara explícitament que la compleix
public class PostgresUserRepository implements UserRepository {
@Override
public User findById(int id) { ... }
@Override
public void save(User user) { ... }
@Override
public void delete(int id) { ... }
@Override
public List<User> findAll() { ... }
}El problema: si només necessites findById en un punt del codi, continues arrossegant els quatre mètodes. Per segregar, necessites crear noves interfícies i que la implementació les declari explícitament. Això desincentiva la segregació perquè cada nova interfície requereix canvis a la classe que implementa.
Kotlin: millor que Java, mateix model
// Kotlin: més net, mateix model
interface UserRepository {
fun findById(id: Int): User?
fun save(user: User)
}
class PostgresUserRepository : UserRepository {
override fun findById(id: Int): User? { ... }
override fun save(user: User) { ... }
}Kotlin millora l’ergonomia amb propietats en interfícies, mètodes per defecte i delegació. Però el model continua sent explícit: la classe ha de declarar quines interfícies implementa.
Go: contractes implícits i descoberts
// Go: defineixes la interfície on la necessites
type UserFinder interface {
FindByID(ctx context.Context, id int) (*User, error)
}
// PostgresStore ja té FindByID, de manera que satisfà UserFinder
// sense necessitat de modificar res
type PostgresStore struct {
db *sql.DB
}
func (s *PostgresStore) FindByID(ctx context.Context, id int) (*User, error) {
// ...
}
func (s *PostgresStore) Save(ctx context.Context, user *User) error {
// ...
}
func (s *PostgresStore) Delete(ctx context.Context, id int) error {
// ...
}La diferència clau: a Go cada consumidor pot definir la seva pròpia interfície amb només els mètodes que necessita. Un handler que només llegeix usuaris depèn de UserFinder. Un altre handler que crea usuaris depèn de UserCreator. Ambdues interfícies són satisfetes pel mateix PostgresStore sense que aquest sàpiga que existeixen.
Taula comparativa
| Aspecte | Java | Kotlin | Go |
|---|---|---|---|
| Satisfacció | Explícita (implements) | Explícita (:) | Implícita |
| Definició | El proveïdor decideix | El proveïdor decideix | El consumidor decideix |
| Segregació | Requereix canvis en la implementació | Requereix canvis en la implementació | Sense canvis en la implementació |
| Herència d’interfícies | extends | : | Composició (embedding) |
| Mètodes per defecte | Sí (Java 8+) | Sí | No |
| Interfícies funcionals | Sí (@FunctionalInterface) | Sí (lambdas) | No necessàries (les funcions són ciutadanes de primera classe) |
| Camps en interfícies | No | Sí (propietats) | No |
El model de Go és més restrictiu en features (no hi ha mètodes per defecte, no hi ha camps) però més flexible en composició i desacoblament. No pots fer tot el que fas amb interfícies a Kotlin, però el que pots fer és més simple i més desacoblat.
Principis per dissenyar interfícies a Go
Després de tot l’anterior, aquests són els principis que segueixo en producció:
1. Defineix interfícies on es consumeixen, no on s’implementen. El paquet que usa la dependència és el que defineix la interfície. No el paquet que la proveeix.
2. Comença sense interfícies. Usa tipus concrets fins que necessitis desacoblament real. Extreure una interfície després és trivial a Go.
3. Mantén les interfícies petites. Un o dos mètodes és l’ideal. Si en té més de tres, pregunta’t si pots dividir-la.
4. Accepta interfícies, retorna structs. Les teves funcions públiques accepten interfícies per a flexibilitat i retornen tipus concrets per a claredat.
5. Anomena les interfícies pel que fan, no pel que són. Reader, Writer, Closer, Stringer. El sufix -er per a interfícies d’un mètode és una convenció que funciona.
6. No crees interfícies “per si de cas”. Cada interfície afegeix una capa d’indireccions. Si no la necessites avui, no la crees.
7. Usa composició per construir interfícies més grans. Combina interfícies petites en lloc de crear interfícies monolítiques.
Aquests principis s’alineen amb l’estructura de projecte Go que descric en un altre article: paquets petits amb responsabilitats clares i dependències expressades a través d’interfícies definides pel consumidor.
No dissenyes interfícies, les descobreixes
Les interfícies de Go són una d’aquelles coses que semblen massa simples fins que les uses en un projecte real. Sense implements, sense frameworks d’injecció de dependències, sense generació de codi per a mocks. Només un contracte implícit que el compilador verifica.
La clau és canviar la mentalitat. No pensis “quin contracte vull que compleixi la meva implementació”. Pensa “quin comportament necessita la meva funció per fer la seva feina”. Defineix aquell comportament com una interfície d’un o dos mètodes al paquet que el consumeix. Deixa que la satisfacció implícita faci la resta.
Si véns de Java o Kotlin, això et resultarà incòmode les primeres setmanes. No tenir implements explícit es nota com conduir sense cinturó. Però després d’un temps entens que el compilador continua verificant tot, que els tests s’escriuen més ràpid, que el desacoblament és més real i que el codi és més simple.


