七叶笔记 » golang编程 » golang中的面向"对象"

golang中的面向"对象"

写在前面

Go语言中的面向”对象”和其他语言非常不同,仅仅支持封装,不支持继承和多态。 那么你可能要问了,仅仅依靠封装能实现一些较为复杂的事情么?Go语言通过接口和封装来实现较为复杂的事,所以更多的是成为接口编程。

既然只有封装,就没有class(类),只有struct( 结构体 )。

结构体

结构体是用户定义的类型,表示若干个字段的集合。当需要将多个数据分组到一个整体,而不是将每个数据作为单独的类型进行维护时,可以使用结构体。是不是有点类的概念?

二分搜索树例子理解结构体知识

下面尝试通过一个二分搜索树的例子来介绍关于结构体的知识。二分搜索树分为3部分,某个节点的值,节点的左子树,节点的右子树。

其实结构体的声明和面向对象中类的声明非常类似:

//定义一个二分搜索树
type treeNode struct{
 value int //节点值为int类型
 left,  right  *treeNode //左右子树为指针类型
}
 

在声明好结构体后,接下来就是定义它了:

func main() {
 var root treeNode //定义一个二分搜索树对象
 root = treeNode{value: 2} //二分搜索树root节点初始化
 root.left = &treeNode{} //二分搜索树root节点左子树初始化
 root.right = &treeNode{value: 6, left:  nil , right: nil} //二分搜索树root节点右子树初始化
 // root.right = &treeNode{6,nil, nil} 
 root.left.left =new(treeNode) //给二分搜索树root节点的左子树的左侧创建一个节点
 nodes :=[]treeNode{
 {value:3},
 {},
 {5,nil,nil},
 {8,nil,&root},
 }
 fmt.Println(nodes)
}
//运行结果:
[{3 <nil> <nil>} {0 <nil> <nil>} {5 <nil> <nil>} {8 <nil> 0xc000048420}]
 

你发现了么,声明结构体就相当于Java中的创建一个类,然后 实例化 这个结构体就是Java中类的实例化过程。 在Go语言中,不论是地址还是结构体本身,一律使用.来访问成员。

var root treeNode //定义一个二分搜索树对象
root = treeNode{value: 2} //二分搜索树root节点初始化
root.left = &treeNode{} //二分搜索树root节点左子树初始化
root.right = &treeNode{value: 6, left: nil, right: nil} //二分搜索树root节点右子树初始化
 // root.right = &treeNode{6,nil, nil} 
root.left.left =new(treeNode) //给二分搜索树root节点的左子树的左侧创建一个节点
 

Go语言提供了很多实例化结构体的方法,因此结构体是没有构造方法的。 当然如果你可以创建一个工厂方法用于实例化构造体:

//用于创建一个结构体对象
func createTreeNode(value int) *treeNode{
 return &treeNode{value:value} //这是一个 局部变量 的地址,但是Go语言允许返回局部变量
}
 

相信聪明的你发现这个createTreeNode函数返回了一个局部对象的地址,这在C++中是不允许的,但是Go语言支持允许返回局部变量地址。然后使用该方法创建一个结构体对象:

 root.left.right = createTreeNode(9)
//运行结果:
&{9 <nil> <nil>}
 

看到这里你可能会问,返回的局部对象是存在于堆上还是栈上呢?像C++,它的局部变量是分配在栈中,函数一旦退出,则局部变量会被销毁,只有定义在堆上的变量才能传递出去,不过这样就有一个麻烦,这个变量就需要你手动释放。而在Java中,通过New关键词生成的对象一般都在堆上,然后等到不使用的时候由垃圾回收机制回收。在Go语言中,你不需要知道它具体分配在何处,因为它是由Go语言编译器和运行环境决定的。

例如下面的treeNode没有取地址且不用返回出去,则这个treeNode可以在栈上分配它;当这个treeNode取了地址且返回出去给其他使用时,这个treeNode就可以在堆上分配,然后这个treeNode就会参与垃圾回收,当这个treeNode的指针不再使用的时候就会被回收。因此不能说函数退出这个局部变量就销毁了,这个在Go语言中是不一定的。既然能返回局部变量,那就不用考虑对象到底在哪里分配了,程序相对来说就好写一些:

func createTreeNode(value int) {
 return treeNode{value:value} //这是一个局部变量的地址,但是Go语言允许返回局部变量
}
 

接下来猜猜这段代码,创建了一个怎样的二分搜索树:

var root treeNode 
root = treeNode{value: 2} 
root.left = &treeNode{} 
root.right = &treeNode{6, nil, nil} 
root.right.left =new(treeNode) 
root.left.right = createTreeNode(9)
 

