关于go:golang中的接口

41次阅读

共计 5517 个字符,预计需要花费 14 分钟才能阅读完成。

0.1、索引

https://waterflow.link/articles/1666171320273

1、概念

接口提供了一种指定对象行为的办法。咱们应用接口来创立多个对象能够实现的通用形象。Go 接口不同的起因在于它们是隐式的。没有像 implements 这样的显式关键字来标记对象 A 实现了接口 B。

为了了解接口的弱小,咱们能够看下规范库中两个罕用的接口:io.Reader 和 io.Writer。io 包为 I/O 原语提供形象。在这些形象中,io.Reader 从数据源读取数据,io.Writer 将数据写入指标。

io.Reader 蕴含一个 Read 办法:

type Reader interface {Read(p []byte) (n int, err error)
}

io.Reader 接口的自定义实现应该接管一个字节切片 p,把数据读取到 p 中并返回读取的字节数或谬误。

io.Writer 定义了一个办法,Write:

type Writer interface {Write(p []byte) (n int, err error)
}

io.Writer 的自定义实现应该将来自切片的数据 p 写入底层数据流并返回写入的字节数或谬误。

因而,这两个接口都提供了根本的形象:

  • io.Reader 从一个源对象读取数据
  • io.Writer 将数据写到一个指标对象

假如咱们须要实现一个将一个文件的内容复制到另一个文件的函数。咱们能够创立一个特定的函数 copyFile,它将应用 io.Reader 和 io.Writer 形象创立一个更通用的函数:

package main

import (
    "io"
    "log"
    "os"
)

func main() {
  // 1 关上一个源文件
    source, err := os.Open("a.txt")
    if err != nil {log.Fatal(err)
    }
    defer source.Close()

  // 2 创立一个指标文件
    dest, err := os.Create("b.txt")
    if err != nil {log.Fatal(err)
    }
    defer dest.Close()

  // 从把源数据复制到指标
    err = CopyFile(source, dest)
    if err != nil {log.Fatal(err)
    }
}

// 复制
func CopyFile(source io.Reader, dest io.Writer) error {var buffer = make([]byte, 1024)
    for {n, err := source.Read(buffer)
        if err == nil {_, err = dest.Write(buffer[:n])
            if err != nil {return err}
        }
        if err == io.EOF {_, err = dest.Write(buffer)
            if err != nil {return err}
            return nil
        }
        return err
    }
}
  1. 咱们利用 os.Open 关上一个文件,该函数返回一个 *os.File 句柄,*os.File 实现了 io.Reader 和 io.Writer
  2. 咱们应用 os.Create 创立一个新的文件,该函数返回一个 *os.File 句柄
  3. 咱们应用 copyFile 函数,该函数有两个参数,source 为一个实现 io.Reader 接口的参数,dest 为一个实现 io.Writer 接口的参数

该函数实用于 os.File 参数(因为 os.File 实现了 io.Reader 和 io.Writer)以及任何其余能够实现这些接口的类型。例如,咱们能够创立本人的 io.Writer 写入数据库,并且代码将放弃不变。它减少了函数的通用性;因而,他是可重用的,这很重要。

此外,为这个函数编写单元测试更容易,因为咱们能够应用提供有用实现的字符串和字节包,而不是解决文件:

package main

import (
    "bytes"
    "strings"
    "testing"
)

func TestCopyFile(t *testing.T)  {
    input := "hahahha"
    source := strings.NewReader(input)
    dest := bytes.NewBuffer(make([]byte, 0))

    err := CopyFile(source, dest)
    if err != nil {t.Fatal(err)
    }

    got := dest.String()
    if got != input {t.Errorf("input is %s, got is %s, want is %s", input, got, input)
    }
}

在下面例子中,source 是 strings.Reader,而 dest 是 bytes.Buffer。在这里,咱们在不创立任何文件的状况下测试 CopyFile,归功于 CopyFile 的参数应用的是接口,只有咱们参数实现了这俩个接口就能够运行单元测试。

