既然GO支持封装,那么相比于C++的三大访问属性:private,protected,public,GO只有private和public,因为protected是因继承而产生的。而C++的访问属性是根据类来说的,在一个类中相当于外界的属性;但是GO中没有类,GO中的访问属性是相对于包来说的,这一章就来讲一讲GO中的包,以及包是怎么做到封装的。
包其实相当于一个目录,如果我们把所有的源文件写在一个文件中,那么会不方便我们管理。这其实也是,在C++中我们也分头文件和源文件,而且会有多个源文件。
包用于组织GO源代码,提供了对代码更好的管理与重用性。
总之,这么理解,把一些功能相近的源文件封装到一个包中,那么这个包其实就是这些文件的一个目录。
一个目录只能有一个包
因为上面的理解,包其实就是一个目录,那么GO就规定了一个目录就只能有一个包,如果有两个包就会报错,下面来看一下。
我新建了一个目录叫newpackage,在这个目录下创建了一个test1文件,那么我的GoLand会自动帮我给我的这个文件添加这么一句话
package newpackage
意思是,我这个test1文件是封装在newpackage这个包中的。这边包名默认就是这个目录名。但其实包名不一定要和目录名一样,可以改,只要在同一个目录下只有一个包名就可以了(但是默认约定包名和目录名相同)。
我再创建了一个test2文件,也自动添加了上面那句话。但是我把包名改成了newpackage1,这个是编译器就报错了。
表示在同一个目录中有多个包。
所以包就是一个目录,一个目录就是一个包,包名可以不和目录名相同,但是一个目录下只能有一个包。
main包包含可执行程序入口
要生成Go语言可执行程序,必须要有main的package包,且必须在该包下有main函数
就像C++要有main函数一样,GO也要有main函数,像之前的文章的测试代码都有一个main函数,且都没有创建别的包,都是在main包中测试的。
所以一个GO程序如果需要执行:
- 必须要有main包(包名必须是main,不可以是别的名)
- main包内必须要有main入口函数
- 如果main包中没有入口函数不能执行,如果只是别的名的包中有入口函数也不能执行
为结构体定义的方法必须在一个包内
GO的面向对象使用结构体来做的,对一个结构体可以定义很多方法。
因为需要封装的原因,所以一个结构体中的方法必须在一个包内定义。也就是说,如果结构体定义在了包A中,那么它对应的方法也必须都定义在包A中,不可以在别的包中定义,这样就破坏了封装的概念。但是可以是不同的文件,即可以在同一个包中的不同源文件中定义结构体的方法。
所以,为了保证封装,为结构体定义的方法必须在一个包内。
创建自定义的包
下面我们把之前一章的测试用例,封装成一个List包,来创建一个我们自定义的包。
先来看一下目录结构
这边将定义的List放到了一个List包的一个源文件list.go中
package List
import "fmt"
//定义一个单链表的节点
type Node struct {
Next *Node
Val int
}
//定义一个方法来遍历这个单链表
func (node *Node) PrintList(){
for node != nil{
fmt.Print(node.Val)
if node.Next != nil {
fmt.Print("->")
}
node = node.Next
}
}
为了对别的包可见,因此我们将变量名改为了首字母大写,GO语言变量名首字母大写表示对别的包可见(public),首字母小写表示对别的包不可见(private)
剩下的main.go中,import了我们自定义的这个包,且它里面只有一个main函数
package main
import "struct/List"
func main() {
root := List.Node{Val:1}
root.Next = &List.Node{Val:2}
root.Next.Next = &List.Node{Val:3}
root.Next.Next.Next = &List.Node{Val:4}
root.PrintList() //使用该方法
}
执行程序
1->2->3->4
Process finished with exit code 0
这样,我们就把定义的List放到了一个包中,更方便我们管理我们的源文件。
我们要导入一个别的包都要用到import关键字,下面来说说导入包时的技巧以及import关键字的使用。
init函数
说import之前先说一下init函数。每个包都可以包含一个init函数,这其实就是一个初始化函数,在调用这个包之前,先做一些对其的初始化操作,这样在我们使用这个包之前,一些必要的init操作就已经被做完了,不用我们再手动去执行一遍。
init 函数不应该有任何返回值类型和参数,在我们的代码中也不能显式地调用它。
我们为上面的List包建一个init函数。
package List
import "fmt"
//加入的init函数
func init(){
fmt.Println("before use package List")
}
//定义一个单链表的节点
type Node struct {
Next *Node
Val int
}
//定义一个方法来遍历这个单链表
func (node *Node) PrintList(){
for node != nil{
fmt.Print(node.Val)
if node.Next != nil {
fmt.Print("->")
}
node = node.Next
}
}
执行结果
before use package List
1->2->3->4
可以看到这里也执行了init函数,并且在程序main函数执行之前执行了,其实它就是在导入这个包时就已经自动执行了。
import导入包初始化顺序
这边再说一下import导入其他包时的一些初始化顺序:
- 如果一个main包里面导入其他包,其他的包将被顺序导入(按声明的顺序导入)
- 如果导入的包中有依赖包(导入包A依赖包B),会首先导入B包,然后初始化B包中的常量和变量,最后如果B包中有init,会自动执行init()(也就是说,会先初始化B包中的一些函数体外的常量和变量,再执行init函数)
- 所有包导入完成后才会对main中常量和变量进行初始化,然后执行main中的init函数(如果有的话),最后执行main函数(所有包都是先执行init函数)
- 如果一个包被导入多次,则该包只会被导入一次(就像C++里的pragma once),不会被重复导入
import特殊用法
1.别名,将导入的包名命名为另一个容易记的别名
有时候当包名很长时我们可以对该包起一个简单容易记的别名,这里我们对上述main包中导入的List包进行一个重命名做测试
package main
import A "struct/List" //起别名A
func main() {
root := List.Node{Val:1}
root.Next = &List.Node{Val:2}
root.Next.Next = &List.Node{Val:3}
root.Next.Next.Next = &List.Node{Val:4}
root.PrintList() //使用该方法
}
这里我们起了别名A,但是上述代码程序会出错
.\main.go:4:10: undefined: List
所以一旦起了别名,原来的名字就不可以用了,必须要全部变为别名。
package main
import A "struct/List"
func main() {
root := A.Node{Val:1}
root.Next = &A.Node{Val:2}
root.Next.Next = &A.Node{Val:3}
root.Next.Next.Next = &A.Node{Val:4}
root.PrintList() //使用该方法
}
上面这样就可以了。
2.点(.)操作
点(.)标识的包导入后,调用该包中的函数时可以省略前缀包名(不建议,容易引起冲突,其实就是相当于C++中的 using namespace,使用某个命名空间),还是拿之前的程序做测试。
package main
import . "struct/List" //在调用该包时省略包名
func main() {
root := List.Node{Val:1}
root.Next = &List.Node{Val:2}
root.Next.Next = &List.Node{Val:3}
root.Next.Next.Next = &List.Node{Val:4}
root.PrintList() //使用该方法
}
但是上述代码还是会出错
.\main.go:3:8: imported and not used: "struct/List"
.\main.go:6:10: undefined: List
所以和之前一样,一旦一个包名被点(.)操作了,那么原来的包名也不可以用了,用到的地方必须全部省略包名
package main
import . "struct/List" //在调用该包时省略包名
func main() {
root := Node{Val:1} //在调用时,省略包名
root.Next = &Node{Val:2}
root.Next.Next = &Node{Val:3}
root.Next.Next.Next = &Node{Val:4}
root.PrintList() //使用该方法
}
上面这样就可以了。
3.下划线(_)操作
导入了包,却不在代码中使用它,这在 Go 中是非法的。当这么做时,编译器是会报错的。其原因是为了避免导入过多未使用的包,从而导致编译时间显著增加。
然而,在程序开发的活跃阶段,又常常会先导入包,而暂不使用它,所以这里就可以使用下划线(_)操作:
导入该包,但不导入整个包,而是执行该包中的init函数,因此无法通过包名来调用包中的其他函数。
package main
import _ "struct/List"
func main() {
root := List.Node{Val:1}
root.Next = &List.Node{Val:2}
root.Next.Next = &List.Node{Val:3}
root.Next.Next.Next = &List.Node{Val:4}
root.PrintList() //使用该方法
}
上述代码会出现,因为我们调用了该包中的内容。
.\main.go:10:10: undefined: List
package main
import _ "struct/List"
func main() {
}
这边我们并没有使用该包中的任何内容,程序不会编译出错,而会去执行该包中的init函数。
执行结果
before use package List