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 (要绑定实例名 要绑定类型) 函数名(参数名 参数类型...