Go中的面向“对象”

第四章 面向对象

  • go语言仅支持封装,不支持继承和多态
  • go语言没有class,只有struct

4.1 结构体和方法

4.1.1 type用法

package main

import "fmt"

//type用法1:声明一种新的数据类型myint,是int的一个别名
type myint int

//type用法2:定义一个结构体
type Book struct {
   name string
   auth string
}

func changeBook(book Book) {
   //传递一个book副本,并不会改变原来的值
   book.name = "java"
}

func changeBook2(book *Book) {
   //指针传递
   book.name = "Java"
}

func main() {
   var a myint = 10
   fmt.Println("a=", a)             //a= 10
   fmt.Printf("type of a =%T\n", a) //type of a =main.myint

   var book1 Book
   book1.name = "Golang"
   book1.auth = "zhangsan"
   fmt.Printf("%v\n", book1) //{Golang zhangsan}

   changeBook(book1)
   fmt.Println(book1.name) //Golang
   changeBook2(&book1)
   fmt.Println(book1.name) //Java
}

4.1.2 结构体和方法

C语言中,通过指向结构体的指针来访问结构体成员时用的是“->”,而在go语言中不论地址还是结构本身,一律使用“.”来访问成员

package main

import "fmt"

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

func main() {
   var root treeNode
   fmt.Println(root) //{0 <nil> <nil>}

   root = treeNode{value: 3} //可以直接root := treeNode{value: 3}
   root.left = &treeNode{}
   root.right = &treeNode{5, nil, nil}
   root.right.left = new(treeNode)
   //若是C语言,那么此句是:root.right->left = new(treeNode),这也是Go与C的小区别
   //C语言中,通过指向结构体的指针来访问结构体成员时用的是“->”,而在go语言中不论地址还是结构本身,一律使用“.”来访问成员

   nodes := []treeNode{
      {3, nil, nil},
      {},
      {6, nil, &root},
   }//这里[]treeNode前面不用加“&”,因为切片是引用类型
   fmt.Println(nodes) //[{3 <nil> <nil>} {0 <nil> <nil>} {6 <nil> 0xc000004078}]
}

使用创造节点的自定义工厂函数来创建节点:

package main

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

//创造节点的自定义工厂函数
func crateNode(value int) *treeNode {
	return &treeNode{value: value} //注意返回了局部变量的地址,在go语言中是被允许的,其他语言会报错
}

func main() {
	var root treeNode = treeNode{value: 3}
	root.right = &treeNode{value: 3}//普通方法创建节点
	root.left = crateNode(4)//用工厂函数创建节点
}

给结构体绑定方法例子,go语言方法以及二叉树遍历:

package main

import "fmt"

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

//创造节点的自定义工厂函数
func crateNode(value int) *treeNode {
	return &treeNode{value: value} //注意返回了局部变量的地址,在go语言中是被允许的,其他语言会报错
}

//打印节点值
func (node treeNode) print() {
	fmt.Print(node.value, " ")
}

//设置节点值
func (node *treeNode) setValue(value int) { //这里如果接收者是值类型则不会改变其value值
	if node == nil {
		fmt.Println("Setting value to nil" +
			"node. Ignored.")
		return //setValue()是一个没有返回值是函数,此处的return就是一个不带返回值的退出
	}
	node.value = value
}

