Go 学习笔记(十八)struct 的导出和暴露问题

本文原创地址:博客园骏马金龙Go 基础系列:struct 的导出和暴露问题

struct 的导出和暴露问题

关于 struct 的导出

struct 的属性是否被导出,也遵循大小写的原则:首字母大写的被导出,首字母小写的不被导出

所以:

  1. 如果 struct 名称首字母是小写的,这个 struct 不会被导出。连同它里面的字段也不会导出,即使有首字母大写的字段名
  2. 如果 struct 名称首字母大写,则 struct 会被导出,但只会导出它内部首字母大写的字段,那些小写首字母的字段不会被导出

也就是说,struct 的导出情况是混合的。

但并非绝对如此, 如果 struct 嵌套了,那么即使被嵌套在内部的 struct 名称首字母小写,也能访问到它里面首字母大写的字段

例如:

type animal struct{
	name string
	Speak string
}
type Horse struct {
	animal
	sound string
}

Horse 中嵌套的 animal 是小写字母开头的,但 Horse 是能被导出的,所以能在其它包中使用 Horse struct,其他包也能访问到 animal 中的 Speak 属性。

很多时候,Horse 这个名字是不安全的,因为这表示导出 Horse 这个 struct 给其他包,也就是将 Horse 给暴露出去了,外界可以直接打开 Horse 这个 "黑匣子"。

但如果不将 Horse 导出,如何能在其它包构建出 Horse 实例?见下文。

不要暴露 struct

很多时候,不应该将某包 (如包 abc) 中的 struct(如 animal)直接暴露给其它包,暴露意味着打开了那个 "黑匣子",所以 struct 会以小写字母开头,不将其导出。

这时在外界其它包中构建包 abc 的 animal,就没法直接通过以下几种方式实现:

  • var xxx abc.animal
  • new(abc.animal)
  • &abc.animal{...}
  • abc.animal{...}

例如,下面的是错误的:

// abc/abc.go文件内容:
package abc

type animal struct{
	name string
	Speak string
}
// test.go内容:
package main

import "./abc"

func main() {
	// 全都错误
	var t1 abc.animal
	t2 := new(abc.animal)
	t3 := &abc.animal{}
	t4 := abc.animal{}
}

那么如何在外界构建隐藏起来的 struct 实例?这时可以在 abc 包中写一个可导出的函数,通过这个函数来构建 struct 实例。例如:

// abc/abc.go文件内容:
package abc

type animal struct{
	name string
	Speak string
}

func NewAnimal() *animal{
	a := new(animal)
	return a
}

// test.go内容:
package main

import (
	"fmt"
	"./abc"
)

func main() {
	t1 := abc.NewAnimal()
//  t1.name = "haha"    // 无法访问name属性
	t1.Speak = "hhhh"
	fmt.Println(t1.Speak)
}

上面的代码一切正常,在 main 包中可以通过 NewAnimal()构建出 abc 包中未导出的 animal struct。注意,上面 NewAnimal() 中是使用 new()函数构造实例的,它返回的是实例的指针,至于如何构造实例,完全可以根据自己的需求,但对于 struct 类型来说,一般都是使用指针的,也就是完全可以将 new() 通用化。

由于 animal 中的 name 字段是不导出的字段,所以在外界即便是通过 NewAnimal() 构建出了 animal 实例,也无法访问该实例的 name 属性,所以没法为 name 字段赋值。换句话说,name 属性永远是初始化的 0 值。

因此,为了让构建实例时自定义 name 属性,需要在构造方法 NewAnimal()上指定设置给 name 属性的参数。修改 NewAnimal() 函数:

func NewAnimal(name string) *animal{
	a := new(animal)
	a.name = name
	return a
}

然后在其它包中构建 animal 实例:

t1 := abc.NewAnimal("longshuai")

虽然其它包中构建的 animal 实例已经具备了 name 属性,但还是无法访问该实例的 name 属性。所以,在 abc 包中继续写一个可导出的方法,该方法用于获取实例的 name 属性:

// abc/abc.go中添加:
func (a *animal) GetName() string {
	return a.name
}

于是外界包中可以通过这个导出的方法获取实例的 name 属性:

t1 := abc.NewAnimal("longshuai")
fmt.Println(t1.GetName())

实际上,上面 NewAnimal()构造对象时,可以不用传递 name 参数,而是像 GetName() 一样,写一个专门的可导出方法来设置实例的 name 属性。改写 abc/abc.go 中的代码:

func NewAnimal() *animal{
	a := new(animal)
	return a
}
func (a *animal) SetName(name string){
	a.name = name
}

现在,abc/abc.go 中的 animal struct 就完全对外隐藏了。

但需要注意的是,上面的 setter 类方法SetName()不能同时被 2 个或多个线程修改,否则值被覆盖,出现线程安全问题,可以使用 sync 包或者 goroutine 和 channel 来解决这个问题。

嵌套 struct 中的方法导出问题

当内部 struct 嵌套进外部 struct 时,内部 struct 的方法也会被嵌套,也就是说外部 struct 拥有了内部 struct 的方法。

但是需要注意方法的首字母大小写问题。由于内、外 struct 在同一包内,所以 直接在该包内构建外部 struct 实例,外部 struct 实例是可以直接访问内部 struct 的所有方法的。但如果在其它包内构建外部 struct 实例,该实例将无法访问内部 struct 中首字母小写的方法

以下是在同一个包内测试,外部实例可以直接调用内部 struct 的方法:

package main

import (
	"fmt"
)

type person struct {
	name string
	age  int
}

// 未导出方法
func (p *person) speak() {
	fmt.Println("speak in person")
}

// 导出的方法
func (p *person) Sing() {
	fmt.Println("Sing in person")
}

// Admin exported
type Admin struct {
	person
	salary int
}

func main() {
	a := new(Admin)
	a.speak()  // 正常输出
	a.Sing()   // 正常输出
}

执行结果时a.speak()a.Sing()都正常输出。

以下是不同包内测试,struct 定义在 abc/abc.go 文件中,main 在 test.go 中,它们的目录结构如下:

$ tree .
.
├── abc
│   └── abc.go
├── test.go

abc/abc.go 的内容为:

package abc

import "fmt"

// 未导出的person
type person struct {
	name string
	age  int
}

// 未导出的方法
func (p *person) speak() {
	fmt.Println("speak in person")
}

// 导出的方法
func (p *person) Sing() {
	fmt.Println("Sing in person")
}

// Admin exported
type Admin struct {
	person
	salary int
}

test.go 的内容为:

package main

import "./abc"

func main() {
	a := new(abc.Admin)

	// 下面报错
//  a.speak()

	// 下面正常
	a.Sing()
}

执行结果是,a.speak()报错,但a.Sing()正常。
上一篇 Go 学习笔记(十七)Go 中的方法
Go 学习笔记(目录)
下一篇 Go 学习笔记(十九)Go 接口