Go的面向对象——Golang学习笔记4

文章目录

      • 结构体和方法
        • 结构体的创建
        • 定义结构体的方法
      • 包和封装
        • 封装
        • 封装和包的例子
      • 扩展已有类型
      • GOPATH以及目录结构

结构体和方法

  • Go面向对象中仅仅支持封装,不支持继承和多态;

  • Go语言没有class,只有struct;

  • 在结构体struct中无论地址还是结构本身,一律使用 . 来访问成员;

结构体的创建

1. 方法一:只定义,初始化默认值空值

  • var 结构体变量名 结构体类型名
type treeNode struct {
     
	value int
	left, right *treeNode
}

func main(){
     
	var node treeNode
	fmt.Println(node)
	
	var node2 *treeNode
	fmt.Println(node2)
}
  • 输出结果:
{
     0 <nil> <nil>}
<nil>


通过var定义结构体变量,会给结构体中每个成员变量开空间,并赋值为默认值;
通过var定义结构体指针 即如上述指针node2,没有指向一个实际存在的内存。这样的指针变量只会占用 1 个指针的存储空间,也就是一个机器字的内存大小。结构体中的变量并没有分配具体的空间,这就是nil结构体指针。

2. 方法二:边定义边初始化

  • var 结构体变量名 结构体类型名 = { }

  • 结构体变量名 := 结构体类型名 { }

type treeNode struct {
     
	value int
	left, right *treeNode
}

func main(){
     
	root := treeNode{
     value: 3} // 边定义边初始化自己值
	fmt.Println(root)
	
	root.left = &treeNode{
     }
	root.right = &treeNode{
     5, nil, nil}
	fmt.Println(root)
	
	root1 := treeNode{
     } // 效果同方法一
	var root2 treeNode= treeNode{
     5, nil, nil} // 对每个成员变量赋值,要么全不赋值,要么全赋值(包括赋值默认值)
	fmt.Println(root1, root2)
}
  • 输出结果:
{
     3 <nil> <nil>}
{
     3 0xc00009c060 0xc00009c080}
{
     0 <nil> <nil>} {
     5 <nil> <nil>}

3. 使用内建函数new()

  • 使用内建函数new()构建时,注意返回的是结构体指针
  • 其同方法一样,会给结构体中每个成员变量开空间,并赋值为默认值;
type treeNode struct {
     
	value int
	left, right *treeNode
}

func main(){
     
	root3 := new(treeNode)  //内建函数new()创建 空的treeNode
	fmt.Println(root3)
}
  • 输出结果:
&{
     0 <nil> <nil>}

4. 使用工厂函数构建Struct

// 自定义工厂函数
func createNode(value int) *treeNode {
      // 返回是指针,也可以定义返回类型为结构体类型,而非结构体类型 针

	return &treeNode{
     value: value}  // 返回局部变量地址

	// 这里注意
	// 在C++中 函数返回局部变量没有什么问题,但返回局部变量的地址会出现错误
	// 返回局部变量地址后,函数退栈之后,局部变量消失, 指针将指向未知区域,所以出现问题
	// 但在Go中函数返回局部变量地址没有问题
}

func main() {
     
	root := treeNode{
     value : 5}
	root.right  = createNode(3)
	root.left = createNode(4)
	fmt.Println(root, root.right, root.left)
}
  • 输出结果:
{
     5 0xc00009c200 0xc00009c1e0} &{
     3 <nil> <nil>} &{
     4 <nil> <nil>}


go语言中的每一个变量,它的生命周期和引用它的周期一样长。该语言的语义和实现时选择的存储位置,是没有关系的;
关于Golang 中的变量是否分配在堆上还是栈上,变量只要被引用就一直会存活,存储在堆上还是栈上由内部实现决定而和具体的语法没有关系;
如果可能,Golang 编译器会将函数的局部变量分配到函数栈帧(stack frame)上;
如果编译器不能确保变量在函数 return 之后不再被引用,编译器就会将变量分配到堆上;
如果一个局部变量非常大,那么它也应该被分配到堆上而不是栈上。

