goarchitecture

Interfaces and errors in go


I have a question about using interfaces when separating business and other layers (such as the database layer in my case). I’ve heard that it is advisable to define interfaces in the same place where they are used.

In my application, I have a RegistryService that encapsulates the logic for registering and deregistering workers in my application cluster. I also have a Memberlist structure that stores my models in a linked list.

In my implementation of RegistryService, I declare a workerProvider interface with methods like SaveWorker, DeleteWorkerByID, etc. These methods return errors. Currently, these errors are defined in the memberlist package. However, I want to abstract these errors so that they are not tightly coupled to the memberlist implementation.

Should I move the workerProvider interface to a separate package and define new error types in it?

    package registry
    
    import ( ... )
    var (
        ErrWorkerAlreadyRegistered = errors.New("worker is already registered")
    )
    
    type workersProvider interface {
        SaveWorker(ctx context.Context, endpoint string, zone string) (domain.Worker, error)
        DeleteWorkerByID(ctx context.Context, id uuid.UUID) (domain.Worker, error)
        ListWorkers(ctx context.Context) ([]domain.Worker, error)
        FindWorkerByID(ctx context.Context, id uuid.UUID) (domain.Worker, error)
    }
    
    type RegistryService struct {
        log *slog.Logger
        wp  workersProvider
    }
    
    func NewRegistryService(log *slog.Logger, wp workersProvider) *RegistryService {
        return &RegistryService{
            log: log,
            wp:  wp,
        }
    }
    
    func (reg *RegistryService) RegisterWorker(ctx context.Context, endpoint string, zone string) (domain.Worker, error) {
        worker, err := reg.wp.SaveWorker(ctx, endpoint, zone)
        if err != nil {
            emptyWorker := domain.Worker{}
            switch {
            case errors.Is(err, memberlist.ErrWorkerAlreadyExists):
                return emptyWorker, ErrWorkerAlreadyRegistered
            default:
                return emptyWorker, lib.WrapError(op, err)
            }
        }
    
        return worker, nil
    }
    package memberlist
    
    import (...)
    
    var (
            ...
        ErrWorkerAlreadyExists = errors.New("worker already exists")
            ...
    )
    
    type Memberlist struct { ... }
    
    func New() *Memberlist { ... }
    
    func (mlist *Memberlist) SaveWorker(_ context.Context, endpoint string, zone string) (domain.Worker, error) {
      ...
      return domain.Worker{}, ErrWorkerAlreadyExists
      ...
    }
    
    
    func (reg *Memberlist) DeleteWorkerByID(_ context.Context, id uuid.UUID) (domain.Worker, error) { ... }
    
    func (reg *Memberlist) ListWorkers(_ context.Context) ([]domain.Worker, error) { ... }
    
    func (reg *Memberlist) FindWorkerByID(_ context.Context, id uuid.UUID) (domain.Worker, error) { ... }

Solution

  • I was always encouraged to try out my ideas and it made me learn a lot of things, so my suggestion is that you first write tests before making changes. Then you can create your package and see if it fits the design and future goals that you have in mind.

    But also ask yourself what problems are you facing if the errors are coupled with memberlist? Does it prevent you from implementing some new feature ? Is the current design too complicated to work with ? These questions will help you take a better decision for yourself