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
}
递归法中序遍历思想:
值接收者vs指针接收者
- 要改变内容必须使用指针接收者
- 结构过大也考虑使用指针接收者
- 一致性:如有指针接收者,最好都是指针接收者
- 值接收者是go语言特有
- 值/指针接收者均可接收值/指针
4.2 包和封装
在Go语言中封装就是把抽象出来的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只能通过被授权的方法,才能对字段进行操作。Go语言通过方法名或者属性名首字母大小写来区分公有或者私有:大写为公有,所有包都可以调用;小写为私有,只有当前包可以调用。
封装的好处:
- 隐藏实现细节;
- 可以对数据进行验证,保证数据安全合理。
如何体现封装:
- 对结构体中的属性进行封装;
- 通过方法,包,实现封装。
封装的实现步骤:
- 将结构体、字段的首字母小写;
- 给结构体所在的包提供一个工厂模式的函数,首字母大写,类似一个构造函数;
- 提供一个首字母大写的 Set 方法(类似其它语言的 public),用于对属性判断并赋值;
- 提供一个首字母大写的 Get 方法(类似其它语言的 public),用于获取属性的值。
封装
go语言通过命名来实现封装
- 名字一般使用CamelCase
- 首字母大写表示public
- 首字母小写表示private
包
- 包是封装的界限
- 每个目录一个包,每个目录的包名是统一的
- main包包含可执行入口,含有main函数的包其包名只能是main
- 为结构体定义的方法必须与结构体放在同一个包内
- 可以是不同文件
【示例1】员工例子
对于员工,不能随便查看年龄,工资等隐私,并对输入的年龄进行合理的验证。代码结构如下:
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三个文件:
为结构体定义的方法必须与结构体放在同一个包内,所以将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语言没有继承,于是采用了以下方法:
- 定义别名
- 使用组合
4.3.1 使用组合
4.3.1.1 SuperMan
例子
SuperMan
是Human
的子类
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.go
、traversal.go
文件代码见【示例2】二叉树例子
【小结】 myTreeNode
类型和Node
类型有点类似继承的关系,myTreeNode
类型的实例可以通过实例名.node.(Node结构体中的成员变量/绑定Node类型的方法)
来从子类的myTreeNode
类型实例访问父类的Node
类型的成员变量和绑定父类Node
类型的方法
【注】go语言没有继承,以上的父类子类的说法仅是为了方便理解而使用。
4.3.2 定义别名
出栈入栈以及验证栈内是否为空功能的代码结构:
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()
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律