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

4 回复

是的,那些内容值得一读。谢谢。

我想最明确的观点是:“在使用的点定义接口。” 这与我之前尝试的做法正好相反。

所以我的外部库包变成了:

// 外部库包
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 的对比

site-icon hyeomans.com

Golang 和接口的误用

关于 Golang,我最喜欢的概念之一是接口。但每次看到它们被当作 C#/Java 接口来使用时,也是我最感到惋惜的地方。这很典型……

Golang 和接口的误用

Francesc Campoy 的演讲“理解接口”: site-icon Speaker Deck

理解接口

Go 接口是语言中必不可少的一部分,但许多人并不完全理解何时或如何使用它们。

在这次演讲中,Francesc 从接口的基本理论讲到最佳实践,涵盖了 Go 代码库中常见的模式。

thumbnail


Efe’s Blog – 29 Dec 19

在 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已经通过方法实现了该接口。嵌入接口主要用于将接口方法转发给嵌入字段,这在装饰器模式或中间件模式中很有用。

回到顶部