第四章 面向对象

简介: 第一天: go对象的基础. 如何创建结构体, 方法, 构造方法(工厂函数), 接收者模式第二天: 包, 如何引入外部包和系统包(定义别名或组合)第三天: 每个目录定义一个main方法.

第一天: go对象的基础. 如何创建结构体, 方法, 构造方法(工厂函数), 接收者模式


第二天: 包, 如何引入外部包和系统包(定义别名或组合)


第三天: 每个目录定义一个main方法.  


一. 面向对象介绍


1. go语言仅支持封装, 不支持继承和多态.


  那么继承和多态所做的事情, 怎么来做呢? 使用接口来实现, go语言是面向接口编程.


2. go语言只支持封装, 所以, go语言没有class, 只有struct

 

二. 结构体的用法


1. 结构体的创建方法

type TreeNode struct {
    Value int
    Left, Right *TreeNode
}
func main()  {
    //创建结构体的方法
    var root TreeNode
    root = TreeNode{Value:4}
    root.Left = &TreeNode{}
    root.Right = &TreeNode{5, nil, nil}
    root.Left.Right = new(TreeNode)
    fmt.Println(root)
}


  • 创建实例的几种方法


  • var root TreeNode


  • 变量名 := TreeNode{}


  • 使用内建函数new


  • 无论地址还是结构本身, 都使用.来访问成员


  •  这句话很重要, 之前就一直不明白, 为什么结构体也是打点就能访问呢


2. slice中实例化结构体的方法


func main()  {
    //创建结构体的方法
    var root TreeNode
    root = TreeNode{Value:4}
    root.Left = &TreeNode{}
    root.Right = &TreeNode{5, nil, nil}
    root.Left.Right = new(TreeNode)
    fmt.Println(root)
    nodes := []TreeNode{
        {4, nil,nil},
        {},
        {Value:3},
        {5, nil, &root},
    }
    fmt.Println(nodes)
}


在slice中构建结构体的时候, 可以省去结构体名


nodes := []TreeNode{
        {4, nil,nil},
        {},
        {Value:3},
        {5, nil, &root},
}


3. go语言构造函数?


root = TreeNode{Value:4}
root.Left = &TreeNode{}
root.Right = &TreeNode{5, nil, nil}
root.Left.Right = new(TreeNode)


  • go语言没有构造函数的说法. 但是从上面的例子可以看出, 他已经给出了各种各样的构造函数, 无参的, 一个参数的, 多个参数的


  • 如果我们还是想定义一个自己的构造方法怎么办?我们可以加工厂函数.


type TreeNode struct {
    Value int
    Left, Right *TreeNode
}
func  NewTreeNode(value int) *TreeNode {
    return &TreeNode{Value:value}
}


  • 看看这个构造函数, 入参是一个value, 出参是一个TreeNode的地址. 返回值是new了一个局部变量. 这就是工厂函数.


  • 工厂函数返回的是一个地址


问题: 在NewTreeNode函数里面返回了一个局部变量的地址. 这种java里是不允许的. 但在go中是允许的.


那么这个局部的TreeNode到底是放在堆里了还是放在栈里了呢?


c语言, 局部变量是放在栈上的, 如果想要呗别人访问到就要放在堆上, 结束后需要手动回收.


java语言, 类是放在堆上的, 使用的时候new一个, 用完会被自动垃圾回收


而go语言, 我们不需要知道他是创建在堆上还是栈上. 这个是由go语言的编译器和运行环境来决定的. 他会判断, 如果TreeNode没有取地址, 他的值不需要给别人用,那就在栈上分配, 如果取地址返回了, 那就是要给别人用, 他就在堆上分配. 在堆上分配完, 会被垃圾回收

如上: 我们定义了一个这样的结构


type TreeNode struct {
    Value int
    Left, Right *TreeNode
}
func NewTreeNode(value int) *TreeNode {
    return &TreeNode{Value:value}
}
func main()  {
    //创建结构体的方法
    var root TreeNode
    root = TreeNode{Value:3}
    root.Left = &TreeNode{}
    root.Right = &TreeNode{5, nil, nil}
    root.Left.Left = new(TreeNode)
    root.Left.Right = NewTreeNode(2)
}


微信图片_20220509172447.png


