Go语言3小时光速入门03——面向对象

*Go中面向对象的基本理念

此节偏理论,如果对其他面向对象语言理解不深,或者对此节内容感到困惑的同学可以先跳过。

组合优于继承(所以在golang中干脆直接干掉了继承,只支持聚合(也就是组合))

Go中主要就是用委托模式

像"类"、"对象"以及"实"这些名词在传统的多层次继承式面向对象编程语言中(如Java,C++等)已经有了的非常清晰的定义,但在Go语言中完全避开使用它们。相反,我们使用“类型”和“值”,其中自定义类型的值可以包含方法。

由于没有继承,因此也就没有虚函数(Java中叫抽象方法)。Go语言对此的支持则是采用类型安全的鸭子类型(duck type)。在 Go语言中,参数可以被声明为一个具体类型(例如,int、string、或者*os.File以及MyType),也可以是接口(interface),即提供了具有满足该接口的方法的值。对于一个声明为接口的参数,我们可以传入任意值,只要该值包含该接口所声明的方法。例如,如果我们有一个值提供了一个Write([]byte)(int, error)方法,我们就可以将该值当做一个io.Writer(即作为一个满足io.Writer接口的值)提供给任何一个需要io.Writer参数的函数,无论该值的实际类型是什么。这点非常灵活而强大,特别是当它与 Go语言所支持的访问嵌入字段的方法相结合时。

继承的一个优点是,有些方法只需在基类中实现一次,即可在子类中方便地使用。Go语言为此提供了两种解决方案。其中一种解决方案是使用嵌入。如果我们嵌入了一个类型,方法只需在所嵌入的类型中实现一次,即可在所有包含该嵌入类型的类型中使用。另一种解决方案是,为每一种类型提供独立的方法,但是只是简单地将包装(通常都只有一行)了功能性作用的代码放进一个函数中,然后让所有类的方法都调用这个函数。

Go语言面向对象编程中的另一个与众不同点是它的接口、值和方法都相互保持独立。接口用于声明方法签名,结构体用于声明聚合或者嵌入的值,而方法用于声明在自定义类型(通常为结构体)上的操作。在一个自定义类型的方法和任何特殊接口之间没有显式的联系。但是如果该类型的方法满足一个或者多个接口,那么该类型的值可以用于任何接受该接口的值的地方。当然,每一个类型都满足空接口(interface{}),因此任何值都可以用于声明了空接口的地方。

一种按Go语言的方式思考的方法是,把is-a关系看成由接口来定义,也就是方法的签名。因此,一个满足io.Reader 接口(即有一个签名为 Read([]byte)(int, error)的方法)的值就叫做 Reader,这并不是因为它是什么(一个文件、一个缓冲区或者一些其他自定义类型),而是因为它提供了什么方法,在这里是Read()方法。而has-a关系可以使用聚合或者嵌入特定类型值的结构体来表达,这些类型构成自定义类型。

虽然没法为内置类型添加方法,但可以很容易地基于内置类型创建自定义的类型,然后为其添加任何我们想要的方法。该类型的值可以调用我们提供的方法,同时也可以与它们底层类型提供的任何函数、方法以及操作符一起使用。例如,假设我们有个类型声明为type Integer int,我们可以不拘形式地使用整型的+操作符将这两种类型的值相加。并且,一旦我们有了一个自定义类型,我们也可以添加自定义的方法。

再谈函数

推荐书目👇
计算机程序的构造和解释

在go语言入门第一节基础HelloWorld中我们对函数进行了基本的说明,但是由于在系列开头比较匆忙,所以这里我们重新介绍一下(其实我这里是想深入进行一些说明但是发现篇幅太大且涉及一些设计思想和理念,在刚学习一门语言时就去理解这些会让人感觉很苦恼(包括上面一段...),所以基于简洁明了易懂的原则进行了压缩):

在Go语言中函数是一等公民(在Scala中你也会经常看到这句话)。但是如何进行深入的理解呢,这里感兴趣的童鞋我推荐可以去阅读上面的推荐书籍《计算机程序的构造和解释》

可变长参数

