【不积小流,无以成江海】关于Go的导出标识符机制你确定了解了吗

156 阅读8分钟

@TOC

最近笔者的boss分享了一篇文章,让我重新对导出标记符机制有了更深入的认识。所以有了这篇阅读笔记,不知道你们对这个机制了解的怎么样啊?下面我会提出几个问题,如果你们都能肯定的说出答案,就可以划走,下一篇啦。

Q1: Go的导出标识符机制是否允许在某些情况下,即使类型本身是非导出的,其导出字段却可以被包外的代码访问呢? 那么该类型的导出方法是否能被访问呢? Q2: 非导出类型是否可以实现某个外部包定义的接口呢 ? Q3:要是当非导出类型被嵌入导出结构体还能使用其导出方法和导出字段吗? Q4:非导出类型是否可以用作泛型函数和泛型类型的类型实参 ?

A:答案将在这篇文章中循序渐进的写出。你也可以划到最后直接看总结确认你的想法。

关于导出标识符的基础认识

在GO语言规范中,明确提出导出标识符的定义:

  1. 标识符名称的第一个字符是 Unicode 大写字母
  2. 标识符在 package 块中声明,或者它是字段名称方法名称

第二点中,没有对于某类型的字段名和方法名之外的该类型本身也要是导出的要求。这样说可能表述不清晰,下面举几个例子来解释。

1.包外访问非导出类型的导出字段和导出方法

我们创建一个非导出类型mystruct,并放入包mypackage里。实现了该类型的导出字段Field和导出方法M1 M2。

package mypackage

import "fmt"

type myStruct struct {
	Field string // 导出的字段
}

// NewMyStruct1是一个导出的函数,返回myStruct的指针
func NewMyStruct1(value string) *myStruct {
	return &myStruct{Field: value}
}

// NewMyStruct2是一个导出的函数,返回myStruct类型变量
func NewMyStruct2(value string) myStruct {
	return myStruct{Field: value}
}

func (m *myStruct) M1() {
	fmt.Println("invoke *myStruct's M1")
}

func (m myStruct) M2() {
	fmt.Println("invoke myStruct's M2")
}

然后尝试访问mystruct的Field字段。

package main

import (
	"demo/mypackage"
	"fmt"
)

func main() {
	// 通过导出的函数获取myStruct的指针
	ms1 := mypackage.NewMyStruct1("Hello1")

	// 尝试访问Field字段
	fmt.Println(ms1.Field) // Hello1

	// 通过导出的函数获取myStruct类型变量
	ms2 := mypackage.NewMyStruct1("Hello2")

	// 尝试访问Field字段
	fmt.Println(ms2.Field) // Hello2
}

然后尝试调用M1和M2方法

package main

import (
	"demo/mypackage"
)

func main() {
	// 通过导出的函数获取myStruct的指针
	ms1 := mypackage.NewMyStruct1("Hello1")
	ms1.M1() //invoke *myStruct's M1
	ms1.M2() //invoke myStruct's M2

	// 通过导出的函数获取myStruct类型变量
	ms2 := mypackage.NewMyStruct2("Hello2")
	ms2.M1() //invoke *myStruct's M1
	ms2.M2() //invoke myStruct's M2
}

通过mystruct的指针和副本的两种形式,都成功访问了导出字段Field和导出方法。

类型方法集合的判定

这里我们对于上面M1和M2两个方法进行一下补充描述。

func (m *myStruct) M1() {
    fmt.Println("invoke *myStruct's M1")
}

func (m myStruct) M2() {
    fmt.Println("invoke myStruct's M2")
}

M1 是一个指针接收者方法,而 M2 是一个值接收者方法。