4. 如何给结构体定义方法


type TreeNode struct {
    Value int
    Left, Right *TreeNode
}
func  NewTreeNode(value int) *TreeNode {
    return &TreeNode{Value:value}
}
func (node TreeNode) Print() {
    fmt.Println(node.Value)
}


如上就定义了一个Print方法,


  • 有一个接收者(node TreeNode), 相当于其他语言的this. 其实go语言的这种定义方法的方式就和普通的方法定义是一样的


func Print(node TreeNode) {
    fmt.Println(node.Value)
}


功能都是相同的, 只不过, 写在前面表示是这个结构体的方法.使用上略有区别


// 结构体函数方法调用
root.print()
//谱图函数方法调用
print(root)


问题: 既然(node TreeNode)放在前面这种形式的写法和普通函数一样, 那么他是传值还

是传引用呢? 答案是传值. 我们来验证一下


type TreeNode struct {
    Value int
    Left, Right *TreeNode
}
func  NewTreeNode(value int) *TreeNode {
    return &TreeNode{Value:value}
}
func (node TreeNode) Print() {
    fmt.Println(node.Value)
}
func (node TreeNode) setValue() {
    node.Value = 200
}
func main()  {
    //创建结构体的方法
    var root TreeNode
    root = TreeNode{Value:3}
    root.Left = &TreeNode{}
    root.Right = &TreeNode{5, nil, nil}
    root.Left.Left = new(TreeNode)
    root.Left.Right = NewTreeNode(2)
    root.Print()
    root.setValue()
    root.Print()
}


输出结果:


33


由此, 可以看出, setValue()方法中修改了Value值为200,但是方法外打印依然是3. 说明: 接收者方法的方法定义是值拷贝的方式, 内部修改, 不会影响外面


那么,如何让他成功set呢, 我们给他传一个地址


func (node *TreeNode) setValue() {
    node.Value = 200
}


  • 和上一个方法的区别是: 接收者传的是一个地址. 用法和原来一样. 这样就实现了地址拷贝, 内部修改, 外部有效.


     总结:


    1. 调用print()方法是将值拷贝一份进行打印


    2. 调用setValue()方法是地址拷贝一份, 给地址中的对象赋值.


4. nil指针也能调用方法

  注意: 这里的重点是nil指针. 而不是nil对象


  这里为什么拿出来单写呢? 是因为, 他和我之前学得java是不同的. null对象调用方法, 调用属性都会报错, 而nil可以调用方法.


  我们先来看这个demo


type TreeNode struct {
    Value int
    Left, Right *TreeNode
}
func  NewTreeNode(value int) *TreeNode {
    return &TreeNode{Value:value}
}
func (node TreeNode) Print() {
    fmt.Println(node.Value)
}
func (node *TreeNode) setValue() {
    node.Value = 200
}
func main()  {
    var node TreeNode
    fmt.Println(node)
    node.Print()
    node.setValue()
    node.Print()
}


输出结果:


{0 <nil> <nil>}
0
200


这里main中的treeNode是对象, 不是地址. 他在初始化的时候如果没有值, 会给一个默认的值. 所以, 使用它来调用, 肯定都没问题. 我们这里要讨论的是空指针. 来看看空指针的情况


type TreeNode struct {
    Value int
    Left, Right *TreeNode
}
func (node *TreeNode) Print() {
    if node == nil {
        fmt.Println("node为空指针")
        return
    }
    fmt.Println(node.Value)
}
func main()  {
    var node *TreeNode
    fmt.Println(node)
    node.Print()
}


和上一个的区别是, 这里的TreeNode是一个指针.


来看看结果


<nil>
node为空指针


确实, 成功调用了Print方法, 并且捕获到node对象是空对象


但这里需要注意, 对nil对象调用属性, 依然是会报错的.


type TreeNode struct {
    Value int
    Left, Right *TreeNode
}
func (node *TreeNode) Print() {
    if node == nil {
        fmt.Println("node为空指针")
        // return
    }
    fmt.Println(node.Value)
}
func main()  {
    var node *TreeNode
    fmt.Println(node)
    node.Print()
}


把return注释掉. 看结果


微信图片_20220509172757.png


报了panic异常.


那么, 指针接收者是不是上来都要判断这个指针是否是nil呢? 这不一定, 要看使用场景.

 

