go语言之结构体和方法

前言

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

结构的建立

结构体定义

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

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

结构体初始化

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

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

简单建立一个树结构以下述代码所示:bash

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取得是地址的话,那么它就会在堆上进行分配,并参与垃圾回收机制。ui

所以咱们须要注意和其余语言不一样:go语言中的局部变量不必定在退出函数就销毁了this

结构体方法

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

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. 访问根节点

经过上述二叉树结构初始化代码后,建立的二叉树结构以下所示:

image

// 前序遍历
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指针也能够调用方法,也就是说接收者容许空指针,所以咱们不须要在调用方法前判断调用者是否为空指针,可是在方法中须要判断接收者是否为空指针,若是为空指针则直接中断程序。

相关文章
相关标签/搜索