第四章 面向对象

前端之家收集整理的这篇文章主要介绍了第四章 面向对象前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

第一天: 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()  {
    .Println(root)


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

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

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

 

3. go语言构造函数?

root = TreeNode{Value:}
root.Left = &TreeNode{}
root.Right = &TreeNode{go语言没有构造函数的说法. 但是从上面的例子可以看出,他已经给出了各种各样的构造函数,无参的,一个参数的,多个参数的
  • 如果我们还是想定义一个自己的构造方法怎么办?我们可以加工厂函数.  
  •   

    TreeNode
    }
    
    func  NewTreeNode(value int) *TreeNode {
        return &TreeNode{Value:value}
    }
    • 看看这个构造函数,入参是一个value,出参是一个TreeNode的地址. 返回值是new了一个局部变量. 这就是工厂函数.
    • 工厂函数返回的是一个地址

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

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

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

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

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

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

    TreeNode
    }
    
    func NewTreeNode(value int) *TreeNode {
        return &TreeNode{Value:value}
    }
    
    func main()  {
         new(TreeNode)
        root.Left.Right = NewTreeNode(2)
        
    }

     

     

     

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

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

    如上就定义了一个Print方法

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

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

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

      问题: 既然(node TreeNode)放在前面这种形式的写法和普通函数一样,那么他是传值还是传引用呢? 答案是传值. 我们来验证一下

      TreeNode{Value:value}
      }
      
      func (node TreeNode) Print() {
          .Println(node.Value)
      }
      
      func (node TreeNode) setValue() {
          node.Value = 200
      }
      
      func main()  {
          )
          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

      

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

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

     

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

    来看看结果

    <nil>
    node为空指针

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

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

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

    把return注释掉. 看结果

    报了panic异常.

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

     

    5. 结构体函数的遍历

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

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

    结果: 

    3
    5
    4

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

     

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

    • 要改变内容,必须使用指针接收者
    • 结构过大也考虑使用指针接收者: 因为结构过大耗费很多内存
    • 一致性: 如果有指针接收者,最好使用指针接收者 (建议)
    • 值接收者是go语言特有的. 指针接收者其他语言也有,c有this指针,java的this不是指针,他是对对象的一个引用,python有self.
    • 值/指针接收者均可接收值/指针: 这句话的含义是,我定义一个对象,或者是指针对象,都可以调用Print方法
       new(TreeNode)
          root.Right.Right = NewTreeNode()
      
          root.traveres()
          
          *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()
        .Print(myNode.node.Value)
    }
    
    func main(){
        
        var root tree.TreeNode
        root = tree.TreeNode{Value:tree.TreeNode{}
        root.Right = &tree.TreeNode{ new(tree.TreeNode)
        root.Right.Right = tree.NewTreeNode()
    
        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可能是空节点.
    myTreeNode{myNode.node.Left} left.postorder() right := myTreeNode{myNode.node.Right} right.postorder() .Print(myNode.node.Value) }

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

     

    第三步: 调用

    func main(){
        tree.TreeNode
        node.Traveres()
    
        treeNode := myTreeNode{&root}
        treeNode.postorder()
    }

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

     

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

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

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

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

    type Queue []int

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

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

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

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

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

     

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

    我们用系统包来举例

     

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

     

    猜你在找的Go相关文章