go语言中支持可变长参数,用...省略号表示

func 函数名(变长参数名 ...参数类型) 返回值类型 {

}

如:

func sum(args ...int) int{
	sum := 0
	for _,op := range args{
		sum += op
	}
	return sum
}

//另外当连续几个参数类型相同时可以简写

如:

func TestArgs(a,b,c,d int, s1,s2 string){
//....
}


命名返回值

我们可以直接给返回值命名(相当于在函数内直接创建了一个变量)


func sum(args ...int) (ret int){
	for _,op := range args{
		//ret不用提前定义
		ret += op
	}
	//命名后直接返回即可
	return
}

defer延迟执行函数

在go语言中我们可以通过defer关键字延迟一个函数(或者当前所创建的匿名函数)的执行,它会在外层函数返回之前但是其返回值(如果有的话)计算之后执行。

熟悉Java,Python,C#的同学有没有对这个描述眼熟?如果我改成:“defer的代码在方法/函数返回前调用”,有没有想到一个东西try{}finally{}?(C++因为异常了只有析构函数释放资源所以可以理解为在函数返回前统一调用了一个方法)我们可以用finally来理解go语言中的defer,只不过不需要try。

需要注意的是在go中一个函数可以有多个defer,那么会以LIFO后进先出(出栈)的顺序执行。

创建defer函数

函数体{
defer 调用要执行的函数

}

或

//在函数内创建匿名函数
defer func(参数名 参数类型){

}(传入参数)


匿名函数

匿名,即没有名字的函数...

func(参数名 参数类型){

     函数体

}(内部调用,给这个匿名函数传入的参数)

func_test.go:

/*
延迟执行函数defer
 */
func TestDefer(t *testing.T){
	defer tryCatch()
	fmt.Println("try")
}

func tryCatch(){
	fmt.Println("finally")
}

/*
defer匿名函数,传参执行
*/
func TestDefer2(t *testing.T){
	i := 0
	defer func(i int) {
		t.Log("要返回了!我赶紧执行",i)
	}(i)
	
	t.Log("我执行了,准备返回",i)
}


/*
多个defer,按出栈顺序执行
*/
func TestDefer3(t *testing.T){
	defer func() {
		t.Log("等等我,我还没上车呢!喂!我还没上车呢!")
	}()

	t.Log("我是函数体执行了,准备返回")

	defer func() {
		t.Log("好的,我也完成了,返回")
	}()

}

struct结构体

结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。

结构体对于熟悉C系语言的人来说肯定很熟悉不需要多说,对于Java的同学来说可能就比较懵逼了。

这里我们看到结构体的定义第一个想到应该就是类,不同的类型结构组成的集合,不就是Java里面的一个类吗?(只不过没有方法并且不是一个单独的文件)所以这里我们可以对照的学习。

结构体的定义

在go语言中我们可以通过struct关键字定义一个结构体

并且如果结构体需要暴露出去(首字母大小),则要加上注释

type 结构体名 struct {
成员1 成员类型
成员2 成员类型
}

如:

//User 定义了一个结构体User,有id,name,password等成员
type User struct {
	 id int64
	 name string
	 password string
}

创建实例与初始化

在go语言中创建一个实例有多种方式

	实例名 := 要创建的实例类型{成员属性值1,成员属性值2,...}
	实例名 := 要创建的实例类型{成员属性名1:成员属性值1,成员属性名2:成员属性值2,...}
	实例名 := new(要创建的实例类型)

	如我们以上面的User为例创建实例:

	user := User{1,"张三","123"}
	user := User{id:1,name:"张三",password:"123"}
	user := new(User)

实例的赋值与修改

和java一样我们可以通过实例名.成员属性名的方式获取实例中的属性,同样我们可以通过实例名.成员属性名 = 值的方式修改实例中成员的值。
如:我们将User的id修改为为2.

user := new(User)
user.id = 2

行为(方法)定义

前面结构体和Java类对象相比有一个明显的差异(或者说不足)那就是没有该实例的行为。我们知道在java中一个对象被分为属性和方法。其中属性用于描述对象拥有的属性,方法用于描述对象的行为。