调用情况分析

  • 指针接收者方法(M1
    • 只能通过指向类型的指针来调用。
    • 例如:
var m *myStruct = &myStruct{}
m.M1() // 可以调用
  • 值接收者方法(M2)
    • 可以通过类型的值或指针来调用。
    • 例如:
var m myStruct
m.M2() // 可以调用

var p *myStruct = &myStruct{}
p.M2() // 也可以调用

结论

*myStruct 类型的变量可以调用 M1 和 M2 两个方法。 myStruct 类型的变量只能调用 M2 方法

如果你有一个 *myStruct 类型的变量,那么它既可以调用 M1 方法,也可以调用 M2 方法。

通过构造函数创建的实例可以调用其方法,无论是指针接收者方法还是值接收者方法。这是因为 Go 语言在调用方法时,会自动处理值和指针之间的转换。具体来说:

  • 如果方法是以值接收者定义的,那么即使实例是值类型,Go 也会将其视为指针来调用该方法。
  • 如果方法是以指针接收者定义的,那么即使实例是值类型,Go 也会将其转换为指针来调用该方法。

以上已经通过举例完全解决了前面提出的问题。但是我们可以提出下一个问题了,Q2: 非导出类型是否可以实现某个外部包定义的接口呢 ?

2.非导出类型实现外部包接口的尝试

go语言定义:如果某个类型T实现了某个接口类型I的方法集合中全部方法,我们就说T实现了I,T的实例可以赋值给I类型的接口变量。

这里对于调用进行了修改,非导出类型的导出字段和导出方法没有变。

调用 main.go

package main

import (
    "demo/mypackage"
)

// 定义一个导出的接口
type MyInterface interface {
    M1()
    M2()
}

func main() {
    var mi MyInterface

    // 通过导出的函数获取myStruct的指针
    ms1 := mypackage.NewMyStruct1("Hello1")
    mi = ms1
    mi.M1()
    mi.M2()

    // 通过导出的函数获取myStruct类型变量
    //ms2 := mypackage.NewMyStruct2("Hello2")
    //mi = ms2 // compile error: mypackage.myStruct does not implement MyInterface
}

这里只有*mystruct实现了所有方法,成功实现了外部包接口。

那我来设想一下,Q3:要是当非导出类型被嵌入导出结构体还能使用其导出方法和导出字段吗?

3.非导出类型用作嵌入字段

我们设计新的导出结构体Exported,并嵌入非导出类型。同时为导出结构体定义方法M2。

package mypackage

import "fmt"

type nonExported struct {
	Field string // 导出的字段
}

// Exported 是导出的结构体,嵌入了nonExported
type Exported struct {
	nonExported // 嵌入非导出结构体
}

func NewExported(value string) *Exported {
	return &Exported{
		nonExported: nonExported{
			Field: value,
		},
	}
}

// M1是导出的函数
func (n *nonExported) M1() {
	fmt.Println("invoke nonExported's M1")
}

// M2是导出的函数
func (e *Exported) M2() {
	fmt.Println("invoke Exported's M2")
}

修改main.go实现调用。

package main

import (
	"demo/mypackage"
	"fmt"
)

// 定义一个导出的接口
type MyInterface interface {
	M1()
	M2()
}

func main() {
	ms := mypackage.NewExported("Hello")
	fmt.Println(ms.Field) // 访问嵌入的非导出结构体的导出字段

	ms.M1() // 访问嵌入的非导出结构体的导出方法 invoke nonExported's M1

	var mi MyInterface = ms
	mi.M1() //invoke nonExported's M1
	mi.M2() //invoke Exported's M2
}

通过执行,我们可以总结出作为嵌入字段的非导出类型的导出字段与方法会被自动promote到外部类型中,通过外部类型的变量可以直接访问这些字段以及调用这些导出方法。这些方法还可以作为外部类型方法集中的一员,来作为满足特定接口类型(如上⾯面代码中的MyInterface)的条件。

最后一个思考,Q4:非导出类型是否可以用作泛型函数和泛型类型的类型实参

4.非导出类型用作泛型实现的类型实参

重新回到这个非导出类型,及其构造方法、导出字段和导出方法

package mypackage

import "fmt"

// 定义一个非导出的结构体
type nonExported struct {
	Field string
}

// 导出的方法
func (n *nonExported) M1() {
	fmt.Println("invoke nonExported's M1")
}

func (n *nonExported) M2() {
	fmt.Println("invoke nonExported's M2")
}

// 导出的函数,用于创建非导出类型的实例
func NewNonExported(value string) *nonExported {
	return &nonExported{Field: value}
}

现在,将其用于泛型函数。泛型可以类比java中的,自行回顾一下,这里不过多铺垫。我们设计泛型函数,将参数类型设置为MyInterface作为约束,此时非导出类型满足其M1 M2方法。

package main

import (
	"demo/mypackage"
)

// 定义一个用作约束的接口
type MyInterface interface {
	M1()
	M2()
}

func UseNonExportedAsTypeArgument[T MyInterface](item T) {
	item.M1()
	item.M2()
}

// 定义一个带有泛型参数的新类型
type GenericType[T MyInterface] struct {
	Item T
}

func NewGenericType[T MyInterface](item T) GenericType[T] {
	return GenericType[T]{Item: item}
}

func main() {
	// 创建非导出类型的实例
	n := mypackage.NewNonExported("Hello")

	// 调用泛型函数,传入实现了MyInterface的非导出类型
	UseNonExportedAsTypeArgument(n) // ok2
    //invoke nonExported's M1
    //invoke nonExported's M2

	g := NewGenericType(n)
	g.Item.M1()     //invoke nonExported's M1
}

我们将构造函数创建的非导出类型实例,传入泛型函数中,是不会报错的。 Go 会通过泛型的类型参数自动推导机制推断出类型实参的类型。也可以通过泛型函数支持的类型参数的自动推导间接获得GenericType的类型实参。

前面的诸多示例证明了:即使类型本身是非导出的,但其内部的导出字段以及它的导出方法依然可以在外部包中使用,并且在实现接口、嵌入字段、泛型等使用场景下均有效。

总结

通过以上关于导出标识符层层推导与思考,我们现在已经得出:

A: 尽管某些类型是非导出的,其内部的导出字段和方法依然可以在包外访问。此外,非导出类型在实现接口、嵌入字段和泛型中也展现出良好的应用。

这种设计不仅促进了封装和接口实现的灵活性,还允许开发者通过构造函数返回非导出类型的实例,从而有效控制实例的创建与管理。这种方式帮助隐藏实现细节,简化外部接口,使得代码结构更加清晰。 希望各位读者能理解到这种思维,并应用到开发中去,才真正掌握了。当然你也可以收藏此文,忘了就放出来看看。温故而知新~

以上源码均引用自: 源码传送门 大家可以自行尝试