Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Registering packages in Go without cyclic dependency

Tags:

I have a central package that provides several interfaces that other packages are dependent on (let us call one Client). Those other packages, provide several implementations of those first interfaces (UDPClient, TCPClient). I instantiate a Client by calling NewClient in the central package, and it selects and invokes the appropriate client implementation from one of the dependent packages.

This falls apart when I want to tell the central package about those other packages, so it knows what clients it can create. Those dependent client implementations also import the central package, creating a cyclic dependency which Go does not allow.

What's the best way forward? I'd prefer not to mash all those implementations in a single package, and creating a separate registry package seems overkill. Currently I have each implementation register itself with the central package, but this requires that the user knows to import every implementation in every separate binary that makes use of client.

import (     _ udpclient     _ tcpclient     client ) 
like image 785
Matt Joiner Avatar asked Mar 26 '15 05:03

Matt Joiner


People also ask

How do you avoid cyclic dependency in Golang?

To avoid the cyclic dependency, we must introduce an interface in a new package say x. This interface will have all the methods that are in struct A and are accessed by struct B.

How do I run a package in go?

Add the Go install directory to your system's shell path. That way, you'll be able to run your program's executable without specifying where the executable is. Once you've updated the shell path, run the go install command to compile and install the package. Run your application by simply typing its name.


2 Answers

The standard library solves this problem in multiple ways:

1) Without a "Central" Registry

Example of this is the different hash algorithms. The crypto package just defines the Hash interface (the type and its methods). Concrete implementations are in different packages (actually subfolders but doesn't need to be) for example crypto/md5 and crypto/sha256.

When you need a "hasher", you explicitly state which one you want and instantiate that one, e.g.

h1 := md5.New() h2 := sha256.New() 

This is the simplest solution and it also gives you good separation: the hash package does not have to know or worry about implementations.

This is the preferred solution if you know or you can decide which implementation you want prior.

2) With a "Central" Registry

This is basically your proposed solution. Implementations have to register themselves in some way (usually in a package init() function).

An example of this is the image package. The package defines the Image interface and several of its implementations. Different image formats are defined in different packages such as image/gif, image/jpeg and image/png.

The image package has a Decode() function which decodes and returns an Image from the specified io.Reader. Often it is unknown what type of image comes from the reader and so you can't use the decoder algorithm of a specific image format.

In this case if we want the image decoding mechanism to be extensible, a registration is unavoidable. The cleanest to do this is in package init() functions which is triggered by specifying the blank identifier for the package name when importing.

Note that this solution also gives you the possibility to use a specific implementation to decode an image, the concrete implementations also provide the Decode() function, for example png.Decode().


So the best way?

Depends on what your requirements are. If you know or you can decide which implementation you need, go with #1. If you can't decide or you don't know and you need extensibility, go with #2.

...Or go with #3 presented below.

3) Proposing a 3rd Solution: "Custom" Registry

You can still have the convenience of the "central" registry with interface and implementations separated with the expense of "auto-extensibility".

The idea is that you have the interface in package pi. You have implementations in package pa, pb etc.

And you create a package pf which will have the "factory" methods you want, e.g. pf.NewClient(). The pf package can refer to packages pa, pb, pi without creating a circular dependency.

like image 68
icza Avatar answered Sep 29 '22 00:09

icza


Those dependent client implementations also import the central package

They should rely on another package defining interfaces that they need to rely on (and that are implemented by the first central package).

This is usually how an import cycle is broken (and/or using dependency inversion).

You have more options described in "Cyclic dependencies and interfaces in Golang".

go list -f can also help visualizing those import cycles.

like image 28
VonC Avatar answered Sep 29 '22 01:09

VonC