GO学习笔记——包和封装(17)

既然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导入其他包时的一些初始化顺序:

  1. 如果一个main包里面导入其他包,其他的包将被顺序导入(按声明的顺序导入)
  2. 如果导入的包中有依赖包(导入包A依赖包B),会首先导入B包,然后初始化B包中的常量和变量,最后如果B包中有init,会自动执行init()(也就是说,会先初始化B包中的一些函数体外的常量和变量,再执行init函数
  3. 所有包导入完成后才会对main中常量和变量进行初始化,然后执行main中的init函数(如果有的话),最后执行main函数(所有包都是先执行init函数)
  4. 如果一个包被导入多次,则该包只会被导入一次(就像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


 

猜你喜欢

转载自blog.csdn.net/lvyibin890/article/details/83651170