5. 此外可以将struct作为切片Slice的内建类型

type treeNode struct {
     
	value int
	left, right *treeNode
}

func main(){
     
	// 在slice中定义
	fmt.Println("-------define struct Slice-------")
	nodes := []treeNode {
     
		{
     value: 3},
		{
     },
		{
     6, nil, &root},
	}
	fmt.Println(nodes)
}
  • 输出结果:
-------define struct Slice-------
[{
     3 <nil> <nil>} {
     0 <nil> <nil>} {
     6 <nil> 0xc00009c020}]

定义结构体的方法

1. 关于普通函数和接收者函数

  • 显示定义和命名方法接收者(在其他语言中可以使用this、self指针定义方法,go中没有,定义方法必须取名);

  • 定义结构体的方法常用接收者函数,如下例中print1,将参数写在方法前,使用 结构体名 . 方法名 进行调用。

type treeNode struct {
     
	value int
	left, right *treeNode
}

// 输出节点的value值
func (node treeNode) print1() {
      // 配套使用方法是 node.print1()
	// print1()的接收者为node treeNode,可以把这种方法使用时类似java类中的内部函数
	fmt.Println(node.value)
}

// 输出节点的value值
func print2(node treeNode) {
      // 配套使用方法是 print2(node)
	// 不同函数方法,node treeNode作为值传递的参数送入函数中并输出
	fmt.Println(node.value)
}

func main() {
     
	node := treeNode{
     value : 5}

	node.print1()
	print2(node)
}
  • 输出结果
5
5

2. 关于值接收者和指针接收者

  • 值接收者和指针接收者是指接收者的类型不同,一个是结构体,另外一个是结构体指针

  • 可以使用指针作为方法接收者,只有使用指针才可以改变结果内容;

  • 值接收者是go语言特有;

type treeNode struct {
     
	value int
	left, right *treeNode
}

// 输出节点的value值
// 值接收者
func (node treeNode) print() {
      // 配套使用方法是 node.print()
	// print()的接收者为node treeNode,可以把这种方法使用时类似java类中的内部函数
	fmt.Println(node.value)
}

// 修改节点的value值
// 指针接收者
func (node *treeNode) setValue(value int) {
      // go中函数参数一般是值传递,这里需要通过指针进行指针传递
	node.value = value
}

// 修改节点的value值
// 值接收者
func (node treeNode) setValue_non(value int) {
      // go中函数参数一般是值传递,这里需要通过指针进行指针传递
	node.value = value
}

func main() {
     
	node := treeNode{
     value : 5}
	node.left = &treeNode{
     value: 4} // 将地址赋值给指针
	node.left.print()

	node.left.setValue_non(6)
	node.left.print()

	node.left.setValue(9)
	node.left.print()
}
  • 输出结果:
4
4
9
  • 上述的案例中print()和setValue_non()是值接受者,而setValue()是指针接收者;

  • 在关于修改node节点中value的值情况中,值接受者setValue_non()无法修改指针接收者setValue()能修改

  • 此外关于值接收者和指针接收者,其实Go在编译的时候有一个隐式转换,将其转换为正确的接收者类型;

    上述中root.left是指针,setValue()作为其的指针接收者,收到是其的地址;

    print()和setValue_non()作为其的值接收者,Go会自动获取指针指向地址空间的值。收到是隐式转化后的值传递的参数;

  • Go的隐式转化保证了值\指针接受者均可接受值\指针

  • 使用指针接收者的情况:

    要改变内容必须使用指针接收者;

    结构体过大也考虑使用指针接收者;

    一致性:建议如果编写代码中有使用指针接收者,最后都使用指针接收者;