接下来介绍如何遍历这个二分搜索树,但在此之前先介绍如何为结构体定义方法。注意结构体方法并不是写在结构体中的,而是写在结构体外面的,它有一个接收者,其他和普通函数差别不大:

//定义结构体方法,用于输出二分搜索树的信息
func (tnode treeNode)print(){
 fmt.Println(tnode.value)
}
 

注意到这个func (tnode treeNode)print(){}没有?普通的方法都是func print(){},这里多了由小括号包含的(tnode treeNode),我们称之为接收者。其实也就是告诉我们这个函数就是treeNode对象使用的:

root.print()
 

当然如果你理解不了这个意思,可以使用普通函数的写法:

func uprint(tnode treeNode){
 fmt.Println(tnode.value)
}
uprint(root)
 

看到没有,这个就是区别,使用前者指定了接收者,故无需再次输入参数,使用后者则需传入指定参数。 Go语言中只有值传递。 我们尝试修改一下之前创建的那个空子树:

root.right.left =new(treeNode) //给二分搜索树root节点的左子树的左侧创建一个节点
 

就是上面那个,我们定义一个方法,看看能不能将其结点的值修改为8:

func (tnode treeNode)setValue(value int){
 tnode.value=value
}
root.right.left.setValue(8)
root.right.left.print()
//运行结果:
0
 

再次强调一点Go语言中只有值传递。 因此这样做是无法修改root.right.left节点的值的,此时可以借助于指针来完成:

func (tnode *treeNode)setValueByPointer(value int){
 tnode.value=value
}
root.right.left.setValue(8)
root.right.left.print()
//运行结果:
8
 

通过指针传入对象(其实就是原来对象的地址,最后结果反映到原来对象上)就能修改其值。

总结一下为结构体定义方法,如下所示,注意就是将普通的函数返回到方法名称之前罢了,其实是普通方法没有什么区别?不过这样写能让大家一眼就能找到哪些是结构体方法,增强了辨识度:

func (tnode treeNode)print(){
 fmt.Println(tnode.value)
}
 

结构体定义方法显示定义和命名方法接收者,只有使用指针作为方法接收者时才能修改结构的内容。nil指针其实也是可以调用方法的

怎么理解nil指针也可以调用方法呢?我们尝试进行一个判断,并输出后测试一下:

func (tnode *treeNode)setValueByPointer(value int){
 if tnode == nil{
 fmt.Println("你传入的是空指针")
 }
 tnode.value=value
}
var testnil *treeNode
testnil.setValueByPointer(99999)
testnil = &root
testnil.setValueByPointer(2345)
testnil.print()
//运行结果:
你传入的是空指针
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x1 addr=0x0 pc=0x49122d]
 

出错是意料之中的事,因为第一次传进去的testnil是一个空指针nil,而setValueByPointer函数是需要有返回值的,而空指针nil是没有值的,因此会报错,其实你只需要在里面添加一个return就可以的解决这个问题:

func (tnode *treeNode)setValueByPointer(value int){
 if tnode == nil{
 fmt.Println("你传入的是空指针")
 return
 }
 tnode.value=value
}
//运行结果:
你传入的是空指针
2345
 

不过需要说明的是并不是每次都需要判断传入的对象是不是nil,然后才进行后续操作,这个需要结合具体场景来的。

接下来介绍如何遍历这个二分搜索树:(学过二分搜索树的人肯定知道 中序遍历 结果是0 9 2 0 6):

采用中序遍历的方式(遍历方式的名称是由该节点的遍历顺序来决定的,节点在最前面是前序,中间是中序,最后是后序)因此这里的中序就是先遍历左子树,再遍历节点,最后遍历右子树:

//二分搜索树的中序遍历,其实采用了递归的思想
func (tnode *treeNode)reverse(){
 if tnode ==nil{
 return
 }
 tnode.left.reverse()
 tnode.print()
 tnode.right.reverse()
}
//运行结果:
0
9
2
0
6
 

结果和我们的预期完全吻合,但是你有没有我们只是判断了tnode节点是否是nil,但是对于其左右子树没有判断,事实上在JavaScript和Java中这个是不用判断的,但是C++中可能需要判断。

接下来谈谈值接收者和指针接收者的区别:

1、需要修改结构体内容的必须使用指针接收者;

2、当结果过大时,也必须使用指针接收者;

3、在具有指针接收者的情况下,建议都采用指针接收者;

4、值接收者是Go语言独有的;很多语言都有指针接收者如Python中的self,Java中的引用等;

封装

接下来介绍封装,在Java中就是使用一些关键词如private、default、protected、public,按照前面的顺序,自上而下,访问范围越来越大;自下而上,限制能力越来越强,它们所控制的范围如下所示:

