Go语言嵌入类型

Go语言允许用户扩展或者修改已有类型的行为。这个功能对代码复用很重要,在修改已有类型以符合新类型的时候也很重要。这个功能是通过嵌入类型(type embedding)完成的。嵌入类型是将已有的类型直接声明在新的结构类型里。被嵌入的类型被称为新的外部类型的内部类型。

通过嵌入类型,与内部类型相关的标识符会提升到外部类型上。这些被提升的标识符就像直接声明在外部类型里的标识符一样,也是外部类型的一部分。这样外部类型就组合了内部类型包含的所有属性,并且可以添加新的字段和方法。外部类型也可以通过声明与内部类型标识符同名的标识符来覆盖内部标识符的字段或者方法。这就是扩展或者修改已有类型的方法。

让我们通过一个示例程序来演示嵌入类型的基本用法,代码如下所示。
// 这个示例程序展示如何将一个类型嵌入另一个类型,以及
// 内部类型和外部类型之间的关系
package main

import (
    "fmt"
)

// user 在程序里定义一个用户类型
type user struct {
    name string
    email string
}

// notify 实现了一个可以通过user 类型值的指针
// 调用的方法
func (u *user) notify() {
    fmt.Printf("Sending user email to %s<%s>\n",
    u.name,
    u.email)
}

// admin 代表一个拥有权限的管理员用户
type admin struct {
    user // 嵌入类型
    level string
}

// main 是应用程序的入口
func main() {
    // 创建一个admin 用户
    ad := admin{
        user: user{
            name: "john smith",
            email: "john@yahoo.com",
        },
        level: "super",
    }

    // 我们可以直接访问内部类型的方法
    ad.user.notify()

    // 内部类型的方法也被提升到外部类型
    ad.notify()
}
在上面代码中,我们的程序演示了如何嵌入一个类型,并访问嵌入类型的标识符。我们从第 10 行和第 24 行中的两个结构类型的声明开始。在第 10 行,我们声明了一个名为 user 的结构类型。在第 24 行,我们声明了另一个名为 admin 的结构类型。在声明 admin 类型的第 25 行,我们将 user 类型嵌入 admin 类型里。

要嵌入一个类型,只需要声明这个类型的名字就可以了。在第 26 行,我们声明了一个名为 level 的字段。注意声明字段和嵌入类型在语法上的不同。

一旦我们将 user 类型嵌入 admin,我们就可以说 user 是外部类型 admin 的内部类型。有了内部类型和外部类型这两个概念,就能更容易地理解这两种类型之间的关系。

代码的 17~21 行,展示了使用 user 类型的指针接收者声明名为 notify 的方法。这个方法只是显示一行友好的信息,表示将邮件发给了特定的用户以及邮件地址。

现在,让我们来看一下 main 函数,在 main 函数中展示了嵌入类型背后的机制。在第 32 行,创建了一个 admin 类型的值。内部类型的初始化是用结构字面量完成的。通过内部类型的名字可以访问内部类型。

对外部类型来说,内部类型总是存在的。这就意味着,虽然没有指定内部类型对应的字段名,还是可以使用内部类型的类型名,来访问到内部类型的值。

在代码的第 41 行,可以看到对 notify 方法的调用。这个调用是通过直接访问内部类型 user 来完成的。这展示了内部类型是如何存在于外部类型内,并且总是可访问的。不过,借助内部类型提升,notify 方法也可以直接通过 ad 变量来访问。

代码中的第 44 行中展示了直接通过外部类型的变量来调用 notify 方法。由于内部类型的标识符提升到了外部类型,我们可以直接通过外部类型的值来访问内部类型的标识符。让我们修改一下这个例子,加入一个接口,代码如下所示。
// 这个示例程序展示如何将嵌入类型应用于接口
package main

import (
    "fmt"
)

// notifier 是一个定义了
// 通知类行为的接口
type notifier interface {
    notify()
}

// user 在程序里定义一个用户类型
type user struct {
    name string
    email string
}

// 通过 user 类型值的指针
// 调用的方法
func (u *user) notify() {
    fmt.Printf("Sending user email to %s<%s>\n",
    u.name,
    u.email)
}

// admin 代表一个拥有权限的管理员用户
type admin struct {
    user
    level string
}

// main 是应用程序的入口
func main() {
    // 创建一个 admin 用户
    ad := admin{
        user: user{
            name: "john smith",
            email: "john@yahoo.com",
        },
        level: "super",
    }

    // 给 admin 用户发送一个通知
    // 用于实现接口的内部类型的方法,被提升到
    // 外部类型
    sendNotification(&ad)
}

