Golang 面向对象全解

Posted Lejeune

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang 面向对象全解相关的知识,希望对你有一定的参考价值。

Golang 面向对象全解


    golang的面向对象是不完备的面向对象,大部分人员都是从java转换而来,造成了很多的坑,这一篇文章记录我已知的golang面向对象性质和坑,希望后来者注意。
    

此文仅是个人在学习过程中摸索的自我总结,无法保证完全理解正确,如有错误,欢迎指正,共同学习!

基础

    go中没有class的概念,只有与之类似的struct结构体(很像c语言),还有接口interface

​ 接下来简单对比一下,他与普通面向对象语言的概念上的区别。

类/结构接口
gostruct
结构体
定义阶段只能描述成员变量
后面通过在函数前面增加标识符来完成方法的描述,可读性不如java
interface
接口
只能描述方法接口
javaclass

可以描述成员变量、方法
interface
接口
可以描述成员与方法接口

    定义上go的interface只用作一种类型定义,可以认为是方法的集合,我们不需要显式的去声明struct继承了什么接口 然后去实现它,而是先去实现那个方法,所有实现了接口中所描述方法的结构体都认为其实现了这个接口。

    这个特性非常有趣,但是貌似也容易出错,无法显式地告诉我们,我们哪些方法还未实现。

    而还有一个特殊的接口interface{}被叫做空接口,里面有0个方法,认为所有对象都实现了空接口

    在做代码对比之前,我们需要先引入接口类型判断的方法,才能说明问题。

类型判断/转换

  1. 转换

    a:=3
    float32(a)
    

        所有类型转换(包括之后的类型转换,和函数参数调用中所声明的类型)严格遵守类型转换后的类型,比如someInterface(Object),这个新对象无法调用原对象当中的首字母大写的成员变量,同时也无法调用没有在someInterface中记录的方法,也就是只能调用someInterface中所记录的方法。

  2. 枚举判断(在已知类型中判断)

    switch interface{}(float32(3)).(type) {
    	case float32:
    		fmt.Prntln("this is float32")
    		
    	
    	}
    
    interface{}(3)//所有对象都实现了空接口
    

        注意 ,Object.(type),Object需要是interface{}类型,interface{}指的是空接口,所有对象都实现了这个接口,从而可以使用上面的类型转换使对象变成interface{}类型。这个语法需要与switch配合

  3. 类型尝试转换(常用于判断是否实现接口)

    语法

    newObject,ok:=Object.(someType)
    

    其中Object对象要求是interface{}类型,ok是转换结果,true or false,newObject是转换之后的对象。

golang中的接口和结构体

  1. struct结构体

    1. 结构体定义时只能描述成员变量

    2. 成员变量以首字母大小写区分公有与私有,首字母大写为公有变量,小写反之。

    3. 结构体类型声明后,其对象存储的是对象实例

      type Person struct {
      	Name string
      }
      var p Person=Person{"Jack"}
      
    4. 结构体定义完成后,可以通过对正常函数增加前缀的方式完成方法的定义与实现。

      //Person类定义同上
      //方式一
      func (p Person) say(){
        fmt.Println(p.Name)
      }
      //这样就定义了一个对象方法,其中p是Person对象的实例
      //
      //方式二
      func (p *Person) say(){
        fmt.Println(p.Name)
      }
      //此时p是Person对象的指针,此时p.Name也可以等价为(*p).Name,go自动做了转换,所以两个代码等价
      //看起来两者都可以完成类方法的定义与实现,但是在使用上会有不同,将会在坑点里面讲 
      
  2. Interface 接口

    1. 接口只能留出方法描述,不做具体实现

    2. 接口不能描述成员变量

Go的面向对象实现

    首先面向对象具有三大特性,封装、继承、多态,go均不直接支持这三种特性,但都可以通过别的方式来实现这三种特性。

封装

    golang中没有class只有struct结构体,类比c语言。go中成员变量没有权限修饰符,public等,其通过成员变量的变量名开头大小写来控制访问。

type Person struct {
	Name string//公有变量
  ago int//私有变量
}

继承

    go中继承是通过包含来实现的。

    包含有两种情况,一种是和java一样,父亲有一个儿子对象,儿子和父亲各自拥有自己的名字,此时的儿子和父亲相当于两个不同的类,只是父亲有一个儿子对象而已。

package main

import "fmt"

type Person struct {
	Name string
}

type Say interface {
	say()
}

func (p *Person)say()  {
	fmt.Println("My name is"+p.Name)
}

type Father struct {
	son Person
	Name string
}

func (f Father)teach()  {
	fmt.Println("I am "+f.Name+", teaching my son"+f.son.Name)
}

func main() {
	p:=Person{"Jack"}
	f:=Father{p,"Tom"}
	f.teach()
}

    上面是java意义下的包含,下面这一种是通过包含实现go的继承,通过传入匿名对象,使得父类的所有成员变量、方法都变为子类的。

package main

import "fmt"

type Person struct {
	Name string
}

type Say interface {
	say()
}

func (p *Person)say()  {
	fmt.Println("My name is "+p.Name)
}

type Teacher struct {
	Person
}

func (f Teacher)teach()  {
	fmt.Println("I am a teacher! And  My name is "+f.Name)
}


func main() {
	t:=Teacher{Person{"Tom"}}
	t.say()
	t.teach()

}

多态

    同一个接口不同实现,这个方法我认为在go中是比较鸡肋的。

package main

import "fmt"

type Person struct {
	Name string
}

type Say interface {
	say()
}

