Go中的接口管理

huangapple go评论81阅读模式
英文:

Interface management in Go

问题

我知道这个问题以各种形式被问过很多次,但我似乎无法按照我所学的方式实现它。任何帮助都将不胜感激。

我有一系列的交易所,它们都实现了大致相同的API。例如,它们中的每一个都有一个GetBalance的端点。然而,有一些独特的东西需要在函数中访问。例如,exchange1在调用其余额API时需要使用一个client,而exchange2则需要使用client变量和clientFutures变量。这是一个后面重要的说明。

我的背景是普通的面向对象编程。显然,Go在很多方面都不同,因此我在这里遇到了困难。

我目前的实现和思路如下:

exchanges模块中:

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

type Api interface {
	GetBalances() []Balance
}

Binance模块中:

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

func (v *BinanceApi) GetBalance() []exchanges.Balance {
    // 需要 v.client 和 v.clientFutures
	return []exchanges.Balance{}
}

Kraken模块中:

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

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

main.go中:

var exchange *Api

现在我的想法是,我应该能够调用类似exchange.GetBalance()的东西,并且它将使用上面的GetBalance函数。我还需要进行某种类型的转换吗?我非常迷茫。exchange可以是Binance或Kraken,这在运行时决定。一些其他的代码基本上调用一个GetExchange函数,该函数返回所需API对象的实例(已经在BinanceApi/KrakenApi中进行了转换)。

我知道继承和多态不像其他语言那样工作,因此我非常困惑。我很难知道应该把什么放在哪里。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 😓

答案1

得分: 2

使用*exchanges.Api是相当奇怪的。你想要的是实现给定接口的东西。底层类型(无论是指针还是值接收器)并不重要,所以应该使用exchanges.Api

然而,还有另一个问题。在Go语言中,接口是隐式的(有时被称为鸭子类型接口)。一般来说,这意味着接口不是在实现它的包中声明的,而是在依赖于给定接口的包中声明的。有人说你应该在返回值方面宽松一些,但在接受参数方面要严格一些。在你的情况下,这意味着你会有一个类似这样的api包:

package api

func NewKraken(args ...any) *KrakenExchange {
   // ...
}

func NewBinance(args ...any) *BinanceExchange {
}

然后在其他包中,你会有类似这样的代码:

package kraken // 或者这可能是一个exchange包

type API interface {
    GetBalances() []types.Balance
}

func NewClient(api API, otherArgs ...T) *KrakenClient {
}

这样,当有人查看这个Kraken包的代码时,他们可以立即知道需要哪些依赖项以及它使用的类型。另一个好处是,如果Binance或Kraken需要额外的不共享的API调用,你可以进去更改特定的依赖项/接口,而不会得到一个被广泛使用但每次只使用接口子集的庞大集中式接口。

这种方法的另一个好处是编写测试时。有一些工具,比如gomockmockgen,可以通过以下方式快速生成用于单元测试的模拟对象:

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
}

然后运行go generate,它将在your/module/path/to/foo/mocks中创建一个实现所需接口的模拟对象。在单元测试中,导入mocks包,你可以做如下操作:

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)

简而言之

要点是:

  • 接口就是接口,不要使用指向接口的指针。
  • 在依赖于接口的包(即用户)中声明接口,而不是在实现(提供者)的一侧声明。
  • 只有在给定包中真正使用的情况下才在接口中声明方法。使用一个中心化的总体接口会使这一点变得更加困难。
  • 将依赖接口与用户声明在一起可以使代码自我说明。
  • 使用这种方式进行单元测试、模拟和存根更加容易,也更容易自动化。
英文:

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

huangapple
  • 本文由 发表于 2022年11月15日 18:09:26
  • 转载请务必保留本文链接:https://go.coder-hub.com/74443828.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定