// sendNotification 接受一个实现了notifier 接口的值
// 并发送通知
func sendNotification(n notifier) {
    n.notify()
}
上面代码所示的示例程序的大部分和之前的程序相同,只有一些小变化,在代码的第 8 行,声明了一个 notifier 接口。之后在第 53 行,有一个 sendNotification 函数,接受 notifier 类型的接口的值。从代码可以知道,user 类型之前声明了名为 notify 的方法,该方法使用指针接收者实现了 notifier 接口。

之后,让我们看一下 main 函数的改动,这里才是事情变得有趣的地方。在代码的第 37 行,我们创建了一个名为 ad 的变量,其类型是外部类型 admin。这个类型内部嵌入了 user 类型。

之后在第 48 行,我们将这个外部类型变量的地址传给 sendNotification 函数。编译器认为这个指针实现了 notifier 接口,并接受了这个值的传递。不过如果看一下整个示例程序,就会发现 admin 类型并没有实现这个接口。

由于内部类型的提升,内部类型实现的接口会自动提升到外部类型。这意味着由于内部类型的实现,外部类型也同样实现了这个接口。运行这个示例程序,会得到如下所示的输出。

代码清单 5-59 listing56.go 的输出

Sending user email to john smith<john@yahoo.com>

可以看到代码中内部类型的实现被调用了。

如果外部类型并不需要使用内部类型的实现,而想使用自己的一套实现,该怎么办?让我们看另一个示例程序是如何解决这个问题的,代码如下所示。
// 这个示例程序展示当内部类型和外部类型要
// 实现同一个接口时的做法
package main

import (
    "fmt"
)

// notifier 是一个定义了
// 通知类行为的接口
type notifier interface {
    notify()
}

// user 在程序里定义一个用户类型
type user struct {
    name string
    email string
}

// 通过user 类型值的指针
// 调用的方法
func (u *user) notify() {
    fmt.Printf("Sending user email to %s<%s>\n",
        u.name,
        u.email)
}

// admin 代表一个拥有权限的管理员用户
type admin struct {
    user
    level string
}

// 通过 admin 类型值的指针
// 调用的方法
func (a *admin) notify() {
    fmt.Printf("Sending admin email to %s<%s>\n",
        a.name,
        a.email)
}

// main 是应用程序的入口
func main() {
    // 创建一个 admin 用户
    ad := admin{
        user: user{
            name: "john smith",
            email: "john@yahoo.com",
        },
        level: "super",
    }

    // 给admin 用户发送一个通知
    // 接口的嵌入的内部类型实现并没有提升到
    // 外部类型
    sendNotification(&ad)

    // 我们可以直接访问内部类型的方法
    ad.user.notify()

    // 内部类型的方法没有被提升
    ad.notify()
}

// sendNotification 接受一个实现了 notifier 接口的值
// 并发送通知
func sendNotification(n notifier) {
    n.notify()
}
上面代码所示的示例程序的大部分和之前的程序相同,只有一些小变化,这个示例程序为 admin 类型增加了 notifier 接口的实现。当 admin 类型的实现被调用时,会显示 "Sending admin email"。作为对比,user 类型的实现被调用时,会显示 "Sending user email"。

main 函数里也有一些变化,在代码的第 46 行,我们再次创建了外部类型的变量 ad。在第 57 行,将 ad 变量的地址传给 sendNotification 函数,这个指针实现了接口所需要的方法集。

在第 60 行,代码直接访问 user 内部类型,并调用 notify 方法。最后,在第 63 行,使用外部类型变量 ad 来调用 notify 方法。这个示例程序的输出结果如下所示。

Sending admin email to john smith<john@yahoo.com>
Sending user email to john smith<john@yahoo.com>
Sending admin email to john smith<john@yahoo.com>

这次我们看到了 admin 类型是如何实现 notifier 接口的,以及如何由 sendNotification 函数以及直接使用外部类型的变量 ad 来执行 admin 类型实现的方法。这表明,如果外部类型实现了 notify 方法,内部类型的实现就不会被提升。

不过内部类型的值一直存在,因此还可以通过直接访问内部类型的值,来调用没有被提升的内部类型实现的方法。

编程帮,一个分享编程知识的公众号。跟着站长一起学习,每天都有进步。

通俗易懂,深入浅出,一篇文章只讲一个知识点。

文章不深奥,不需要钻研,在公交、在地铁、在厕所都可以阅读,随时随地涨姿势。

文章不涉及代码,不烧脑细胞,人人都可以学习。

当你决定关注「编程帮」,你已然超越了90%的程序员!

编程帮二维码
微信扫描二维码关注