• 第十六章:接口


    image

    本篇翻译自《Practical Go Lessons》 Chapter 16: Interfaces

    1 你将在本章学到什么?

    • 什么是类型接口?
    • 如何定义接口。
    • “实现一个接口”是什么意思?
    • 接口的优点

    2 涵盖的技术概念

    • 接口 interface
    • 具体实现 concrete implementation
    • 实现一个接口
    • 接口的方法集

    3 介绍

    刚开始编程时,接口似乎很难理解。通常,新手程序员并不能完全理解接口的潜力。本节旨在解释什么是接口,它的有趣之处在哪里,以及如何创建接口。

    4 接口的基本定义

    • 接口是定义一组行为的契约
    • 接口是一个纯粹的设计对象,它们只是定义了一组行为(即方法),而没有给出这些行为的任何实现
    • 接口是一种类型,它定义了一组方法而不实现它们

    “实现” = “编写方法的代码”,这是一个示例接口类型(来自标准包 io):

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

    这里我们有一个名为 Reader 的接口类型,它指定了一种名为 Read 的方法。该方法没有具体实现,唯一指定的是方法名称及其签名(参数类型和结果类型)。

    4.0.0.1 接口类型的零值

    接口类型的零值为 nil,例子:

    var r io.Reader
    log.Println(r)
    // 2021/11/28 12:27:52 <nil>
    

    5 基本示例

    type Human struct {
        Firstname string
        Lastname string
        Age int
        Country string
    }
    
    type DomesticAnimal interface {
        ReceiveAffection(from Human)
        GiveAffection(to Human)
    }
    
    • 首先,我们声明一个名为 Human 的类型
    • 我们声明了一个名为 DomesticAnimal 的新类型接口
    • 这种类型的接口有一个由两个方法组成的方法集:ReceiveAffectionGiveAffect

    DomesticAnimal 是一个契约。

    • 它告诉开发者,要成为 DomesticAnimal,我们至少需要有两种行为:ReceiveAffectionGiveAffection

    让我们创建两个类型:

    type Cat struct {
        Name string
    }
    
    type Dog struct {
        Name string
    }
    

    我们有两种新类型。为了让他们遵守我们的接口 DomesticAnimal 的契约,
    我们必须为每种类型定义接口指定的方法。

    我们从 Cat 类型开始:

    func (c Cat) ReceiveAffection(from Human) {
        fmt.Printf("The cat named %s has received affection from Human named %s\n", c.Name, from.Firstname)
    }
    
    func (c Cat) GiveAffection(to Human) {
        fmt.Printf("The cat named %s has given affection to Human named %s\n", c.Name, to.Firstname)
    }
    

    现在 Cat 类型实现了 DomesticAnimal 接口。我们现在对 Dog 类型做同样的事情:

    func (d Dog) ReceiveAffection(from Human) {
        fmt.Printf("The dog named %s has received affection from Human named %s\n", d.Name, from.Firstname)
    }
    
    func (d Dog) GiveAffection(to Human) {
        fmt.Printf("The dog named %s has given affection to Human named %s\n", d.Name, to.Firstname)
    }
    

    我们的 Dog 类型现在正确地实现了 DomesticAnimal 接口。现在我们可以创建一个函数,它接受一个带有参数的接口:

    func Pet(animal DomesticAnimal, human Human) {
        animal.GiveAffection(human)
        animal.ReceiveAffection(human)
    }
    

    Pet 函数将 DomesticAnimal 类型的接口作为第一个参数,将 Human 作为第二个参数。

    在函数内部,我们调用了接口的两个函数。

    让我们使用这个函数:

    func main() {
    
        // Create the Human
        var john Human
        john.Firstname = "John"
    
    
        // Create a Cat
        var c Cat
        c.Name = "Maru"
    
        // then a dog
        var d Dog
        d.Name = "Medor"
    
        Pet(c, john)
        Pet(d,john)
    }
    
    • DogCat 类型实现了接口 DomesticAnimal 的方法
    • 也就是说 DogCat 类型的任何变量都可以看作DomesticAnimal

    只要 Cat 实现的方法的函数签名与接口定义一致就可以,不强制要求完全相同变量名和返回名。所以我们将函数 func (c Cat) ReceiveAffection(from Human) {...} 改成 func (c Cat) ReceiveAffection(f Human) {...} 也是可以的

    6 编译器在看着你!

    遵守类型 T 的接口契约意味着实现接口的所有方法。让我们试着欺骗编译器看看会发生什么:

    // ...
    // let's create a concrete type Snake
    type Snake struct {
        Name string
    }
    // we do not implement the methods ReceiveAffection and GiveAffection intentionally
    //...
    
    
    func main(){
    
        var snake Snake
        snake.Name = "Joe"
    
        Pet(snake, john)
    }
    
    • 我们创建了一个新类型的 Snake
    • 该类型没有实现 DomesticAnimal 动物的任何方法
    • 在主函数中,我们创建了一个新的 Snake 类型的变量
    • 然后我们用这个变量作为第一个参数调用 Pet 函数

    结果是编译失败:

    ./main.go:70:5: cannot use snake (type Snake) as type DomesticAnimal in argument to Pet:
        Snake does not implement DomesticAnimal (missing GiveAffection method)
    

    编译器在未实现的按字母顺序排列的第一个方法处检查停止。

    7 例子:database/sql/driver.Driver

    我们来看看 Driver 接口(来自包database/sql/driver

    type Driver interface {
        Open(name string) (Conn, error)
    }
    
    • 存在不同种类的 SQL 数据库,因此 Open 方法有多种实现。
    • 为什么?因为你不会使用相同的代码来启动到 MySQL 数据库和 Oracle 数据库的连接。
    • 通过构建接口,你可以定义一个可供多个实现使用的契约。

    8 接口嵌入

    你可以将接口嵌入到其他接口中。让我们举个例子:

    // the Stringer type interface from the standard library
    type Stringer interface {
        String() string
    }
    // A homemade interface
    type DomesticAnimal interface {
        ReceiveAffection(from Human)
        GiveAffection(to Human)
        // embed the interface Stringer into the DomesticAnimal interface
        Stringer
    }
    

    在上面的代码中,我们将接口 Stringer 嵌入到接口 DomesticAnimal 中。
    因此,已经实现了 DomesticAnimal 的其他类型必须实现 Stringer 接口的方法。

    • 通过接口嵌入,你可以在不重复的情况下向接口添加功能。
    • 这也是有代价的,如果你从另一个模块嵌入一个接口,你的代码将与其耦合
      • 其他模块接口的更改将迫使你重写代码。
      • 请注意,如果依赖模块遵循语义版本控制方案,则这种危险会得到缓和
      • 你可以毫无畏惧地使用标准库中的接口

    9 来自标准库的一些有用(和著名)的接口

    9.1 Error 接口

    type error interface {
        Error() string
    }
    

    这个接口类型被大量使用,用于当函数或方法执行失败是返会error类型接口:

    func (c *Communicator) SendEmailAsynchronously(email *Email) error {
        //...
    }
    

    要创建一个 error ,我们通常调用: fmt.Errorf() 返回一个 error 类型的结果,或者使用 errors.New()函数。
    当然,你也可以创建实现error接口的类型。

    9.2 fmt.Stringer 接口

    type Stringer interface {
        String() string
    }
    

    使用 Stringer 接口,你可以定义在调用打印方法时如何将类型打印为字符串(fmt.Errorf(),fmt.Println, fmt.Printf, fmt.Sprintf...)

    这有一个示例实现

    type Human struct {
        Firstname string
        Lastname string
        Age int
        Country string
    }
    
    func (h Human) String() string {
        return fmt.Sprintf("human named %s %s of age %d living in %s",h.Firstname,h.Lastname,h.Age,h.Country)
    }
    

    Human 现在实现了 Stringer 接口:

    package main
    
    func main() {
        var john Human
        john.Firstname = "John"
        john.Lastname = "Doe"
        john.Country = "USA"
        john.Age = 45
    
        fmt.Println(john)
    }
    

    输出:

    human named John Doe of age 45 living in the USA
    

    9.3 sort.Interface 接口

    type Interface interface {
        Len() int
        Less(i, j int) bool
        Swap(i, j int)
    }
    

    通过在一个类型上实现 sort.Interface 接口,可以对一个类型的元素进行排序(通常,底层类型是一个切片)。

    这是一个示例用法(来源:sort/example_interface_test.go):

    type Person struct {
        Age int
    }
    // ByAge implements sort.Interface for []Person based on
    // the Age field.
    type ByAge []Person
    
    func (a ByAge) Len() int           { return len(a) }
    func (a ByAge) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
    func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
    
    • ByAge 类型实现了 sort.Interface
      • 底层类型是 Person 的一个切片
    • 接口由三个方法组成:
      • Len() int:返回集合内的元素数
      • Less(i, j int) bool:如果索引 i 处的元素应该排在索引 j 处的元素之前,则返回 true
      • Swap(i, j int):交换索引 i & j 处的元素;换句话说,我们应该将位于索引 j 的元素放在索引 i 处,而位于索引 i 的元素应该放在索引 j 处。
        然后我们可以使用 sort.Sort 函数对 ByAge 类型的变量进行排序
    func main() {
        people := []Person{
            {"Bob", 31},
            {"John", 42},
            {"Michael", 17},
            {"Jenny", 26},
        }
    
        sort.Sort(ByAge(people))
    }
    

    10 隐式实现

    接口是隐式实现的。当你声明一个类型时,你不必指定它实现了哪些接口。

    11 PHP 和 JAVA

    在其他语言中,你必须指定接口实现。
    这是 Java 中的一个示例:

    // JAVA
    public class Cat implements DomesticAnimal{
        public void receiveAffection(){
            //...
        }
        public void giveAffection(){
            //..
        }
    }
    

    这是 PHP 中的另一个示例:

    //PHP
    <?php
    
    class Cat implements DomesticAnimal {
        public function receiveAffection():void {
            // ...
        }
        public function giveAffection():void {
            // ...
        }
    }
    ?>
    

    你可以看到,在声明实现接口的类时,必须添加关键字"implements"

    你可能会问 Go 运行时如何处理这些隐式接口实现。我们将后面解释接口值的机制。

    12 空接口

    Go 的空接口是你可以编写的最简单、体积更小的接口。它的方法集正好由 0 个方法组成。

    interface{}
    

    也就是说,每种类型都实现了空接口。你可能会问为什么需要这么无聊的空接口。根据定义,空接口值可以保存任何类型的值。如果你想构建一个接受任何类型的方法,它会很有用。
    让我们从标准库中举一些例子。

    • log 包中,你有一个 Fatal 方法,可以将任何类型的输入变量作为输入:
    func (l *Logger) Fatal(v ...interface{}) { }
    
    • fmt 包中,我们还有许多方法将空接口作为输入。例如 Printf 函数:
    func Printf(format string, a ...interface{}) (n int, err error) { }
    

    12.1 类型转换

    接受空接口作为参数的函数通常需要知道其输入参数的有效类型。
    为此,该函数可以使用“类型开关”,这是一个 switch case 将比较类型而不是值。
    这是从标准库(文件 runtime/error.go,包 runtime)中获取的示例:

    // printany prints an argument passed to panic.
    // If panic is called with a value that has a String or Error method,
    // it has already been converted into a string by preprintpanics.
    func printany(i interface{}) {
        switch v := i.(type) {
        case nil:
            print("nil")
        case bool:
            print(v)
        case int:
            print(v)
        case int8:
            print(v)
        case int16:
            print(v)
        case int32:
            print(v)
        case int64:
            print(v)
        case uint:
            print(v)
        case uint8:
            print(v)
        case uint16:
            print(v)
        case uint32:
            print(v)
        case uint64:
            print(v)
        case uintptr:
            print(v)
        case float32:
            print(v)
        case float64:
            print(v)
        case complex64:
            print(v)
        case complex128:
            print(v)
        case string:
            print(v)
        default:
            printanycustomtype(i)
        }
    }
    

    image

    12.2 关于空接口的使用

    • 你应该非常小心地使用空接口。
    • 当你别无选择时,请使用空接口。
    • 空接口不会向将使用你的函数或方法的人提供任何信息,因此他们将不得不参考文档,这可能会令人沮丧。

    你更喜欢哪种方法?

    func (c Cart) ApplyCoupon(coupon Coupon) error  {
        //...
    }
    
    func (c Cart) ApplyCoupon2(coupon interface{}) (interface{},interface{}) {
        //...
    }
    

    ApplyCoupon 方法严格指定它将接受和返回的类型。而 ApplyCoupon2 没有在输入和输出中指定它的类型。作为调用方,ApplyCoupon2 的使用难度比 ApplyCoupon 大。

    13 实际应用:购物车存储

    13.1 规则说明

    你建立了一个电子商务网站;你必须存储和检索客户购物车。必须支持以下两种行为:

    1. 通过 ID 获取购物车
    2. 将购物车数据放入数据库

    为这两种行为提出一个接口。还要创建一个实现这两个接口的类型(不要实现方法中的逻辑)。

    13.2 答案

    这是一个设计的接口:

    type CartStore interface {
        GetById(ID string) (*cart.Cart, error)
        Put(cart *cart.Cart) (*cart.Cart, error)
    }
    

    实现接口的类型:

    type CartStoreMySQL struct{}
    
    func (c *CartStoreMySQL) GetById(ID string) (*cart.Cart, error) {
        // implement me
    }
    
    func (c *CartStoreMySQL) Put(cart *cart.Cart) (*cart.Cart, error) {
        // implement me
    }
    

    另一种实现接口的类型:

    type CartStorePostgres struct{}
    
    func (c *CartStorePostgres) GetById(ID string) (*cart.Cart, error) {
        // implement me
    }
    
    func (c *CartStorePostgres) Put(cart *cart.Cart) (*cart.Cart, error) {
        // implement me
    }
    
    • 你可以为你使用的每个数据库模型创建一个特定的实现
    • 添加对新数据库引擎的支持很容易!你只需要创建一个实现接口的新类型。

    14 为什么要使用接口?

    14.1 易于升级

    当你在方法或函数中使用接口作为输入时,你将程序设计为易于升级的。未来的开发人员(或未来的你)可以在不更改大部分代码的情况下创建新的实现。

    假设你构建了一个执行数据库读取、插入和更新的应用程序。你可以使用两种设计方法:

    1. 创建与你现在使用的数据库引擎密切相关的类型和方法。
    2. 创建一个接口,列出数据库引擎的所有操作和具体实现。
    • 在第一种方法中,你创建将特定实现作为参数的方法。
    • 通过这样做,你将程序限制到一个实现。
    • 在第二种方法中,你创建接受接口的方法。
    • 改变实现就像创建一个实现接口的新类型一样简单。

    14.2 提高团队合作

    团队也可以从接口中受益。
    在构建功能时,通常需要多个开发人员来完成这项工作。如果工作需要两个团队编写的代码进行交互,他们可以就一个或多个接口达成一致。
    然后,两组开发人员可以处理他们的代码并使用商定的接口。他们甚至可以 mock 其他团队的返回结果。通过这样做,团队不会被阻塞。

    14.3 Benefit from a set of routines

    在自定义类型上实现接口时,你可以不需要开发就使用的附加功能。让我们从标准库中举一个例子:sort 包。这并不奇怪。这个包是用来进行排序的。这是 go 源代码的摘录:

    // go v.1.10.1
    package sort
    //..
    
    type Interface interface {
        // Len is the number of elements in the collection.
        Len() int
        // Less reports whether the element with
        // index i should sort before the element with index j.
        Less(i, j int) bool
        // Swap swaps the elements with indexes i and j.
        Swap(i, j int)
    }
    
    // Sort sorts data.
    // It makes one call to data.Len to determine n, and O(n*log(n)) calls to
    // data.Less and data.Swap. The sort is not guaranteed to be stable.
    func Sort(data Interface) {
        n := data.Len()
        quickSort(data, 0, n, maxDepth(n))
    }
    

    在第一行,我们声明当前包:sort。在接下来的几行中,程序员声明了一个名为 Interface 的接口。这个接口 Interface 指定了三个方法:Len、Less、Swap

    在接下来的几行中,函数 Sort 被声明。它将接口类型 data 作为参数。这是一个非常有用的函数,可以对给定的数据进行排序。

    我们如何在我们的一种类型上使用这个函数?实现接口

    假设你有一个 User 类型:

    type User struct {
        firstname string
        lastname string
        totalTurnover float64
    }
    

    还有一个类型 Users ,它是 User 类型切片:

    type Users []User
    

    让我们创建一个 Users 实例并用三个 User 类型的变量填充它:

    user0 := User{firstname:"John", lastname:"Doe", totalTurnover:1000}
    user1 := User{firstname:"Dany", lastname:"Boyu", totalTurnover:20000}
    user2 := User{firstname:"Elisa", lastname:"Smith Brown", totalTurnover:70}
    
    users := make([]Users,3)
    users[0] = user0
    users[1] = user1
    users[2] = user2
    

    如果我们想按营业额排序怎么办?我们可以从头开始开发符合我们规范的排序算法。或者我们可以只实现使用 sort 包中的内置函数.Sort 所需的接口。我们开始吧:

    // Compute the length of the array. Easy...
    func (users Users) Len() int {
      return len(users)
    }
    
    // decide which instance is bigger than the other one
    func (users Users) Less(i, j int) bool {
      return users[i].totalTurnover < users[j].totalTurnover
    }
    
    // swap two elements of the array
    func (users Users) Swap(i, j int) {
        users[i], users[j] = users[j], users[i]
    }
    

    通过声明这些函数,我们可以简单地使用 Sort 函数:

    sort.Sort(users)
    fmt.Println(users)
    // will output :
    [{Elisa Smith Brown 70} {John Doe 1000} {Dany Boyu 20000}]
    

    15 一点建议

    1. 尽量使用标准库提供的接口
    2. 方法太多的接口很难实现(因为它需要编写很多方法)。

    16 随堂测试

    16.1 问题

    1. 举一个接口嵌入另一个接口的例子。
    2. 判断真假。嵌入接口中指定的方法不是接口方法集的一部分。
    3. 说出使用接口的两个优点。
    4. 接口类型的零值是多少?

    16.2 答案

    1. 举一个接口嵌入另一个接口的例子。
    type ReadWriter interface {
        Reader
        Writer
    }
    
    1. 判断真假。嵌入接口中指定的方法不是接口方法集的一部分。
      错。接口的方法集是由两个部分组成:
      1. 直接指定到接口中的方法
      2. 来自嵌入接口的方法
    2. 说出使用接口的两个优点。
      1. 轻松地在开发人员之间拆分工作:
        1.定义接口类型
        2.一个人开发接口的实现
        3.另一个人可以在其功能中使用接口类型
        4.两个人可以互不干扰地工作。
      2. 易于升级
        1.当你创建一个接口时,你就创建了一个契约
        2.不同的实现可以履行这个契约。
        3.在一个项目的开始,通常有一个实现
        4.随着时间的推移,可能需要另一种实现方式。
    3. 接口类型的零值是多少?
      nil

    17 关键要点

    • 接口就是契约
    • 它指定方法(行为)而不实现它们。
    type Cart interface {
        GetById(ID string) (*cart.Cart, error)
        Put(cart *cart.Cart) (*cart.Cart, error)
    }
    
    • 接口是一种类型(就像structs, arrays, maps,等)
    • 我们将接口中指定的方法称为接口的方法集。
    • 一个类型可以实现多个接口。
    • 无需明确类型实现了哪个接口
      • 与其他需要声明它的语言(PHP、Java 等)相反
    • 一个接口可能嵌入到另一个接口中;在这种情况下,嵌入的接口方法被添加到接口中。
    • 接口类型可以像任何其他类型一样使用
    • 接口类型的零值为 nil
    • 任何类型实现空接口 interface{}
    • 空接口指定了 0 个方法
    • 要获取空接口的具体类型,您可以使用 type switch:
    switch v := i.(type) {
        case nil:
            print("nil")
        case bool:
            print(v)
        case int:
            print(v)
    }
    
    • 当我们可以通过各种方式实现一个行为时,我们或许可以创建一个接口。
      • 例如:存储(我们可以使用 MySQL、Postgres、DynamoDB、Redis 数据库来存储相同的数据)
  • 相关阅读:
    uni-app之预加载和取消预加载(仅支持APP和H5)——uni.preloadPage、uni.unPreloadPage
    JavaScript 之数组对象(Array)
    【2019csp模拟】文件列表
    【2019csp模拟】两段子序列
    B. 【普转提七联测 Day 6】载重
    C.【普转提七联测 Day 6】分数
    A. 【普转提七联测 Day 6】石头
    struct和class的区别
    TagHelper中获取当前Url
    为什么要使用 Taghelper (标记助手)
  • 原文地址:https://www.cnblogs.com/Zioyi/p/15615030.html
Copyright © 2020-2023  润新知