5. 结构体函数的遍历


func(node *TreeNode) traveres() {
    if node == nil{
        return
    }
    node.Left.traveres()
    node.Print()
    node.Right.traveres()
}


遍历左子树, 打印出来, 在遍历又子树, 打印出来


结果:


0
0
3
5
4


注意: 这里的node.Left.traveres()的写法. 我们只判断了node是否为nil. 如果在java中, 我们还需要判断node.Left是否为null. 否则会抛异常, 但是go不会, nil指针也可以调用方法

 

到底应该使用值接受者还是指针接收者?


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


  • 结构过大也考虑使用指针接收者: 因为结构过大耗费很多内存


  • 一致性: 如果有指针接收者, 最好使用指针接收者 (建议)


  • 值接收者是go语言特有的.指针接收者其他语言也有, c有this指针, java的this不是指针,他是对对象的一个引用, python有self.


  • 值/指针接收者均可接收值/指针: 这句话的含义是, 我定义一个对象, 或者是指针对象, 都可以调用Print方法


func main()  {
    //创建结构体的方法
    var root TreeNode
    root = TreeNode{Value:3}
    root.Left = &TreeNode{}
    root.Right = &TreeNode{5, nil, nil}
    root.Left.Left = new(TreeNode)
    root.Right.Right = NewTreeNode(4)
    root.traveres()
    var node *TreeNode
    node.traveres()
}


  • root是一个值, node是一个指针, 都可以调用指针接收者traveres. 同样, root和node也都可以调用一个值接收者


三. 包


包里面重点说明的是


1. 首字母大写表示public, 首字母小写表示private


2. 包的定义: 一个目录下只能有一个包.  比如, 定义了一个文件夹叫tree. 那么他里面所有的文件的包名都是tree. 或者都是main(这样也是允许的). 不能既有tree又有main.  


四. 如何扩展系统包或者别人定义的包?


假如有一个别人写的结构体, 我想用, 但是还不满足我的需求, 我想扩展, 怎么扩展呢?

在其他语言, 比如c++和java都是继承, 但继承有很多不方便的地方. 所以go取消了继承.


用以下两种方法实现


  • 定义别名


  • 使用组合


1. 定义别名: 比如上面treeNode的例子. 如果我想在另外一个包里扩展, 使用定义别名的方式如何实现呢?


package main
import (
    "aaa/tree"
    "fmt"
)
// 原来遍历方式是前序遍历. 现在想扩展一个后序遍历. 怎么办呢? 我们使用组合的方式来实现一下
// 第一步: 自己定义一个类型, 然后引用外部类型. 引用的时候最好使用指针, 不然要对原来的结构体进行一个值拷贝
// 第二步: 扩展自己的方法
type myTreeNode struct {
    node *tree.TreeNode
}
func (myNode *myTreeNode) postorder() {
    if myNode == nil || myNode.node == nil{
        return
    }
    left := myTreeNode{myNode.node.Left}
    left.postorder()
    right := myTreeNode{myNode.node.Right}
    right.postorder()
    fmt.Print(myNode.node.Value)
}
func main(){
    //创建结构体的方法
    var root tree.TreeNode
    root = tree.TreeNode{Value:3}
    root.Left = &tree.TreeNode{}
    root.Right = &tree.TreeNode{5, nil, nil}
    root.Left.Left = new(tree.TreeNode)
    root.Right.Right = tree.NewTreeNode(4)
    root.Traveres()
    var node *tree.TreeNode
    node.Traveres()
    treeNode := myTreeNode{&root}
    treeNode.postorder()
}


第一步: 先定义一个自己的类型, 然后引入外部结构. 这里组好引入的是指针类型, 不然对外部结构还要进行一份值拷贝


type myTreeNode struct {
    node *tree.TreeNode
}


这样做, 当前这个对象已经拥有了原来定义的TreeNode结构体. 想象一下使用的时候, 传递进来了一个TreeNode类型的结构体. 然后我们对这个TreeNode结构体进行操作

 

第二步: 实现自己的方法, 后序遍历