2、什么时候应用接口

咱们什么时候应该在 Go 中创立接口?让咱们看一下通常认为接口带来价值的三个具体用例:

  • 通用行为
  • 解耦
  • 限度行为

2.1、通用行为

在多种类型实现独特行为时应用接口。在这种状况下,咱们能够合成出接口外部的行为。如果咱们查看规范库,咱们能够找到许多此类用例的示例。例如,能够通过 2 个办法让共享资源变得平安:

  • 给共享资源加锁
  • 给共享资源开释锁

因而,sync 包中增加了以下接口:

type Locker interface {Lock()
    Unlock()}

该接口具备弱小的可重用后劲,因为它蕴含对任何共享资源进行不同形式爱护的常见行为。

咱们都晓得 sync.Mutex 是不反对锁的可重入的,然而有时咱们心愿同一个协程能够给资源重复上锁,而不会引起报错。因而,加锁和解锁就能够被抽象化,咱们能够依赖 sync.Locker。

所以咱们就能够很轻松的实现可重入锁,像上面这样:

package main

import (
    "fmt"
    "github.com/petermattis/goid"
    "log"
    "sync"
    "sync/atomic"
)

type RecursiveMutex struct {
    sync.Mutex
    owner int64
    recursion int32
}

// 1
func (m *RecursiveMutex) Lock()  {gid := goid.Get()
    if atomic.LoadInt64(&m.owner) == gid {
        m.recursion++
        return
    }

    m.Mutex.Lock()
    atomic.StoreInt64(&m.owner, gid)
    m.recursion = 1
}

// 2
func (m *RecursiveMutex) Unlock()  {gid := goid.Get()

    if atomic.LoadInt64(&m.owner) != gid {panic(fmt.Sprintf("Wrong the owner (%d): %d!", m.owner, gid))
    }

    m.recursion--
    if m.recursion != 0 {return}
    atomic.StoreInt64(&m.owner, -1)
    m.Mutex.Unlock()}

func main()  {l := &RecursiveMutex{}
    foo1(l)
}

func foo1(l *RecursiveMutex) {log.Println("in foo")
    l.Lock()
    bar1(l)
    l.Unlock()}

func bar1(l *RecursiveMutex) {l.Lock()
    log.Println("in bar")
    l.Unlock()}
  1. 实现 sync.Locker 的 Lock 办法
  2. 实现 sync.Locker 的 Unlock 办法

2.2、解耦

如果咱们依赖形象而不是具体的实现,则能够用另一个具体实现取替换,甚至不用更改咱们的代码。这就是 里氏替换准则。

解耦的益处之一可能与单元测试无关。假如咱们要实现一个 StoreCourseware 办法来创立一个课件。咱们决定间接依赖具体实现:

// 课件模型
type Courseware struct {id int64}

type Store struct {
}
func (s Store) StoreCourseware(courseware Courseware) error {
    // 须要走数据库
    return nil
}

type CoursewareService struct {store Store}

func (cw CoursewareService) CreateCourseware(id int64) error {courseware := Courseware{id: id}
    return cw.store.StoreCourseware(courseware)
}

当初,如果咱们想测试这个办法怎么办?因为 CoursewareService 依赖于理论实现来存储课件,所以咱们不得不通过集成测试对其进行测试,这须要启动 MySQL 实例(除非咱们应用诸如 go-sqlmock 之类的代替技术,但这不是本节要探讨的内容)。只管集成测试很有帮忙,但这并不总是咱们想要做的。为了使咱们代码有更大的灵活性,咱们应该将 CoursewareService 与理论实现拆散,这能够通过如下接口实现:

// 课件模型
type Courseware struct {id int64}

// 增加课件的一种实现
type Store struct {
}
func (s Store) StoreCourseware(courseware Courseware) error {
    // 须要走数据库
    return nil
}

