go语言接口速成(译)

97 阅读5分钟

这是我参与2022首次更文挑战的第26天,活动详情查看:2022首次更文挑战

本文为译文,原文链接:www.calhoun.io/crash-cours…

在这篇文章中,我们将做一个关于Go接口的速成课程,然后在本系列的其余部分,我们将继续探讨接口是如何工作的,如何有效地使用它们,等等。

让我们先来谈谈Go与动态语言(如JavaScript、Ruby和Python)的区别。在动态语言中创建函数时,不需要定义期望接收的类型。相反,任何东西都可以传入。下面是一个使用JavaScript的例子。

function Greeting(name) {
  return "Hello, " + name
}

// The following is all valid in JS, even if it doesn't make much sense.
Greeting("Jon")
// Output: "Hello, Jon"

Greeting(123)
// Output: "Hello, 123"

Greeting(document)
// Output: "Hello, [object HTMLDocument]"

在Go中,你必须显式地声明类型,这意味着当你创建一个函数时,你必须声明你希望name参数是一个字符串。

func Greeting(name string) string {
  return fmt.Sprintf("Hello, %v!", name)
}

这在很大程度上是一件好事。它向任何阅读您的代码的人清楚地表明您所期望的内容,并且防止其他开发人员意外地传入错误的内容。

如果您曾经使用过动态语言,您很可能会遇到错误,因为有人向函数中传递了错误的东西——就像JS示例中传递整个文档一样!这种类型的错误在Go中发生的可能性要小得多,因为它是静态类型系统。

不幸的是,静态类型也限制了我们的代码。例如,假设您有一些代码将我们的问候写到一个文件中。

func WriteGreeting(name string, f *os.File) error {
  greeting := Greeting(name)
  _, err := f.Write([]byte(greeting))
  if err != nil {
    return err
  }
  return nil
}

这段代码工作得很完美,但仅限于写入文件。如果我们想把问候语写成strings.builder呢?

如果没有接口,我们将被迫重写函数以接受string.builder!

func WriteGreeting(name string, sb *strings.Builder) error {
  greeting := Greeting(name)
  _, err := sb.Write([]byte(greeting))
  if err != nil {
    return err
  }
  return nil
}

旁注:是的,我知道strings.Builder有一个riteString方法。这就能解释为什么我选择用Write来代替了。

查看这两个函数的代码,您会注意到它们非常相似。忽略变量名的更改,唯一真正的区别是,在第一个代码示例中,我们使用的是File。第二步,我们使用Builder.Write,但在这两种情况下,我们使用的方法都有以下定义:

Write(p []byte) (int, error)

实际上,我们并不关心我们是否在处理一个strings.Builderos.File。我们真正关心的是使用Write方法给我们一些东西。这样我们的代码就可以继续工作了。

幸运的是,Go有一个叫做接口的东西,可以让我们准确地表达这一点。下面是一个接口示例,它适用于这种特殊情况。

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

定义了接口后,我们可以像这样重写WriteGreeting函数:

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

func WriteGreeting(name string, w Writer) error {
  greeting := Greeting(name)
  _, err := w.Write([]byte(greeting))
  if err != nil {
    return err
  }
  return nil
}

现在,任何具有Write方法的类型都可以传递给WriteGreeting!实际上,这个接口非常常见,所以在标准库👉io.Writer

围棋最巧妙的地方之一是它使用了鸭子类型。引用马特(以及最初创造这个术语的人)的话,“如果它看起来像一只鸭子,而且它叫起来也像一只鸭子,那么它就是一只鸭子。”

(好吧,从技术上讲,它不是鸭子类型,但“结构化类型”没有朗朗上口的“鸭子一样呱呱”的引语!😂)

或者在Go的上下文context中,如果它看起来像一个Writer接口,并且有Writer接口定义的方法,那么它就是一个Writer接口实现。

我之所以提到duck类型,是因为它在所有静态类型语言中都不适用。例如,在Java中,您需要显式地声明一个类实现了一个接口,否则编译器将不允许您将其用作接口实现。与Java等语言相比,这对Go的界面使用有很大的影响,我们将在以后的邮件中进一步讨论这个问题。

更具体的细节是,任何具有由接口定义的每个方法的类型都将实现该接口。如果类型的方法比接口定义的多,也可以,但在接受接口的函数中,这些额外的方法将不可访问。

func main() {
  var sb strings.Builder
  WriteGreeting("Jon", &sb)
}

func WriteGreeting(name string, w Writer) error {
  greeting := Greeting(name)
  // We can access Write here because it is defined by the Writer interface,
  // but we CANNOT access the Builder.WriteString method here even if we passed
  // in a strings.Builder because WriteString is not defined as part of the
  // Writer interface.
  _, err := w.Write([]byte(greeting))
  if err != nil {
    return err
  }
  return nil
}

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

接口定义的每个方法必须为类型实现接口而实现。下面的例子将导致编译错误,因为我们的Demo类型没有定义由BigInterface定义的所有方法:

type BigInterface interface {
  A()
  B()
  C()
}

type Demo struct {}

func (Demo) A() {}
func (Demo) B() {}

func Test(bi BigInterface) {}

func main() {
  var demo Demo
  Test(demo) // errors because Demo doesn't implement the C() method.
}

因此,通常将接口限制为特定于您需要的方法,而不是一组大型方法,因为这使得其他类型更容易实现接口。

希望这篇文章能够帮助我们了解什么是界面,以及我们为什么想要使用它们。在接下来的几篇文章中,我将深入探讨以下主题:

  • 接口的缺点,包括一个关于当我刚开始学习Go的时候,io.Writer和io.Reader让我很困惑。

  • 接口的通用命名模式

  • 指针方法如何影响类型是否实现接口。

  • 与Java等语言相比,duck类型是如何改变我们在Go中使用接口的方式的

  • 接口方法的命名参数信息

  • 使用接口会使代码变得更糟的例子。

  • 接口如何使测试更容易。