那么在Go语言中,如何给一个实例添加或定义行为呢?

在Go语言中我可以通过函数绑定的方式为一个结构体绑定对应的行为(函数)。

在实例上添加行为:

func (要绑定实例名 要绑定类型) 函数名(参数名 参数类型...)(返回类型...){

	函数体

}

如:我们需要在前面的User中添加"打招呼"的行为。

oo_test.go:

type User struct {
	 id int64
	 name string
	 password string
}

/*
给User赋予打招呼的行为
*/
func (user User) sayHello(){
   fmt.Println("Hi! I'm ",user.name)
}

func TestBlindFuc(t *testing.T){
	user := new(User)
	user.name = "张三"
	user.sayHello()
}

注意:用上面的方法给函数绑定实例时,因为前面我们讲过Go中都是值传递,所以如果直接绑定类型本身,在实例对应方法被调用时,实例的成员会进行值复制。

所以通常情况下为了避免内存拷贝我们可以绑定指针,即在类型前面加一个*号来取指针

//注意这个*号
func (user *User) sayHello(){
  type User struct {
	 id int64
	 name string
	 password string
}

/*
给User赋予打招呼的行为
注意这个*号
*/
func (user *User) sayHello(){
   fmt.Println("Hi! I'm ",user.name)
}

func TestBlindFuc(t *testing.T){
	user := new(User)
	user.name = "张三"
	user.sayHello()
}
}

Go语言中的接口

接口是什么?有面向对象经验的应该都知道吧...

在 Go语言中,接口是一个自定义类型,它声明了一个或者多个方法签名。接口是完全抽象的,因此不能将其实例化。然而,可以创建一个其类型为接口的变量,它可以被赋值为任何满足该接口类型的实际类型的值。

其中
interface{}类型是声明了空方法集的接口类型。无论包含不包含方法,任何一个值都满足 interface{}类型。(相当于Java中的Object,只不过Go语言中没有继承的概念只有基于鸭子类型的接口封装)

*毕竟,如果一个值有方法,那么其方法集包含空的方法集以及它实际包含的方法。这也是interface{}类型可以用于任意值的原因。我们不能直接在一个以interface{}类型值传入的参数上调用方法(虽然该值可能有一些方法),因为该值满足的接口没有方法。因此,通常而言,最好以实际类型的形式传入值,或者传入一个包含我们想要的方法的接口。当然,如果我们不为有方法的值使用接口类型,我们就可以使用类型断言、类型开关或者是反射等方式来访问。(这些方法见后面章节)。

在Go语言中:接口为非入侵性,实现不依赖于接口定义,所以接口的定义可以包含在接口使用者包内

接口定义

type 接口名 interface{
  行为函数签名
}

如我们定义一个Person接口:

type Person interface {
	GetUserName()string
}

接口实现

type Person interface {
	GetUserName()string
}


type User struct {
	 id int64
	 name string
	 password string
}

/*
User实现接口Person的GetUserName()行为
注意,接口只定义行为
*/
func (u *User) GetUserName()string{
	return u.name
}

接口变量

在上面的接口实现中,我们通过User类型去实现了Person接口的GetUserName,然后我们只需要通过User去调用就可以了。这样固然可行,但是有没有一种办法可以让我们通过接口去调用了?如何写出类似Java中List list = new ArrayList的语句。

在Go中我们可以通过像Java一样子类型赋给父类型var 变量名 接口类型 = 实现类型值var 变量名 接口类型 = &实现类型{}来获取一个接口类型变量。

如:
oop_test.go

type Person interface {
	GetUserName()string
}

type User struct {
	 id int64
	 name string
	 password string
}


func (u *User) GetUserName()string{
	return u.name
}

func TestOOP(t *testing.T){
	user := new(User)
	user.name = "张三"
	var person Person = user
	fmt.Println(person.GetUserName())
	var person2 Person = &User{1,"李四","321"}
	fmt.Println(person2.GetUserName())
}

Go语言的复合(组合/聚合)

因为Go语言不支持继承,那么它如何来做到对传统面向对象的父类代码的复用呢?

