英文:
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调用,你可以进去更改特定的依赖项/接口,而不会得到一个被广泛使用但每次只使用接口子集的庞大集中式接口。
这种方法的另一个好处是编写测试时。有一些工具,比如gomock
和mockgen
,可以通过以下方式快速生成用于单元测试的模拟对象:
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
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论