3. 关于nil结构体指针调用方法

  • nil是预定义的标识符,代表指针、通道、函数、接口、映射或切片的零值,也就是预定义好的一个变量,并非是关键字

  • nil是一个合法的接收者,可以调用结构体方法,类似函数允许nil指针作为实参,方法的接收者允许是nil指针;

  • nil 结构体是指结构体指针变量没有指向一个实际存在的内存。这样的指针变量只会占用 1 个指针的存储空间,也就是一个机器字的内存大小。

    上述的nil特征让我们无需在每次调用方法前判断指针是否为nil;

    但在一些结构体中方法,需要输出或修改结构体成员变量时,需要判断接收者是否为nil,如果是nil,报错并return,由于nil中结构体成员变量未分配空间,不存在。如果在方法中没有判断接收者是否为nil,代码在运行时直接panic出错;

type treeNode struct {
     
	value int
	left, right *treeNode
}

// 输出节点的value值
// 值接收者
func (node treeNode) print() {
      // 配套使用方法是 node.print()
	fmt.Println(node.value)
}

// 修改节点的value值
// 指针接收者
func (node *treeNode) setValue(value int) {
     
	if node == nil  {
     
		fmt.Println("setting value to nil node. Ignored!")
		return // 这里return必须有,其作用是当node == nil时,终止函数运行,直接返回,避免执行node.value = value
		// 若执行node.value = value,由于node是nil,不存在value变量,运行报错
	}
	node.value = value
}


func main() {
     
	var node *treeNode
	fmt.Println(node)
	// node.print() // 运行报错,不存在value变量,这里需不要考虑node是否为指针,go会隐式转化
	// 这里如果定义为结构体变量var node treeNode,而非是结构体指针变量var node *treeNode,运行node.print()不会出错
	// 是因为结构体变量定义结果是{0  },存在value成员变量并且赋值为0,而定义结构体指针变量结果是,仅仅给指针开辟内存,不存在value等变量

	// 为nil
	node.setValue(99)
	// node.print() //运行报错

	// 实例化后
	node = &treeNode{
     66,nil,nil}
	// node = &treeNode{value: 66} // 效果相同
	node.setValue(999)
	node.print()
}
  • 输出结果:
<nil>
setting value to nil node. Ignored!
999

4. 二叉树中序变量例子

type treeNode struct {
     
	value int
	left, right *treeNode
}

// 输出节点的value值
// 值接收者
func (node treeNode) print() {
      // 配套使用方法是 node.print()
	fmt.Println(node.value)
}

// 自定义工厂函数
func createNode(value int) *treeNode {
     

	return &treeNode{
     value: value}  // 返回局部变量地址

	// 这里注意
	// 在C++中 函数返回局部变量没有什么问题,但返回局部变量的地址会出现错误
	// 返回局部变量地址后,函数退栈之后,局部变量消失, 指针将指向未知区域,所以出现问题
	// 但在Go中函数返回局部变量地址没有问题
}

// 中序遍历
func (node *treeNode) traverse(){
     
	if node == nil {
     
		return
	}

	node.left.traverse()
	node.print()
	node.right.traverse()
}

func main() {
     
	root := treeNode{
     0, nil,nil}
	root.left = createNode(1)
	root.right = createNode(2)
	root.left.left = createNode(3)
	root.left.right = &treeNode{
     value: 4}
	root.right.left = &treeNode{
     5, nil,nil}
	root.right.right = createNode(6)

	root.traverse()
}
  • 输出结果:
3
1
4
0
5
2
6

包和封装

  • 每个文件夹(目录)一个包;

  • 一个文件夹内也只能有一个包,但是一个文件夹可以有多个文件;

  • 文件名可以和包名不同;

  • 特殊的包:main包,其包含可执行入口;

  • 如果文件中包含main()函数,则这文件只能在main包中(package main),否则将在当前文件夹(目录)下创建一个文件夹,创建新的go文件包含mian()函数,将文件夹放入main包中;

  • 为结构体定义的方法必须放在同一个包内,可以是在不同文件中;

  • 引用其他包的结构体或方法时,是通过包名.结构体名/方法名 来实现引用;

封装

  • 目标是对结构体的方法进行封装;

  • 封装函数的名字一般使用CamelCase,即骆驼拼写法,单词首字母大写,多个单词拼接在一起;

  • 首字母大写表示函数、结构体、常量、变量等所有定义是public

  • 首字母小写表示函数、结构体、常量、变量等所有定义是private

