前言:下面的内容都是边看【飞雪无情】大佬的博客,自己边整理的,其中部分内容有过删改,推荐大家去看原作者的博客进行学习,本博客内容仅作为自己的学习笔记。在此之前,我跟着b站韩茹老师刷完了Go语言入门教程。
学习链接:https://www.flysnow.org/archives/
参考书籍:《Go语言实战》
十一、Go 标志符可见性(封装)
Go的标志符,这个翻译怪怪的,可以理解为Go的变量、类型、字段等。这里的可见性,也就是说那些方法、函数、类型或者变量字段的可见性,比如哪些方法不想让另外一个包访问,我们就可以把它们声明为非公开的;如果需要被另外一个包访问,就可以声明为公开的,和Java语言里的作用域类似。
在Go语言中,没有特别的关键字来声明一个方法、函数或者类型是否为公开的,Go语言提供的是以大小写的方式进行区分的,如果一个类型的名字是以大写开头,那么其他包就可以访问;如果以小写开头,其他包就不能访问。
package common
type count int
package main
import (
"flysnow.org/hello/common"
"fmt"
)
func main() {
c := common.count(10) // (补充:会报错)
fmt.Println(c)
}
这是一个定义在common
包里的类型count
,因为它的名字以小写开头,所以我们不能在其他包里使用它,否则就会报编译错误。
./main.go:9: cannot refer to unexported name common.count
因为这个类型没有被导出,如果我们改为大写,就可以正常编译运行了,大家可以自己试试。
现在这个类型没有导出,不能使用,现在我们修改下例子,增加一个函数,看看是否可行。
package common
type count int
func New(v int) count {
return count(v)
}
func main() {
c := common.New(100)
fmt.Println(c)
}
这里我们在common
包里定义了一个导出的函数New
,该函数返回一个count
类型的值。New
函数可以在其他包访问,但是count
类型不可以,现在我们在main包里调用这个New
函数,会发现是可以正常调用并且运行的,但是有个前提,必须使用:=
这样的操作符才可以,因为它可以推断变量的类型。
这是一种非常好的能力,试想,我们在和其他人进行函数方法通信的时候,只需约定好接口,就可以了,至于内部实现,使用方是看不到的,隐藏了实现。
package common
import "fmt"
func NewLoginer() Loginer{ // (补充:返回Loginer接口的名为NewLoginer的函数)
return defaultLogin(0)
}
type Loginer interface { // (补充:定义Loginer接口)
Login()
}
type defaultLogin int
func (d defaultLogin) Login(){
fmt.Println("login in...")
}
func main() {
l := common.NewLoginer() // (补充:这里l被初始化为Loginer接口类型)
l.Login()
}
以上例子,我们对于函数间的通信,通过Loginer
接口即可,在main函数中,使用者只需要返回一个Loginer
接口,至于这个接口的实现,使用者是不关心的,所以接口的设计者可以把defaultLogin
类型设计为不可见,并让它实现接口Loginer
,这样我们就隐藏了具体的实现。如果以后重构这个defaultLogin
类型的具体实现时,也不会影响外部的使用者,极为方便,这也就是面向接口的编程。
假如一个导出的结构体类型里,有 一个未导出的字段,会出现怎样的问题。
type User struct {
Name string
email string // (补充:在其他包声明和初始化User时,无法初始化email)
}
当我们在其他包声明和初始化User
的时候,字段email
是无法初始化的,因为它没有导出,无法访问。此外,一个导出的类型,包含了一个未导出的方法也一样,也是无法访问的。
我们再扩展,导出和未导出的类型相互嵌入,会有什么什么样的发现?
type user struct {
Name string
}
type Admin struct {
user
}
被嵌入的user
是未导出的,但是它的外部类型Admin
是导出的,所以外部可以声明初始化Admin
。
func main() {
var ad common.Admin
ad.Name = "张三"
fmt.Println(ad)
}
这里因为user
是未导出的(补充:这里的user首字母是小写),所以我们不能再使用字面值直接初始化user
了,所以只能先定义一个Admin
类型的变量,再对Name
字段初始化。这里Name
可以访问是因为它是导出的,在user
嵌入到Admin
中时,它已经被提升为Admin
的字段,所以它可以被访问。(补充:注意字段的自动提升,应该有更进一步的理解)
如果我们还想使用:=
操作符怎么做呢?
ad := common.Admin{}
字面值初始化的时候什么都不做就好了,因为user
未导出,所以我们不能直接使用字面值初始化Name
字段。
(补充:说来说去,只要外部Admin
是导出的,内部user | User
不管是不是导出的,内部的首字母大写的字段如Name
,外部都是可以访问的,但是不能直接初始化Name
字段。)
(补充:如果Admin
中有导出的字段,那么在common.Admin{}
中进行初始化,而Name
字段放在外面访问,ad.Name = "xxx"
)
还有要注意的是,因为user
未导出,所以我们不能通过外部类型访问内部类型了,也就是说ad.user
这样的操作,都会编译不通过。
最后,我们做个总结,导出还是未导出,是通过名称首字母的大小写决定的,它们决定了是否可以访问,也就是标志符的可见性。
对于.
操作符的调用,比如调用类型的方法,包的函数,类型的字段,外部类型访问内部类型等等,我们要记住:.
操作符前面的部分导出了,.
操作符后面的部分才有可能被访问;如果.
前面的部分都没有导出,那么即使.
后面的部分是导出的,也无法访问。
例子 | 可否访问 |
---|---|
Admin.User.Name | 是 |
Admin.User.name | 否 |
Admin.user.Name | 否 |
Admin.user.name | 否 |
以上表格中Admin
为外部类型,User(user)
为内部类型,Name(name)
为字段,以此来更好的理解最后的总结,当然方法也适用这个表格。