Go中结构体(struct)的外部类型和内部类型

307 阅读6分钟

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

1. 基础概念: 外部类型和内部类型


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

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

user 是外部类型 admin 的内部类型

2. 内部类型提升

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

我们为user定义一个方法,使用指针接收者为其设置。

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

可以看到对 notify 方法的调用。这个调用是通过直接访问内 部类型 user 来完成的。这展示了内部类型是如何存在于外部类型内,并且总是可访问的。

不过, 借助内部类型提升,notify 方法也可以直接通过 ad 变量来访问

// 内部类型的方法也被提升到外部类型 
ad.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()
}

我们声明了一个 notifier 接口,有一个 sendNotification 函数,接受 notifier 类型的接口的值。从代码可以知道,user实现了接口中的notify方法。

在我们不了解结构体中的内部类型提升时,想到多态的思想时调用sendNotification()方法时

sendNotification(&user)   // 参数是实现了接口中的方法的user结构体

不过示例代码却不是这么做的,

sendNotification(&ad) //编译通过,不会报错

这里才是事情变得有趣的地方。我们创建了一个名为 ad 的变 量,其类型是外部类型admin。这个类型内部嵌入了 user 类型。我们将这个外部类型变量的地址传给 sendNotification 函数。编译器认为这个指针实现了 notifier 接口,并接受了这个值的传递。不过如果看一下整个示例程序,就会发现 admin 类型并没有实现这个接口。 由于内部类型的提升,内部类型实现的接口会自动提升到外部类型。这意味着由于内部类型的实现,外部类型也同样实现了这个接口。

这也是类型内部提升的一种。

如果内部类型admin也实现了notifier方法呢?


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

答案是: 如果外部类型 实现了 notify 方法,内部类型的实现就不会被提升。不过内部类型的值一直存在,因此还可以 通过直接访问内部类型的值,来调用没有被提升的内部类型实现的方法。


sendNotification(&ad)

当调用sendNotification()时,执行的admin的方法实现。

当然我们也可以直接方法user实现的方法notify

ad.user.notify()

3. 公开或未公开的标识符

要想设计出好的 API,需要使用某种规则来控制声明后的标识符的可见性. Go语言中通过设置开头的字母大小写控制可见性(变量,结构体,方法等)


package counters

// alertCounter 是一个未公开的类型 
type alertCounter int



package main

//编译器会给出错误提示
//不能引用未公开的名字未定义:counters.alertCounter
counters.alertCounter 


这里给出了一种解决方法,通过创建New()工厂函数

 // New 创建并返回一个未公开的 alertCounter 类型的值
 func New(value int) alertCounter {
  return alertCounter(value)  
 }

package main

// 使用 counters 包公开的 New 函数来创建一个未公开的类型的变量 
counter := counters.New(10)

这个 New 函数 返回的值被赋给一个名为 counter 的变量。这个程序可以编译并且运行,但为什么呢?New 函 数返回的是一个未公开的 alertCounter 类型的值,而 main 函数能够接受这个值并创建一个未公开的类型的变量

答案是:

在这个示例代码中,alertCounter 是一个未公开的类型,因为它的名称以小写字母开头。但是,New 函数返回了 alertCounter 类型的值,这意味着通过调用 New 函数,我们可以获得一个 alertCounter 类型的值。由于 New 函数是可导出的(它的名称以大写字母开头),所以其他包可以通过调用 New 函数来获取 alertCounter 类型的值,然后使用该值进行操作。

需要注意的是,尽管其他包可以通过调用 New 函数来获取 alertCounter 类型的值,但是它们不能直接访问 alertCounter 类型或其字段,因为它是未公开的。

再来看下面这段代码:


package entities

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

// Admin 在程序里定义了管理员
type Admin struct {
	user   // 嵌入的类型未公开
	Rights int
}

adminuser是未公开的嵌入类型或者说外部类型

func main() {
    // 创建 entities 包中的 Admin 类型的值
    a := entities.Admin{
        Rights: 10,
    }

    // 设置未公开的内部类型的
    // 公开字段的值
    a.Name = "Bill"
    a.Email = "bill@email.com"
}

这里通过创建 entities 包中的 Admin 类型的值 a

他可以直接访问未公开的结构体user中的值,为什么?

答案是:

内部类型 user 是未公开的,这段代码无法直接通过结构字面量的方式初始化该内部类型, 不过内部类型里声明的字段依旧是公开的! 内部类型的标识符提升到了外部类型,因为 user 类型是未公开的,所以这里没有直接访问内部类型。


参考书籍:《GO语言实战》