//中序遍历二叉树
func (node *treeNode) traverse1() { //go语言中方法的接收者允许接收空指针,这在其他语言诸如C和Java中是不被允许的
	if node == nil {
		return //若为空树则直接退出函数,没有返回值的函数中的return就是退出函数
	}

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

//先序遍历二叉树
func (node *treeNode) traverse2() {
	if node == nil {
		return
	}

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

//后序遍历二叉树
func (node *treeNode) traverse3() {
	if node == nil {
		return
	}

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

func main() {
	root := treeNode{3, nil, nil}    //普通方式定义一个节点
	root.left = &treeNode{}          //创捷一个空值节点,默认为其类型的零值,这里为int型的零值0和指针型的零值nil
	root.right = &treeNode{value: 5} //只定义节点的value,不赋值左右子节点默认为其类型的零值,这里为指针型的零值nil
	root.right.left = new(treeNode)  //用new创建一个空值节点
	root.left.right = crateNode(2)   //用自定义的一个创建节点的函数来新建一个节点
	root.right.left.setValue(4)      //将根节点的右子节点的左子节点value改为4

	root.setValue(100)  //将根节点的value改为100
	pRoot := &root      //proot指向root
	pRoot.setValue(200) //通过指向根节点的指针改变根节点的value值
	pRoot.print()       //控制台输出:200
	root.print()        //控制台输出:200,成功通过指向根节点的指针改变根节点的value值为200
	root.traverse1()    //中序遍历控制台输出:0 2 200 4 5
	fmt.Println()
	root.traverse2() //先序遍历控制台输出:200 0 2 5 4
	fmt.Println()
	root.traverse3() //后序遍历控制台输出:2 0 4 5 200
}

递归法中序遍历思想:

image

值接收者vs指针接收者

  • 要改变内容必须使用指针接收者
  • 结构过大也考虑使用指针接收者
  • 一致性:如有指针接收者,最好都是指针接收者
  • 值接收者是go语言特有
  • 值/指针接收者均可接收值/指针

4.2 包和封装

在Go语言中封装就是把抽象出来的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只能通过被授权的方法,才能对字段进行操作。Go语言通过方法名或者属性名首字母大小写来区分公有或者私有:大写为公有,所有包都可以调用;小写为私有,只有当前包可以调用。

封装的好处:

  • 隐藏实现细节;
  • 可以对数据进行验证,保证数据安全合理。

如何体现封装:

  • 对结构体中的属性进行封装;
  • 通过方法,包,实现封装。

封装的实现步骤:

  • 将结构体、字段的首字母小写;
  • 给结构体所在的包提供一个工厂模式的函数,首字母大写,类似一个构造函数;
  • 提供一个首字母大写的 Set 方法(类似其它语言的 public),用于对属性判断并赋值;
  • 提供一个首字母大写的 Get 方法(类似其它语言的 public),用于获取属性的值。

封装

go语言通过命名来实现封装

  • 名字一般使用CamelCase
  • 首字母大写表示public
  • 首字母小写表示private

  • 包是封装的界限
  • 每个目录一个包,每个目录的包名是统一的
  • main包包含可执行入口,含有main函数的包其包名只能是main
  • 为结构体定义的方法必须与结构体放在同一个包内
  • 可以是不同文件

【示例1】员工例子

对于员工,不能随便查看年龄,工资等隐私,并对输入的年龄进行合理的验证。代码结构如下:

image

person.go文件代码:

package model

import "fmt"

type person struct {
	Name string
	age  int
	sal  float64
	//将age和sal首字母小写表示private,其它包不能直接访问
}

//写一个工厂模式的函数,相当于构造函数
//将此方法首字母大写表示public
func NewPerson(name string) *person {
	return &person{
		Name: name,
	}
}

//为了访问age和sal要写一对SetXXX和GetXXX方法
//将此方法首字母大写表示public
func (p *person) SetAge(age int) {
	if age > 0 && age < 150 {
		p.age = age
	} else {
		fmt.Println("年龄范围不正确..")
	}
}

//将此方法首字母大写表示public
func (p *person) GetAge() int {
	return p.age
}

//将此方法首字母大写表示public
func (p *person) SetSal(sal float64) {
	if sal >= 3000 && sal <= 30000 {
		p.sal = sal
	} else {
		fmt.Println("薪水范围不正确..")
	}
}

//将此方法首字母大写表示public
func (p *person) GetSal() float64 {
	return p.sal
}

main.go文件代码:

package main

import (
   "../model"
   "fmt"
)

func main() {
   p := model.NewPerson("Lee")
   p.SetAge(23)
   p.SetSal(25000)
   fmt.Println(p)
   fmt.Println(p.Name, "age:", p.GetAge(), "sal:", p.GetSal())
}

运行结果:

&{Lee 23 25000}
Lee age: 23 sal: 25000

【示例2】二叉树例子

将上面二叉树的原码分配为entry、node、traversal三个文件:

image

为结构体定义的方法必须与结构体放在同一个包内,所以将node和traversal一起放在tree包,traversal就能直接使用同一包下的node结构体。

node.go文件代码:

package tree

import "fmt"

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

//创造节点的自定义工厂函数
func CrateNode(value int) *Node {
   return &Node{Value: value} //注意返回了局部变量的地址,在go语言中是被允许的,其他语言会报错
}

//打印节点值
func (node Node) Print() {
   fmt.Print(node.Value, " ")
}

//设置节点值
func (node *Node) SetValue(value int) { //这里如果接收者是值类型则不会改变其value值
   if node == nil {
      fmt.Println("Setting value to nil" +
         "node. Ignored.")
      return //SetValue()是一个没有返回值是函数,此处的return就是一个不带返回值的退出
   }
   node.Value = value
}

traversal.go文件代码:

package tree

//中序遍历二叉树
func (node *Node) Traverse1() { //go语言中方法的接收者允许接收空指针,这在其他语言诸如C和Java中是不被允许的
   if node == nil {
      return //若为空树则直接退出函数,没有返回值的函数中的return就是退出函数
   }

   node.Left.Traverse1()
   node.Print()
   node.Right.Traverse1()
}

//先序遍历二叉树
func (node *Node) Traverse2() {
   if node == nil {
      return
   }

   node.Print()
   node.Left.Traverse2()
   node.Right.Traverse2()
}

//后序遍历二叉树
func (node *Node) Traverse3() {
   if node == nil {
      return
   }

   node.Left.Traverse3()
   node.Right.Traverse3()
   node.Print()
}

entry.go文件代码:

package main

import (
   "fmt"
   "learngo/tree"//导tree包只用写src下的相对路径即可,不用写绝对路径
)

func main() {
   root := tree.Node{3, nil, nil}      //普通方式定义一个节点
   root.Left = &tree.Node{}            //创捷一个空值节点,默认为其类型的零值,这里为int型的零值0和指针型的零值nil
   root.Right = &tree.Node{Value: 5}   //只定义节点的value,不赋值左右子节点默认为其类型的零值,这里为指针型的零值nil
   root.Right.Left = new(tree.Node)    //用new创建一个空值节点
   root.Left.Right = tree.CrateNode(2) //用自定义的一个创建节点的函数来新建一个节点
   root.Right.Left.SetValue(4)         //将根节点的右子节点的左子节点value改为4

   root.SetValue(100)  //将根节点的value改为100
   pRoot := &root      //proot指向root
   pRoot.SetValue(200) //通过指向根节点的指针改变根节点的value值
   pRoot.Print()       //控制台输出:200
   root.Print()        //控制台输出:200,成功通过指向根节点的指针改变根节点的value值为200
   root.Traverse1()    //中序遍历控制台输出:0 2 200 4 5
   fmt.Println()
   root.Traverse2() //先序遍历控制台输出:200 0 2 5 4
   fmt.Println()
   root.Traverse3() //后序遍历控制台输出:2 0 4 5 200
}

4.3 扩展已有类型(类似继承)

如何扩充系统类型或者别人的类型?

Java会采用继承来实现,但是Go语言没有继承,于是采用了以下方法:

  1. 定义别名
  2. 使用组合

4.3.1 使用组合

4.3.1.1 SuperMan例子

SuperManHuman的子类

image

human.go

package catalog

import "fmt"

//父类Human有Eat()和Walk()两个方法
type Human struct {
   Name   string
   Gender string
}

func (h *Human) Eat() {
   fmt.Println("父类方法Eat()")
}

func (h *Human) Walk() {
   fmt.Println("父类方法Walk()")
}

superman.go

package catalog

import "fmt"

//子类SuperMan有重写了父类Human的Eat()方法、继承了Human类的Walk()方法、自己独有的Fly()方法
type SuperMan struct {
   //只写一个类名Human表示继承了该类的方法
   //这种方式也被称为内嵌
   Human

   Level int
}

func (s *SuperMan) Eat() {
   fmt.Println("子类方法Eat()")
}

func (s *SuperMan) Fly() {
   fmt.Println("子类方法Fly()")
}

main.go

package main

import "learngo/test/catalog"

func main() {
   h := catalog.Human{"lee", "male"}
   h.Eat()  //控制台输出:父类方法Eat()
   h.Walk() //控制台输出:父类方法Walk()

   /*
      也可以这样定义
      s := catalog.SuperMan{
         catalog.Human{"lily", "female"},
         17,
      }
   */
   var s catalog.SuperMan
   s.Name = "lily"
   s.Gender = "female"
   s.Level = 17
   s.Eat()  //控制台输出:子类方法Eat()
   s.Walk() //控制台输出:父类方法Walk()
   s.Fly()  //控制台输出:子类方法Fly()
}

4.3.1.2 二叉树例子

entry.go文件代码:

package main

import (
   "fmt"
   "learngo/tree"
)

//若在traversal.go中没有实现后序遍历那么只能自己扩充
//扩充部分:--------------------------------------------------------------------------------------------------
type myTreeNode struct {
   node *tree.Node//使用组合的方式扩展已有的Node类型
}

func (myNode *myTreeNode) postOrder() {
   if myNode == nil || myNode.node == nil { //要判断两个是否为nil
      return
   }

   left := myTreeNode{myNode.node.Left}
   left.postOrder()
   right := myTreeNode{myNode.node.Right}
   right.postOrder()
   myNode.node.Print()
}

//扩充部分:--------------------------------------------------------------------------------------------------

func main() {
   root := tree.Node{3, nil, nil}      //普通方式定义一个节点
   root.Left = &tree.Node{}            //创捷一个空值节点,默认为其类型的零值,这里为int型的零值0和指针型的零值nil
   root.Right = &tree.Node{Value: 5}   //只定义节点的value,不赋值左右子节点默认为其类型的零值,这里为指针型的零值nil
   root.Right.Left = new(tree.Node)    //用new创建一个空值节点
   root.Left.Right = tree.CrateNode(2) //用自定义的一个创建节点的函数来新建一个节点
   root.Right.Left.SetValue(4)         //将根节点的右子节点的左子节点value改为4

   fmt.Println()
   myNode := myTreeNode{&root}
   myNode.postOrder()//2 0 4 5 3
}

node.gotraversal.go文件代码见【示例2】二叉树例子

【小结】 myTreeNode类型和Node类型有点类似继承的关系,myTreeNode类型的实例可以通过实例名.node.(Node结构体中的成员变量/绑定Node类型的方法)来从子类的myTreeNode类型实例访问父类的Node类型的成员变量绑定父类Node类型的方法

【注】go语言没有继承,以上的父类子类的说法仅是为了方便理解而使用。

4.3.2 定义别名

出栈入栈以及验证栈内是否为空功能的代码结构:

image

stack.go文件代码:

package stack

type Stack []int//使用定义别名的方式扩展已有类型

func (s *Stack) Push(v int) {
   *s = append(*s, v)
}

func (s *Stack) Pop() int {
   back := (*s)[len(*s)-1]
   *s = (*s)[:len(*s)-1]
   return back //注意不要直接返回(*s)[len(*s)-1]而省略back := (*s)[len(*s)-1],这样会返回出栈之后栈内最后一个元素
}

func (s *Stack) IsEmpty() bool {
   return len(*s) == 0 //注意不要返回*s == nil,就算切片没内容也不会是nil
}

entry.go文件代码:

package main

import (
   "fmt"
   "learngo/stack"
)

func main() {
   s := stack.Stack{1}
   s.Push(2)
   s.Push(3)
   fmt.Println(s.Pop())//3
   fmt.Println(s.Pop())//2
   fmt.Println(s.IsEmpty())//false
   fmt.Println(s.Pop())//1
   fmt.Println(s.IsEmpty())//true
}

4.4 使用内嵌来扩展已有类型

内嵌省略了包含已有结构体类型的结构体中的结构体名,这样的好处是可以使代码更简洁,类似语法糖。

比如如下例子中,原本需要通过实例名.node.(Node结构体中的成员变量/绑定Node类型的方法)简化成了→实例名.(Node结构体中的成员变量/绑定Node类型的方法),使用内嵌之后,Node本身Node结构体中的成员变量绑定了Node类型的方法都可以被myTreeNode类型的实例通过“ . ”调用。

package main

import (
	"fmt"
	"learngo/tree"
)

type myTreeNode struct {
	*tree.Node //内嵌Embedding省略node
}

//myTreeNode类型和Node类型有点类似继承的关系,root现在可以调用父类Node里的成员变量以及绑定Node类型的方法
func (myNode *myTreeNode) postOrder() {
	//使用内嵌之后可以发现myNode可以直接点出来tree.Node里的成员变量
	if myNode == nil || myNode.Node == nil { //myNode.node变成了myNode.Node
		return
	}

	left := myTreeNode{myNode.Left} //使用内嵌后不用再mynode.node.Left
	left.postOrder()
	right := myTreeNode{myNode.Right} //使用内嵌后不用再mynode.node.Right
	right.postOrder()
	myNode.Print() //使用内嵌后不用再mynode.node.Print()
}

func main() {
	root := myTreeNode{&tree.Node{Value: 3}} //直接让root成为myTreeNode类型
	root.Left = &tree.Node{}
	root.Right = &tree.Node{Value: 5}
	root.Right.Left = new(tree.Node)
	root.Left.Right = tree.CrateNode(2)
	root.Right.Left.SetValue(4)

	fmt.Println()
	root.PreOrderTraversal() //myTreeNode类型的root可以直接调用绑定了父类Node类型的PreOrderTraversal()
	fmt.Println()
	root.postOrder() //myTreeNode类型的root可以直接调用绑定了父类Node类型的postOrder()

	fmt.Println()
	root.postOrder() //myTreeNode类型的root可以直接调用绑定了myTreeNode类型的postOrder()
}

【扩展】重写父类Node方法

package main

import (
   "fmt"
   "learngo/tree"
)

type myTreeNode struct {
   *tree.Node //内嵌Embedding省略node
}

//重写父类Print()方法
func (myNode *myTreeNode) Print() {
   fmt.Println("this method is shadowed")
}

func main() {
   root := myTreeNode{&tree.Node{Value: 3}} //直接让root成为myTreeNode类型
   root.Left = &tree.Node{}
   root.Right = &tree.Node{Value: 5}
   root.Right.Left = new(tree.Node)
   root.Left.Right = tree.CrateNode(2)
   root.Right.Left.SetValue(4)

   root.Print()      //控制台输出:this method is shadowed,在重写Print()后默认调用的是重写后的Print()
   root.Node.Print() //控制台输出:3,若想调用父类Node中的Print()则需要通过实例root先调用父类类型名Node再调用Print()
}
posted @   雪碧锅仔饭  阅读(151)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示