0%

Go基础之面向“对象”

面向对象

1
2
3
4
5
6
7
8
9
10
11
12
13
type TreeNode struct {
Left, Right *TreeNode
Value int
}

func (root *TreeNode) traverse() {
if root == nil{
return
}
root.Left.traverse()
fmt.Println(root.Value)
root.Right.traverse()
}
  • go语言仅支持封装,不支持继承和多态
  • 继承和多态使用接口实现
  • go语言没有class ,只有struct
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import "fmt"

type treeNode struct {
Value int
Left, Right *treeNode
}

func main() {
// 以下分别使用了3中不同结构初始化方法
var root treeNode
root = treeNode{Value: 3}
root.Left = &treeNode{} // 此句的效果和使用new()的效果是一样的。
root.Right = &treeNode{5, nil,nil}
root.Right.Left = new(treeNode) // 内建函数new,将创建一个指定类型的匿名变量,返回的是类型指针
nodes_slice := []treeNode {
{Value: 3},
{},
{6,nil,&root},
}
fmt.Println(nodes_slice)
}
// 结果
[{3 <nil> <nil>} {0 <nil> <nil>} {6 <nil> 0xc00009c020}]

new (Type) *Type函数为内建函数,接受值为类型,会在内部申请一块该类型的空间并进行清零,然后返回指向该地址的指针,是一个值为空的结构的地址&{},和nil不一样。

上面演示了创建结构的创建以及初始化,初始化时可以只给个别值赋值,则其余值为该类型的零值,或者都不赋初值。赋值时采用类似字典的方式k:v 的形式进行赋值。

也可以使用变量名.结构体内部变量的方式来赋值

  • 不论地址还是结构本身,一律使用.来访问成员
1
2
3
4
5
func createNode(value int) *treeNode{
// 工厂函数
return &treeNode{Value:value} //c++中会有局部变量的问题,在c++中此处就是返回的局部变量
}
root.Left.Right = createNode(2)
  • 使用自定义工厂函数
  • 注意返回了局部变量地址!(局部变量在函数执行结束后会进行垃圾回收,函数内声明的局部变量会消失,局部变量地址也就没有意义了)但是这在go语言中是没有问题的

Go语言的结构创建在堆上还是栈上?

  • 不需要知道
  • 堆上分配的会参与垃圾回收,Go语言会自动根据变量是否会被使用决定分配的情况,像上面的函数中的treeNode在函数退出后并没有消失,所以放在堆中,参与垃圾回收,当不在被使用后会被回收,而不是像c++中的函数内是局部变量,一旦退出函数后,就消失。
1
2
3
4
5
6
7
8
// 结构方法一:

// 为结构定义方法, 放到结构外面 func (接收者) 函数名(普通参数) {}
// 事实上接受者是结构方法的第一个参数,类似Python中类实例方法的self参数
func (node treeNode) print() {
fmt.Print(node.Value)
}
root.print() // print()函数有一个参数,为接收者 node

(node treeNode)是调用的结构对象,可以理解为将原先func print(node treeNode)后面的参数前移动了,print()函数名之后依然是可以放其他参数的,总的来说就是,将调用者前移,其他的和普通的函数调用没有区别。

1
2
3
4
5
6
7
8
// 结构方法二:

func (node *treeNode) setValue(value int){
// 值传递
// 因为go语言都是值传递类型,所以此处的node都是值传递,将node复制了一份,执行修改操作并不会修改原始的数据,包括左右子树的指针类型
// 所以node 使用指针
node.Value = value // go语言中,使用指针都可以省略* (*node).Value = value
}

因为Go语言都是值传递,所以,若在此处使用值类型,对node的修改是不会反映到实际节点上的,因为在调用的时候是将节点重新复制了一份去操作的。所以,若设计到修改,都需传递指针。

因为结构方法node可以是值传递接受值,也可能是指针传递接受地址,所以,Go编译器对结构方法的调用相当智慧:

1
2
3
4
5
6
7
8
9
10
11
12
// 结构方法的使用
root.print() // print()函数有一个参数,为接收者 node
root.setValue(3)
// 结构方法调用时相当人性化,会自动进行相应的转换
// root.print()中的node是值类型,root是值类型,这里会直接复制一份
// root.setValue()中的node是指针类型,而root是值,所以go会自动将node转为root的地址
pRoot := &root
pRoot.print()
pRoot.setValue(2)
// 这里的pRoot是指针类型,存储的是地址
// pRoot.print()中的node是值类型,这里会将pRoot地址的值得到复制一份
// pRoot.setValue()中node正好需要指针

结构方法小结

  • 显示定义和命名方法的接受者

  • 只有指针才能改变结构内容

  • nil指针也可以调用方法!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    func (node *treeNode) setValue(value int){
    if node == nil {
    fmt.Println("Setting value to nill " +
    "node. Ignore")
    return
    }
    node.Value = value
    }
    var pRoot *treeNode // 指针类型的默认初始值为nil
    fmt.Println(pRoot.setValue(200))

    会正常执行,因为nil也是可以直接使用的,但是要注意,nil直接使用了,但是这里不能取到value,所以做一下判断,直接return结束函数。

遍历树

1
2
3
4
5
6
7
8
9
func (node *treeNode) traverse() {
// 中序遍历
if node == nil{
return
}
node.Left.traverse() // 相比c++ 和 java ,此处不需要先判断node.left != nil ,因为即使为nil,也是可以进行运算的
node.print()
node.Right.traverse()
}

因为Go中的nil可以进行运算,所以traverse()处不需要判断,只要进入方法后,判断一下就行。

值接收者vs指针接收者

接收者是在结构方法中的概念,在结构方法中用来代表结构对象,类似于Python类实例方法中的self

  • 要改变内容的必须使用指针接收者
  • 结构过大也考虑使用指针就收者,因为使用值接收者的话要拷贝,结构过大,造成资源浪费
  • 一致性:如有指针接收者,最好都是指针接收者
  • 值接收者是Go语言特有
  • 值/指针接收者均可接收值/指针,Go编译器会自动转换
  • 一般较多使用的时候都是指针接受者 ,因为涉及到改变内容的操作较多