func (p Person)say()  {
	fmt.Println("My name is "+p.Name)
}

type Teacher struct {
	Name string
}

func (f Teacher)say()  {
	fmt.Println("I am a teacher! And  My name is "+f.Name)
}


func main() {
	var t,p Say =Teacher{"Tom"},Person{"Jack"}
	
	t.say()
	p.say()


}

坑点

  1. 接口实现无法被程序员直接知道,只能依赖于ide来告诉程序员这个接口中的方法没有被实现。

  2. go在接口类型对象赋值的时候,可以接受其引用或者实例。

    package main
    
    import (
    	"fmt"
    )
    
    type Person struct {
    	Name string
    }
    
    type Say interface {
    	say()
    }
    
    func (p Person)say()  {
    	fmt.Println("My name is "+p.Name)
    }
    
    func main(){
      //以下两种都可以,但是在大多数情况下,对于接口直接用指针赋值一定不会错
      //这两者的不同请看下一个坑点
    	//var p Say=&Person{"Tom"}
      var p Say=Person{"Tom"}
    	p.say()
    }
    
  3. ide判断接口实现与否,严格按照定义的前缀

        之前提到函数名前面的类型加不加星号,看起来没有区别。但是当我们将前缀加上星号时,

    //上面不变
    func (p *Person)say()  {
    	fmt.Println("My name is "+p.Name)
    }
    
    func main(){
      //这句话报错
    	//var p Say=Person{"Tom"}
      //这句话正常
      var p Say=&Person{"Tom"}//new(Person)返回的也是指针,也可以使用
    	p.say()
    }
    

    这是因为,ide在检测接口是否实现时,严格按照上面的定义类型,现在方法被定义在Person类的指针上,Person实例就无法调用,所以在使用时仅能通过指针调用,加上接口对象能接受指针或实例,所以需要加一个取地址符号。如果上面是没有星号的,说明方法定义在实例上,实例和指针都能对其进行调用。但是不管怎么说,对于接口实例对象加一个取地址符号&,是一定能运行的。建议对于接口类型都直接加上&

  4. 子类调用父类方法上的异常

    package main
    
    import (
    	"fmt"
    )
    
    type Person struct {
    	Name string
    }
    
    //type Say interface {
    //	say()
    //}
    func (p *Person)call()  {
    	p.say()
    }
    
    func (p *Person)say()  {
    	fmt.Println("My name is "+p.Name)
    }
    
    type Teacher struct {
    	Person
    }
    
    func (t Teacher)say()  {
    	fmt.Println("I'm a teacher,"+t.Name)
    }
    
    
    
    func main(){
    	var t Teacher=Teacher{Person{"Tom"}}
    	t.call()
    }
    

        这段代码去调用了person的say,和java上的结果是不同的,这是反直觉的。

        猜测实质是因为go的函数调用严格遵守类型,call函数的形式参数是person 那么他就不知道存在teacher的say函数,所以直接去调用了person的say。

        那么如何解决?之前说接口是一类方法的集合,我们调用方法时,先想一想,我们到底需要多少方法,将这个函数剥离出结构体之外 ,单独成为一个函数,形式参数传递所需要的方法组成的接口对象,如下:

    package main
    
    import (
    	"fmt"
    )
    
    type Person struct {
    	Name string
    }
    
    type Say interface {
    	say()
    }
    func call(s Say)  {
    	s.say()
    }
    
    func (p *Person)say()  {
    	fmt.Println("My name is "+p.Name)
    }
    
    type Teacher struct {
    	Person
    }
    
    func (t Teacher)say()  {
    	fmt.Println("I'm a teacher,"+t.Name)
    }
    
    
    
    func main(){
    	var t Teacher=Teacher{Person{"Tom"}}
    	call(t)
    }
    

        但是每一次子类方法重载后都需要写一个外部函数去调用,有时候属实麻烦,我们希望通过继承能够完成像java一样的直接调用重载的接口,可以考虑在实现方法的时候,不去调用他原本所属类的实例,而是通过传入一个方法接口实例,从而达到写一次能继承的效果。

    package main
    
    import (
    	"fmt"
    )
    
    type Person struct {
    	Name string
    }
    
    type Say interface {
    	say()
    }
    
    //注意此处 前缀只用作限制其方法所属,真正实例由方法调用时传入
    func (Person)call(s Say)  {
    	s.say()
    }
    
    func (p Person)say()  {
    	fmt.Println("My name is "+p.Name)
    }
    
    type Teacher struct {
    	Person
    }
    
    func (t Teacher)say()  {
    	fmt.Println("I'm a teacher,"+t.Name)
    }
    
    
    
    func main(){
    	var t Teacher=Teacher{Person{"Tom"}}
    	t.call(t)
    }
    

        不管是哪种方法都对程序员的抽象能力做了更高的要求,程序员需要清晰的知道,方法内部需要调用哪些方法,从而抽象出其接口。如果怕出错,不如在有继承关系的结构体实现时,都通过参数来传递对象。

    golang的坑点大多数都在interface上,这是我们学习常规面向对象语言之后所遗留下来的情况,听说go官方并不是很建议用go去面向对象,但是面向对象确实能提高效率。。。我们在go中不能以之前的面向对象思维去看待它。

以上是关于Golang 面向对象全解的主要内容,如果未能解决你的问题,请参考以下文章

Golang 面向对象全解

Golang 面向对象全解

VSCode自定义代码片段——JS中的面向对象编程

VSCode自定义代码片段9——JS中的面向对象编程

代码片段 - Golang 实现集合操作

golang 面向对象特性