Golang库包中实现"接口与结构体嵌入"的方法
Golang库包中实现"接口与结构体嵌入"的方法 我有一个库包,用于调用Web服务以验证凭据。它基本上是这样的:
package myauthsvc
// 客户端对象
type Client struct {
AccessToken string
Secret string
}
func (c *Client) Authenticate(userID string, secret string) (bool, error) {
// 执行一些HTTP调用,返回结果
return false, nil
}
它工作正常,但显然难以模拟,也不符合Go语言的惯用法。对于包之间使用接口,我比较清楚。但当涉及到库包时,特别是结构体嵌入,我就感到困惑了。
如果我想改进我的实现,下面这样对吗?
package myauthsvc
type ClientInterface interface {
Authenticate(userID string, secret string) (bool, error)
}
type Client struct {
AccessToken string
Secret string
ClientInterface
}
func (c *Client) Authenticate(userID string, secret string) (bool, error) {
// 执行一些操作
return false, nil
}
func NewClient(token string, secret string, ci ClientInterface) *Client {
return &Client{token, secret, ci}
}
首先,NewClient 方法定义在包中,但在类型/接口之外。
其次,Authenticate 方法的接收器类型是 Client。我认为这正是我理解不足的地方。是否有什么隐含的机制意味着 Client “符合” ClientInterface,并且选择器的简写导致 Client.Authenticate == Client.ClientInterface.Authenticate?
最后,如果我的库包是自包含的,但我希望让其他开发者能够轻松模拟这些方法,那么实现应该是什么样子?
package main
import (
"github.com/thisdougb/myauthsvc" // 这个仓库只是一个示例,并不真实存在
)
func test() {
c := myauthsvc.NewClient("userid", "secret", ci) // <- ci 从何而来?
}
在创建新客户端时,我该如何创建 ci?
我已经阅读了 solid-go-design 以及 type-embedding
感谢帮助。
更多关于Golang库包中实现"接口与结构体嵌入"的方法的实战教程也可以访问 https://www.itying.com/category-94-b0.html
是的,那些内容值得一读。谢谢。
我想最明确的观点是:“在使用的点定义接口。” 这与我之前尝试的做法正好相反。
所以我的外部库包变成了:
// 外部库包
package myauthsvc
type Client struct {
AccessToken string
Secret string
}
func (c Client) Authenticate(userID string, secret string) (bool, error) {
// 使用 c.AccessToken 和 c.Secret 做一些操作
return false
}
我的主应用现在看起来像这样:
// handler.go
package handler
// 不导入 myauthsvc
type Authenticator interface { // 在使用的点定义
Authenticate(userID string, secret string) (bool, error)
}
func auth(userID string, secret string, auth Authenticator) bool {
return auth.Authenticate(userID, secret)
}
我像这样使用这个处理器:
// main.go
package main
import (
"github.com/thisdougb/myapp/handler"
"github.com/thisdougb/myauthsvc" // 这个仓库仅作为示例,并不真实存在
)
func test() {
var authClient myauthsvc.Client
result := handler.auth("userID", "secret", authClient)
}
我是这样理解的:在 handler 子包中,我定义了所需的最小行为,并将其称为一个接口。然后,我可以创建满足该接口的具体类型,并将它们传入。这些具体类型可以来自我的外部库包(myauthsvc),也可以是在 handler_test.go 中创建的模拟对象。
现在很明显的是,我可以在不破坏 handler 实现的情况下扩展 myauthsvc。如果我的接口更大并且位于外部包内部,我之前就会破坏它。
测试也变得非常容易,这也是我最初的动机。
感谢所有的链接,非常有用。
更多关于Golang库包中实现"接口与结构体嵌入"的方法的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
对于任何走在相同道路上的人,在尝试了许多方法之后,我想我已经弄明白我之前哪里出错了。
我的动机是让我的库包易于被其他人模拟。事实证明,这并不是“在结构体中嵌入接口”所做的事情。这是我的第一个推论。
为了让使用你的库包的人能够轻松地进行模拟/测试,你需要包含一个接口类型。这个 ClientInterface(包接口?)随后可以在方法签名中用作参数类型。具体来说,这就是我之前弄错的地方。
package myauthsvc
type ClientInterface interface {
Authenticate(userID string, secret string) bool
}
type Client struct {
AccessToken string
Secret string
}
func (c Client) Authenticate(userID string, secret string) (bool, error) {
// 使用 c.AccessToken 和 c.Secret 做一些操作
return false
}
因此,当有人想在他们自己的代码中使用 myauthsvc 时,我们使用正常的依赖注入。这是可能的,因为 myauthsvc 实现了一个接口。在标准的“接口”用法中,ci 接受任何实现了 ClientInterface 方法的对象。
// handler.go
import (
"github.com/thisdougb/myauthsvc" // 这个仓库仅作为示例,并不真实存在
)
func auth(userID string, secret string, ci myauthsvc.ClientInterface) bool {
return ci.Authenticate(userID, secret)
}
这进而允许我们为测试模拟 myauthsvc。我像这样设置了一个测试。一个同样符合 ClientInterface 的模拟结构体。
// handler_test.go
type MockAuthSvc struct{}
func (m MockAuthSvc) Authenticate(userID string, secret string) bool {
return true
}
func TestAuth(t *testing.T) {
var mockC MockAuthSvc
assert.Equal(t, true, auth("userID", "secret", mockC)
}
因此,我可以将 myauthsvc 作为第三方库导入,并轻松地模拟它以运行我自己包的测试。
希望你能发现这些与你的情况相关的文章非常有用:
https://github.com/golang/go/wiki/CodeReviewComments#interfaces – 官方 Golang WIKI
https://www.ardanlabs.com/blog/2016/10/avoid-interface-pollution.html
https://rakyll.org/interface-pollution/ – Joanna 与 C++/Java 的对比
https://blog.chewxy.com/2018/03/18/golang-interfaces/ — 与 Java 的对比
Golang 和接口的误用
关于 Golang,我最喜欢的概念之一是接口。但每次看到它们被当作 C#/Java 接口来使用时,也是我最感到惋惜的地方。这很典型……
Golang 和接口的误用
Francesc Campoy 的演讲“理解接口”:
Speaker Deck
理解接口
Go 接口是语言中必不可少的一部分,但许多人并不完全理解何时或如何使用它们。
在这次演讲中,Francesc 从接口的基本理论讲到最佳实践,涵盖了 Go 代码库中常见的模式。

在 Go 中暴露接口
接口是我在 Go 中最喜欢的功能。接口类型代表一组方法。与大多数其他语言不同,你不必显式声明某个类型实现了某个接口。如果结构体 S 实现了接口 I 的所有方法,那么 S 就隐式地实现了接口 I……
在Go中,接口实现是隐式的,结构体嵌入接口确实可以实现接口方法的转发,但你的实现方式存在几个问题。以下是正确的实现方法:
package myauthsvc
// 定义接口
type ClientInterface interface {
Authenticate(userID string, secret string) (bool, error)
}
// 具体实现结构体
type Client struct {
AccessToken string
Secret string
}
// Client隐式实现了ClientInterface
func (c *Client) Authenticate(userID string, secret string) (bool, error) {
// 执行HTTP调用
return false, nil
}
// 工厂函数返回接口类型
func NewClient(token string, secret string) ClientInterface {
return &Client{
AccessToken: token,
Secret: secret,
}
}
对于需要模拟的场景,可以这样使用:
package main
import (
"github.com/thisdougb/myauthsvc"
)
// 模拟实现
type MockClient struct {
myauthsvc.ClientInterface // 嵌入接口(可选)
}
func (m *MockClient) Authenticate(userID string, secret string) (bool, error) {
// 模拟实现
return true, nil
}
func test() {
// 使用真实客户端
realClient := myauthsvc.NewClient("token", "secret")
// 使用模拟客户端
mockClient := &MockClient{}
// 测试函数接收接口类型
testAuthentication(realClient)
testAuthentication(mockClient)
}
func testAuthentication(client myauthsvc.ClientInterface) {
ok, err := client.Authenticate("user", "secret")
// 处理结果
}
如果需要在创建时注入不同的实现,可以这样修改:
package myauthsvc
type ClientInterface interface {
Authenticate(userID string, secret string) (bool, error)
}
// 基础客户端结构体
type BaseClient struct {
AccessToken string
Secret string
}
// 具体HTTP实现
type HTTPClient struct {
BaseClient
}
func (h *HTTPClient) Authenticate(userID string, secret string) (bool, error) {
// HTTP实现
return false, nil
}
// 工厂函数支持不同实现
func NewClient(token, secret string, implType string) ClientInterface {
base := BaseClient{
AccessToken: token,
Secret: secret,
}
switch implType {
case "http":
return &HTTPClient{BaseClient: base}
case "mock":
return &MockClient{BaseClient: base}
default:
return &HTTPClient{BaseClient: base}
}
}
结构体嵌入接口的正确用法示例:
type Logger interface {
Log(msg string)
}
type Service struct {
Logger // 嵌入接口
}
// 这样Service就可以直接调用Log方法
func (s *Service) DoWork() {
s.Log("starting work") // 转发到嵌入的Logger
}
// 使用
type ConsoleLogger struct{}
func (c ConsoleLogger) Log(msg string) {
fmt.Println(msg)
}
func main() {
svc := &Service{Logger: ConsoleLogger{}}
svc.DoWork()
}
在你的场景中,Client结构体不需要嵌入ClientInterface,因为Client已经通过方法实现了该接口。嵌入接口主要用于将接口方法转发给嵌入字段,这在装饰器模式或中间件模式中很有用。

