go语言之结构体和方法

前言

关于面向对象编程大家肯定都十分熟悉了,面向对象编程的三个要素就是封装、继承和多态。但相对其他编程语言而言,go语言仅支持封装,不支持继承和多态,它没有class概念,只有struct(结构体),本文主要总结了关于golang中结构体的创建和方法,通过创建一个二叉树的树结构并简单实现其遍历的方法观察下在golang中是如何贯彻面向对象编程的理念的。

结构的创建

结构体定义

二叉树是每个结点最多有两个子树的树结构,它由一个一个树节点组成,每个节点包含当前节点的值和左右两个节点的地址,一个个节点连接组成一颗完整的树,它的树节点结构体类型可以定义如下:

type treeNode struct {
	value       int
	left, right *treeNode
}
复制代码

结构体初始化

golang中可以通过声明式语法创建treeNode{value:666,left:nil,right:nil}(这里若不指定某一结构体成员的值,该成员的值将默认用零值填充)或者直接将参数按结构体定义成员顺序传入treeNode{5, nil, nil}(这种初始化形式必须要给所有成员都进行赋值),除此之外还可以通过new(treeNode)内建函数进行初始化。

需要注意的是用new(T)内建函数进行初始化时,它返回的是一个分配了零值填充的结构体的内存空间的地址。

简单创建一个树结构如下述代码所示:

root := treeNode{value: 666}
root.left = &treeNode{value: 1}
root.right = &treeNode{5, nil, nil}
root.left.left = new(treeNode)
root.left.right = &treeNode{888, nil, nil}
复制代码

这里需要注意的是root节点的right成员存储的是地址,但仍然通过.操作符的形式去访问其成员,这在go语言当中是可行的。因为go语言中规定:无论是地址还是结构本身,一律使用.操作法来访问成员。

通过工厂函数创建结构体

golang中没有构造函数的说法,其提供的struct已经满足绝大多数场景下的应用,但是有些时候我们想要控制结构体的构造可以使用自定义的工厂函数返回局部变量的地址,具体代码如下:

func createTreeNode(value int) *treeNode {
	return &treeNode{value: value}
}
复制代码

如果有c++编程经验的同学可能会觉得上述代码有些奇怪,因为c++中局部变量是分配在栈上的,函数退出后就会及时销毁,而如果要传出去则需要在堆上分配,而在堆上分配就必须要手动进行释放,因此c++中是不允许函数中返回局部变量的地址供外部程序进行使用的,而在golang中就不存在该限制。

结构是创建在堆上还是栈上

关于这个问题的答案是不需要知道,因为结构是创建在堆上还是栈上是由go语言的编译器和它的运行环境决定的。比如说如果上述的工厂函数代码返回的treeNode没有取地址而是直接返回值得话,编译器很可能就认为这个变量不需要被外部程序使用,那么它就会在栈上分配。反之如果treeNode取得是地址的话,那么它就会在堆上进行分配,并参与垃圾回收机制。

因此我们需要注意和其他语言不同:go语言中的局部变量不一定在退出函数就销毁了

结构体方法

在结构体定义方法,不是写在结构体花括号里面,而是在结构体外面的。假设我们要给结构体定义一个方法让其打印当前节点的值为后续进行遍历做准备可以这样做:

func (node treeNode) print() {
	fmt.Println(node.value)
}
复制代码

我们可以发现,结构体方法和普通函数的语法是非常类似的,唯一不同的是,结构体的方法在函数名前有一个接收者,意味着这个方法是由指定接收者接收的。go语言当中没有this指针的概念,而是由接收者来代替this。

其实结构体方法本质上就是函数,我们可以把它的接收者看成函数参数的形式,因此结构体方法和下述写法是等价的:

func print(node treeNode) {
	fmt.Println(node.value)
}
复制代码

这样当想要调用此方法时,直接将接收者通过参数的形式传入就可以了print(root),而通过结构体方法需要调用则是通过点操作符的形式root.print(),可以看出以上两种写法本质上其实是一样的,只是调用语法上不同。

那么结构体方法中接收者是按值传递还是按引用地址传递呢?

我们都知道,go语言中函数的参数都是按值传递的,既然接收者可以类比为函数的参数,那么同理接收者也是一样。如果接收者定义为指针接收者,那么就会直接传入调用者的地址,如果定义为值接收者,就会将调用者的地址解析出来拿到值后拷贝一份再传入,非常灵活。

接下来分别通过值接收者和指针接收者实现一个给树节点设置值的方法,观察下二者的不同: 假设节点原有的值为666,分别看下调用setValue方法后二者的输出结果。

// 值接收者
func (node treeNode) setValue(val int) {
	node.value = val
}
root.setValue(8)
// 输出:666
复制代码
// 指针接收者
func (node *treeNode) setValue(val int) {
	node.value = val
}
root.setValue(8)
// 输出:8
复制代码

由此我们可以看出,要想改变结构体内容时就需要使用指针接收者。

值接收者 vs 指针接收者

那什么时候该使用值接收者,什么时候使用指针接收者呢,可归纳为以下几点:

  • 要更改内容的时候必须使用指针接收者
  • 值接收者是go语言特有,因为它函数传参过程是通过值的拷贝,因此需要考虑性能问题,结构体过大也需要考虑使用指针接收者
  • 一致性:如果有指针接收者,最好都使用指针接收者
  • 值/指针接收者均可接受值/指针,定义方法的人可以随意改动接收者的类型,这并不会改变调用方式

树遍历方法实现

掌握了结构体方法的定义后,来简单实现一下二叉树的前、中、后序遍历。

首先来看看前序、中序、后序遍历的特性:

前序遍历

  1. 访问根节点
  2. 前序遍历左子树
  3. 前序遍历右子树

中序遍历

  1. 中序遍历左子树
  2. 访问根节点
  3. 中序遍历右子树

后序遍历

  1. 后序遍历左子树
  2. 后序遍历右子树
  3. 访问根节点

通过上述二叉树结构初始化代码后,创建的二叉树结构如下所示:

// 前序遍历
func (node *treeNode) traverse() {
	if node == nil {
		return
	}
	node.print()
	node.left.traverse()
	node.right.traverse()
}
// 输出 666 1 0 888 5
复制代码
// 中序遍历
func (node *treeNode) traverse() {
	if node == nil {
		return
	}
	node.left.traverse()
	node.print()
	node.right.traverse()
}
// 输出 0 1 888 666 5
复制代码
// 后序遍历
func (node *treeNode) traverse() {
	if node == nil {
		return
	}
	node.left.traverse()
	node.right.traverse()
	node.print()
}
// 输出 0 888 1 5 666
复制代码

这里需要注意的是:go语言中nil指针也可以调用方法,也就是说接收者允许空指针,因此我们不需要在调用方法前判断调用者是否为空指针,但是在方法中需要判断接收者是否为空指针,如果为空指针则直接中断程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值