封装和包的例子

  1. 在文件夹tree下构建node.go文件和tarversal.go文件,他们所在的包名为tree,这里包名tree和文件名tree可以取不同;
  • node.go文件
package tree

import "fmt"

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

// 输出节点的Value值
// 值接收者
func (node Node) Print() {
      // 配套使用方法是 node.Print()
	fmt.Println(node.Value)
}

// 自定义工厂函数
func CreateNode(Value int) *Node {
     

	return &Node{
     Value: Value}  // 返回局部变量地址

	// 这里注意
	// 在C++中 函数返回局部变量没有什么问题,但返回局部变量的地址会出现错误
	// 返回局部变量地址后,函数退栈之后,局部变量消失, 指针将指向未知区域,所以出现问题
	// 但在Go中函数返回局部变量地址没有问题
}
  • tarversal.go文件——表示结构体和结构体的方法可以不在同一文件中,但必须在同一包中
package tree

// 中序遍历
func (node *Node) Traverse(){
     
	if node == nil {
     
		return
	}

	node.Left.Traverse()
	node.Print()
	node.Right.Traverse()
}
  1. 可执行入口main()方法
  • 在tree目录下创建一个新的目录main,将其划分带main包中,以便执行main()方法;
package main

import "xxx/tree" // Node所在的绝对路径

func main() {
     
	root := tree.Node{
     0, nil,nil}
	root.Left = tree.CreateNode(1)
	root.Right = tree.CreateNode(2)
	root.Left.Left = tree.CreateNode(3)
	root.Left.Right = &tree.Node{
     Value: 4}
	root.Right.Left = &tree.Node{
     5, nil,nil}
	root.Right.Right = tree.CreateNode(6)

	fmt.Println("------inOrder------")
	root.Traverse()
}
  1. 输出结果
------inOrder------
3
1
4
0
5
2
6

扩展已有类型

  • 扩充系统类型或者别人的类型的方法:
  1. 定义别名
  2. 使用组合
  • 使用组合方法在封装和包的例子的基础上将Node结构体组合一起,在这里只有Node一个 已知的类型。

  • 工作内容:定义一个新的结构体组合其他类型,定义后序遍历的方法

type myNode struct {
      // 只用组合方法扩充系统类型或别类型
	node *tree.Node
}

func (myyNode *myNode) postOrder() {
     
	if myyNode == nil || myyNode.node == nil {
     
		return
	}

	left := myNode{
     myyNode.node.Left}
	right := myNode{
     myyNode.node.Right}

	left.postOrder()
	right.postOrder()
	myyNode.node.Print()
}

// 在main()方法中的末尾添加
	fmt.Println("------postOrder------")
	node := myNode{
     &root}
	node.postOrder()
  • 输出结果:
------postOrder------
3
4
1
5
6
2
0

GOPATH以及目录结构

  • GOROOT就是go的安装路径;

  • GOPATH是作为编译后二进制的存放目的地和import包时的搜索路径(也就是你的工作目录, 你可以在src下创建你自己的go源文件和包);

  • GOPATH是一个环境变量,可以指向多个目录;

  • 在unix,linux系统GOPATH默认在~/go,在windows系统中默认在%USERPROFILE

  • 官方推荐:所有项目和第三方库都放在同一GOPATH下;

  • 但可以将每个项目放在不同的GOPATH下;

  • 关于获取Go的第三方库:使用go get命令,对于无法下载的包可以在github上下载或者通过 gopm工具下载;

  • 安装gopm

go get github.com/gpmgo/gopm
  • go build来编译

  • go install 产生pkg文件和可执行文件

  • go run直接编译 运行

  • GOPATH目录下有三个文件夹,分别是src,pkg,bin;

    src文件夹下存在各种项目;

    pkg文件夹是src中各种项目build的中间文件;

    bin文件夹下存放是项目的可执行文件;

你可能感兴趣的:(Golang,golang)