以Java为例,当我们在一个类中需要用到ArrayList存储数据时,一般而言有两种办法。

  1. 在该类中添加一个类型是ArrayList的属性,需要使用时通过该属性去调用ArrayList的方法。
  2. 将该类直接继承ArrayList这样我们就有了ArrayList的全部功能。

事实上我们很少使用第二种方法,最直观的一点是它将自身与ArrayList的耦合度加大了。上述的两种方法,第一种我们就称之为组合,第二种则是继承。

在Go语言中也是通过同样的方法来完成代码的复用的。当然如果每次都通过类型值.成员.函数的方法来调用自己的成员类型函数未免繁琐,所以go语言中增加了匿名类型嵌入

如:我们在上面的例子中加上HumanUser和WomanUser两个新的自定义类型

oop_test.go:

type Person interface {
	GetUserName()string
}

type User struct {
	 id int64
	 name string
	 password string
}

type WomanUser struct {
	//传统的成员类型,定义成员变量名
	user User
}


type HumanUser struct {
	//匿名成员变量类型
	User
}

func TestOOP(t *testing.T){
	user := new(User)
	user.name = "张三"
	var person Person = user
	fmt.Println(person.GetUserName())
	var person2 Person = &User{1,"李四","321"}
	fmt.Println(person2.GetUserName())

	woman := WomanUser{user:*user}
	human := HumanUser{*user}
	//显示的传统成员需要 类型值.成员.函数
	fmt.Println(woman.user.GetUserName())
	//匿名的可以直接 类型值.函数
	fmt.Println(human.GetUserName())
}

一定要注意:虽然上面的匿名成员类型看上去是直接调用。但它并不是继承go语言中没有继承!

Go语言中的多态

因为前面我们知道go语言中函数是直接和类型进行绑定的,所以在go中实现多态非常简单——只需要把新函数实现绑定到对应类型即可。

如:在上面的例子中我们再添加上儿童类型,儿童在GetUserName时除了返回名字还要在名字后面加上儿童两个字,以提高说明。

type Person interface {
	GetUserName()string
}

type User struct {
	 id int64
	 name string
	 password string
}

type HumanUser struct {
	User
}

type WomanUser struct {
	User
}

type ChildUser struct {
	User
}

func (u *User) GetUserName()string{
	return u.name
}

func (u *ChildUser) GetUserName()string{
	return u.name + "【儿童】"
}

func TestOOP(t *testing.T){
	user := new(User)
	user.name = "张三"
	var person Person = user
	fmt.Println(person.GetUserName())
	var person2 Person = &User{1,"李四","321"}
	fmt.Println(person2.GetUserName())

	woman := WomanUser{*user}
	human := HumanUser{*user}
	child := ChildUser{*user}

	fmt.Println(woman.GetUserName())
	fmt.Println(human.GetUserName())
	fmt.Println(child.GetUserName())
}

空接口

空接口表示没有任何行为签名的接口interface{}相当于Java中的Object。你也可以理解为空集是所有集合的子集。所以用空接口可以表示任何类型。

我们可以通过断言来将空接口转换为指定的类型。
如:

//传入空接口类型参数(即外面什么类型都可以传)
func NullInterface(i interface{}){
	/*
	断言转换将传入的空接口转为int。返回两个值
	如果ok = true表示转换成功,num是转换后的值
	*/
	num,ok := i.(int)
	if ok {
		fmt.Println(num)

	}else{
		//转换错误
		//num,ok2:= i.(int) 
		//...	
	}
}

Go语言接口的基本建议

尽量使用小的接口定义,很多接口只包含一个方法
较大的接口定义,可以由多个小接口定义组合而成
只依赖于必要功能的最小接口

更新时间:2020-03-26 18:22:19

本文由 寻非 创作,如果您觉得本文不错,请随意赞赏
采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
原文链接:https://www.zhouning.group/archives/go语言3小时光速入门03面向对象
最后更新:2020-03-26 18:22:19

评论

Your browser is out of date!

Update your browser to view this website correctly. Update my browser now

×