func (myNode *myTreeNode) postorder() {
   // 这里需要注意的是myNode.node可能是空节点.
    if myNode == nil || myNode.node == nil{
        return
    }
    left := myTreeNode{myNode.node.Left}
    left.postorder()
    right := myTreeNode{myNode.node.Right}
    right.postorder()
    fmt.Print(myNode.node.Value)
}


取出外部结构体, 然后获取结构体的左子树. 在获取结构体的右子树, 在打印出来, 这样就实现了对原来结构体的调用了.

 

第三步: 调用


func main(){
    //创建结构体的方法
    var root tree.TreeNode
    root = tree.TreeNode{Value:3}
    root.Left = &tree.TreeNode{}
    root.Right = &tree.TreeNode{5, nil, nil}
    root.Left.Left = new(tree.TreeNode)
    root.Right.Right = tree.NewTreeNode(4)
    root.Traveres()
    var node *tree.TreeNode
    node.Traveres()
    treeNode := myTreeNode{&root}
    treeNode.postorder()
}


调用也很简单. 吧root传进来地址, 然后调用方法即可


 

2. 定义别名的方式实现外部结构体或系统结构体的调用


下面我们给切片定义一个别名. --- 队列


package main
import "fmt"
type Queue []int
func(q *Queue) add(v int){
    *q = append(*q, v)
}
func(q *Queue) pop() int{
    tail := (*q)[len(*q)-1]
    *q = (*q)[:len(*q)-1]
    return tail
}
func(q *Queue) isEmpty() bool {
    return len(*q) == 0
}
func main() {
    q := Queue{1}
    q.add(2)
    q.add(3)
    fmt.Println(q.pop())
    fmt.Println(q.pop())
    fmt.Println(q.isEmpty())
    fmt.Println(q.pop())
    fmt.Println(q.isEmpty())
}


第一步: 给切片定义一个别名


type Queue []int


然后对这个切片进行操作, 添加一个元素


func(q *Queue) add(v int){
    *q = append(*q, v)
}


这里需要注意: 在add方法里. 我们上面说了接收者这种写法类似于this, 但是这个方法里, *q 对地址的值进行修改了. 也就是说add以后, 他已经不是原来的地址了.


我们运算完以后的地址也不是原来的地址了


func main() {
    q := Queue{1}
    fmt.Printf("地址: 0x%x \n", &q[0])
    q.add(2)
    fmt.Printf("地址: 0x%x \n", &q[1])
    q.add(3)
    fmt.Println(q.pop())
    fmt.Println(q.pop())
    fmt.Println(q.isEmpty())
    fmt.Println(q.pop())
    fmt.Println(q.isEmpty())
}


地址: 0xc000096008 
地址: 0xc000096028 
3
2
false
1
true


两次打印出来的地址是不同的. 说明他的地址变了

 

五. 包名的定义, 每一个文件夹下面只能有一个main


我们用系统包来举例

微信图片_20220509173050.png


 所以,我们在定义文件的时候, 在每一个文件夹下定义一个main函数.  

 


相关文章
|
6月前
|
Java 物联网 测试技术
Java面向对象程序设计3面向对象基础
Java面向对象程序设计3面向对象基础
327 0
|
5月前
|
算法 编译器 C语言
C++进阶之路:深入理解编程范式,从面向过程到面向对象(类与对象_上篇)
C++进阶之路:深入理解编程范式,从面向过程到面向对象(类与对象_上篇)
66 3
|
C++
20 C++ - 面向对象程序设计案例
20 C++ - 面向对象程序设计案例
82 0
|
6月前
|
C++
C++从入门到精通:2.1.2函数和类——深入学习面向对象的编程基础
C++从入门到精通:2.1.2函数和类——深入学习面向对象的编程基础
|
C++
C++语言学习面向对象编程应用案例
C++语言学习面向对象编程应用案例
82 1
|
搜索推荐
|
存储
面向对象程序设计第四章
面向对象程序设计第四章
128 1
面向对象程序设计第四章
|
存储 Java
面向对象程序设计概述
面向对象程序设计概述
196 0
面向对象程序设计-第二章
面向对象程序设计-第二章
49 0
面向对象程序设计-第二章
面向对象程序设计-第一章:面向对象程序设计基础
面向对象程序设计-第一章:面向对象程序设计基础
179 0
面向对象程序设计-第一章:面向对象程序设计基础
下一篇
无影云桌面