// 增加课件的接口,只有实现接口不论走 mysql 还是内存
type CoursewareStorer interface {StoreCourseware (courseware Courseware) error
}

type CoursewareService struct {store CoursewareStorer}

func (cw CoursewareService) CreateCourseware(id int64) error {courseware := Courseware{id: id}
    return cw.store.StoreCourseware(courseware)
}

因为当初存储客户是通过一个接口实现的,这给了咱们更多的灵活性来测试咱们想要的办法。例如,咱们能够:

  • 通过集成测试应用具体实现
  • 通过单元测试应用模仿(或任何类型的测试替身)

2.3、限度行为

假如咱们实现了一个自定义配置包来解决动静配置。咱们通过一个 Config 构造保留配置,该构造还公开了两种办法:Get 和 Set。以下是该代码的实现:

type Config struct {
    rabbitmq string
      cpu int
}
 
func (c *Config) Rabbitmq() string {return c.rabbitmq}
 
func (c *Config) SetRabbitmq(value string) {c.rabbitmq = value}

当初,假如 Config 有个 cpu 配置,然而在咱们的代码中,咱们不心愿更新他,让他只读。如果咱们不想更改配置包,如何从语义上强制执行此配置是只读的?通过创立一个将行为限度为只读的形象:

type ConfigCPUGetter interface {Get() int
}

而后,在咱们的代码中,咱们能够依赖 ConfigCPUGetter 而不是具体的实现:

type Foo struct {threshold ConfigCPUGetter}
 
func NewFoo(threshold ConfigCPUGetter) Foo {return Foo{threshold: threshold}
}
 
func (f Foo) Bar()  {threshold := f.threshold.Get()         
    // ...
}

在这个例子中,配置 getter 被注入到 NewFoo 工厂办法中。它不会影响此函数的客户端,因为它依然能够在实现 ConfigCPUGetter 时传递 Config 构造。而后,咱们只能读取 Bar 办法中的配置,不能批改它。因而,咱们还能够出于各种起因应用接口将类型限度为特定行为。

3、接口净化

在 Go 我的项目中适度应用接口是很常见的。兴许开发人员的背景是 C# 或 Java,他们发现在具体类型之前创立接口是很天然的。然而,这不是 Go 中的工作形式。

正如咱们所探讨的,接口是用来创立形象的。当编程遇到形象时,次要的正告是记住应该发现形象,而不是创立形象。这是什么意思?这意味着如果没有间接的理由,咱们不应该开始在咱们的代码中创立形象。咱们不应该应用接口进行设计,而是期待具体的需要。换句话说,咱们应该在须要时创立接口,而不是在咱们预见到可能须要它时。

如果咱们适度应用接口,次要问题是什么?答案是它们使代码流更加简单。增加无用的间接级别不会带来任何价值;它创立了一个毫无价值的形象,使代码更难浏览、了解和推理。如果咱们没有充沛的理由增加接口,并且不分明接口如何使代码变得更好,咱们应该挑战这个接口的目标。为什么不间接调用实现呢?

留神当通过接口调用办法时,咱们也可能会遇到性能开销。它须要在哈希表的数据结构中查找以找到接口指向的具体类型。但这在许多状况下都不是问题,因为开销很小。

总之,在咱们的代码中创立形象时咱们应该小心——应该发现形象,而不是创立形象。对于咱们软件开发人员来说,通过依据咱们认为当前可能须要的货色来猜想完满的形象级别是什么来适度设计咱们的代码是很常见的。应该防止这个过程,因为在大多数状况下,它会用不必要的形象净化咱们的代码,使其浏览起来更加简单。

咱们不要试图抽象地解决问题,而是解决当初必须解决的问题。最初但同样重要的是,如果不分明接口如何使代码变得更好,咱们可能应该思考删除它以使咱们的代码更简略。

正文完
 0