Search code examples
goinheritancepolymorphism

Interface management in Go


I know this has been asked in various forms many times before but I just can't seem to implement what I'm learning in the way that I need. Any help is appreciated.

I have a series of exchanges which all implement roughly the same APIs. For example, each of them have a GetBalance endpoint. However, some have one or two unique things which need to be accessed within the functions. For example, exchange1 needs to use a client when calling it's balance API, while exchange2 requires both the client variable as well as a clientFutures variable. This is an important note for later.

My background is normal OOP. Obviously Go is different in many ways, hence I'm getting tripped up here.

My current implementation and thinking is as follows:

In exchanges module

type Balance struct {
    asset       string
    available   float64
    unavailable float64
    total       float64
}

type Api interface {
    GetBalances() []Balance
}

In Binance module


type BinanceApi struct {
    key           string
    secret        string
    client        *binance.Client
    clientFutures *futures.Client
    Api           exchanges.Api
}

func (v *BinanceApi) GetBalance() []exchanges.Balance {
    // Requires v.client and v.clientFutures
    return []exchanges.Balance{}
}

In Kraken module


type KrakenApi struct {
    key           string
    secret        string
    client        *binance.Client
    Api           exchanges.Api
}

func (v *KrakenApi) GetBalance() []exchanges.Balance {
    // Requires v.client
    return []exchanges.Balance{}
}

In main.go

var exchange *Api

Now my thought was I should be able to call something like exchange.GetBalance() and it would use the GetBalance function from above. I would also need some kind of casting? I'm quite lost here. The exchange could either be Binance or Kraken--that gets decided at runtime. Some other code basically calls a GetExchange function which returns an instance of the required API object (already casted in either BinanceApi/KrakenApi)

I'm aware inheritance and polymorphism don't work like other languages, hence my utter confusion. I'm struggling to know what needs to go where here. Go seems to require loads of annoying code necessary for what other languages do on the fly 😓


Solution

  • using *exchanges.Api is quite weird. You're wanting something that implements a given interface. What the underlying type is (whether it's a pointer or a value receiver) is not important, so use exchanges.Api instead.

    There is another issue, though. In golang, interfaces are implicit (sometimes referred to as duck-type interfaces). Generally speaking, this means that the interface is not declared in the package that implements it, but rather in the package that depends on a given interface. Some say that you should be liberal in terms of what values you return, but restrictive in terms of what arguments you accept. What this boils down to in your case, is that you'd have something like an api package, that looks somewhat like this:

    package api
    
    func NewKraken(args ...any) *KrakenExchange {
       // ...
    }
    
    func NewBinance(args ...any) *BinanceExchange {
    }
    

    then in your other packages, you'd have something like this:

    package kraken // or maybe this could be an exchange package
    
    type API interface {
        GetBalances() []types.Balance
    }
    
    func NewClient(api API, otherArgs ...T) *KrakenClient {
    }
    

    So when someone looks at the code for this Kraken package, they can instantly tell what dependencies are required, and what types it works with. The added benefit is that, should binance or kraken need additional API calls that aren't shared, you can go in and change the specific dependencies/interfaces, without ending up with one massive, centralised interface that is being used all over the place, but each time you only end up using a subset of the interface.

    Yet another benefit of this approach is when writing tests. There are tools like gomock and mockgen, which allow you to quickly generate mocks for unit tests simply by doing this:

    package foo
    
    //go:generate go run github.com/golang/mock/mockgen -destination mocks/dep_mock.go -package mocks your/module/path/to/foo Dependency
    type Dependency interface {
        // methods here
    }
    

    Then run go generate and it'll create a mock object in your/module/path/to/foo/mocks that implements the desired interface. In your unit tests, import he mocks package, and you can do things like:

    ctrl := gomock.NewController(t)
    dep := mocks.NewDependencyMock(ctrl)
    defer ctrl.Finish()
    dep.EXPECT().GetBalances().Times(1).Return(data)
    k := kraken.NewClient(dep)
    bal := k.Balances()
    require.EqualValues(t, bal, data)
    

    TL;DR

    The gist of it is:

    • Interfaces are interfaces, don't use pointers to interfaces
    • Declare interfaces in the package that depends on them (ie the user), not the implementation (provider) side.
    • Only declare methods in an interface if you are genuinely using them in a given package. Using a central, overarching interface makes this harder to do.
    • Having the dependency interface declared along side the user makes for self-documenting code
    • Unit testing and mocking/stubbing is a lot easier to do, and to automate this way