这是我参与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.Builder或os.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中使用接口的方式的
-
接口方法的命名参数信息
-
使用接口会使代码变得更糟的例子。
-
接口如何使测试更容易。