但是在Go语言中就不一样了,Go语言通过函数的名字来进行范围控制的,名字一般使用CamelCase。 首字母大写表示public,首字母小写表示private,这两个都是针对包而言的

package main这个就是一个main包,默认使用的就是这个main包,main包包含了可执行入口。在Go语言中,每个目录都只能有一个包,包名不一定要和目录名一致。为结构体定义的方法必须放在同一个包内。

尝试将之前的关于二分搜索树的代码拆分成不同的文件,然后进行导包操作:

tree包里面包含一个包entry和文件node.go,而entry包中又包含entry.go文件。其中entry.go中只含有main方法,定义前面package为main包,当然也可以定义为entry包(每个目录都只能有一个包,包名不一定要和目录名一致。),但是我们只是让他运行main方法,因此定义package为main包。既然这样设置,那么以后包entry所有的go文件的package都必须是mian,否则会出错。同样外面的node.go文件中的package定义为tree包,因此以后tree文件里面所有go文件的package都必须定义为tree!

扩展已有类型

现在有一个问题,就是你在开发过程中需要使用别人的包,那应该怎样使用呢?也就是如何扩展系统类型或别人的类型呢?你可以使用 别名 或者 组合 来解决这个问题。

这个需要配置GoPATH 环境变量 的,默认情况unix和linux是在,~/go下,Windows是在%USERPROFILE%\go。官方建议所有项目和第三方库都放在同一个GoPATH下面,但也可以将每个项目放在不同的GoPATH下面。Go语言会在编译时去各个GoPATH中找到不同的包。

Go语言导包正确操作

那么如何保证自己的go语言程序能正常运行呢?下面教大家如何设置(假设我准备所有go项目存放在I:\Go\GoTest文件夹下面,而我的Go语言安装在G:\Applications\Go文件夹下面):

第一步: 在I:\Go\GoTest文件夹下面新建src文件夹,注意必须是这个名字,不能随意修改;

第二步: 设置环境变量GOROOT=G:\Applications\Go(其实就是Go语言安装路径)和Path=G:\Applications\Go\bin;及GOPATH=I:\Go\GoTest(项目存放的地址,注意不能写成I:\Go\GoTest\src,仅仅写到GoTest文件夹为止)。

第三步: 配置GoLand参数,File–>Settings–>Go,如下图所示:

之后点击确认,可能需要重启GoLand,然后使用Alt+Enter键就能实现自动导包了!

获取第三方库

接下来介绍如何获取Go语言的第三方库。在Python中你可以使用pip install +库名的方式,而在Go语言中可以使用go get +库名的办法。但是直接从谷歌服务器上下载库在国内似乎不行,这时推荐使用gopm +库名的方式:

需要说明的是go get是内置的命令,而gopm是第三方工具,因此在使用前需要使用go get来安装gopm:

go get github.com/gpmgo/gopm
 

之后会在你的src文件夹里面多了两个新的文件夹bin和github.com:

还记得前面设置的Path=G:\Applications\Go\bin;这个环境变量么,打开该文件夹发现里面都是可执行的exe文件:

而我们刚才生成的bin目录下有一个gopm.exe,因此需要将这个gopm.exe复制到

G:\Applications\Go\bin文件夹下面才能保证其正常运行。如果你觉得这种操作很麻烦可以直接修改path参数为Path=%GOPATH%\bin;这样就不需要导入了,那这样我们GOROOT下面的bin目录中的go、godoc、gofmt就无法正常运行了,那没事因为我们用到它的时候不多,手动使用他们也是可以接受的。

这个src文件夹里面会存有你的项目和你下载的第三方库。关于Go内置的一些其他命令可以查看这里GO 命令教程。

下面介绍gopm的使用。其实也是使用gopm get+库名的方式,当然还可以使用gopm help查看各种参数实现自定义下载位置配置:

NAME:
 Gopm - Go Package Manager
USAGE:
 Gopm [global options] command [command options] [arguments...]
VERSION:
 0.8.8.0307 Beta
COMMANDS:
 list list all dependencies of current project
 gen generate a gopmfile for current Go project
 get fetch remote package(s) and dependencies
 bin download and link dependencies and build binary
 config configure gopm settings
 run link dependencies and go run
 test link dependencies and go test
 build link dependencies and go build
 install link dependencies and go install
 clean clean all temporary files
 update check and update gopm resources including itself
 help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
 --noterm, -n disable color output
 --strict, -s strict mode
 --debug, -d debug mode
 --help, -h show help
 --version, -v print the version
 

还可以使用go build来编译,使用go install会产生pkg文件和可执行文件;使用go run会直接编译且运行。

其实看到这里有一个非常大的问题,就是有些文件夹里面有多个main方法的入口,这是不允许的,特别是在go build时候,因此建议一个文件夹下面就仅仅只有一个go程序。

相关文章