go struct¶
Quote
I think a lot of new programmers like to use advanced data structures and advanced language features as a way of demonstrating their ability. I call it the lion-tamer syndrome. Such demonstrations are impressive, but unless they actually translate into real wins for the project, avoid them. - Glyn Williams
go 支持 OOP 么?¶
细心的你可能发现了,go 连 class 关键字都没有,如何支持面向对象编程呢?流行的编程语言一般都支持定义一个类,类里边有数据(data)和方法(method)。 go 虽然没有提供 class 关键词,但是提供了 struct 用来定义自己的类型,struct 里可以放入需要的数据成员,并且可以给自定义 struct 增加方法。
使用 struct 自定义类型¶
我们开始使用 go 的 struct 来看一下 go 里边是如何实现类似其他编程语言的面向对象编程的。在 go 语言里,我们使用 struct 来定义一个新的类型,这和使用 class 非常像,比如这里我们定义一个简单的动物结构(类),包含 Name 和 Age 成员:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
ok,然后还可以给 struct 定义方法,比如动物都需要睡觉,所以我们给它添加一个方法叫做 Sleep:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
这样定义既有数据又有了方法,是不是和类比较像了,这就是在 go 中使用 OOP 的方式。当然了 OOP 还远不止这些,比如传统的 面向对象编程还有访问控制,构造函数,继承,多态等概念,我们会看一下它们是如何在 go 里边实现的,go 的方式和 Python/Java 等实现还是有挺大区别的。
访问控制¶
在 Java 和 C++ 中,对于类的成员有着比较严格的访问控制,比如对成员有 public/private 等关键词用来声明它的访问权限。但是像是 Python 的实现就没有那么严格,Python 里边是通过命名的方式来约定的,比如私有方法和成员一般是用下划线开头,告诉调用者这个是 类的私有方法和成员,你不应该直接使用它们,而是使用暴露出去的公有方法。但这只是一个『君子协定』,如果类的设计者没有 设计完善,让你非要去访问下划线开头的『私有方法』,其实 python 也不会禁止。
Go 也有类似的访问控制,不过是通过数据和方法的命名首字母大小写决定的。在 Go 的包(package)中,只有首字母大写的才能被其他包 导入使用,小写开头的则不行。所以一般结构体的私有数据成员和方法,我们使用小写开头,而公有的数据成员和方法,我们使用大写开头就好了。
我们现在给 Animal 加上一个私有的数据成员叫做 petName 表示动物的小名,同时新增一个方法叫做 SetPetName 用来设置它:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
打印上边代码看下结果?符合你的预期么?如果没有正确设置 petName 为什么呢?
指针接收者(pointer receiver) vs 值接收者(value receiver)¶
在函数章节我们讲到过函数的参数传递都是通过值进行传递的,也就是会复制参数的值,如果我们想要修改传入的值,就需要传递一个指针, 这就是为什么上边的 SetPetName 方法没有起作用的原因。如果我们想要修改一个结构体,就需要传入一个结构体指针作为接收者: (注意这里依然是值拷贝,不过拷贝的是指针的值而不是整个结构体的值,通过指针就可以修改对应的结构体)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
运行代码你就会发现我们成功修改了 Animal 的 petName 成员了。一般如果必须需要修改结构体,或者结构体数据成员比较多(减少复制成本), 我们就需要使用指针接收者。如果你不好判断使用指针还是值接收者,推荐你使用指针接收者。(注意一般我们使用非常简短的名称给receiver命名)
Warning
注意代码里的 NOTE 注释,go 里提供了简化指针访问成员的方式,比如我们没有使用 (*a).petName
而是直接使用的
a.petName = petName
。这里并不是语法错误,而是 go 提供的一个好用的语法糖,让我们可以直接用这种方式访问成员。
构造函数如何实现?¶
上文我们是通过初始化一个 Animal 结构体的方式创建了一个 Animal "对象",go 里并没有像其他语言那样提供构造函数的方式来创建一个对象(是不是很无趣,对 go 就是这么吝啬)。 我们知道 go 里边创建一个空结构体的时候,不同类型的成员被赋值成其类型的『零值』,比如对于 Animal 里的 Name(string)是空字符串, Age(int) 是 0。
那如果我们想创建一个 Animal 的时候根据传入的参数来初始化呢?go 里边虽然没有直接提供构造函数,但是一般我们是通过定义一些 NewXXX 开头的函数来实现构造函数的功能的,比如我们可以定义一个 NewAnimal 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
这样我们就实现了一个类似构造函数的功能,当然你也可以根据不同的需求来定义多个构造函数。
组合 vs 继承¶
上文学习了如何定义 go 的 "对象",我们给 struct 加入了数据成员和方法,还实现了构造函数,看起来稍微有点面向对象编程的意思了。 OOP 中还有一个重要的概念就是继承,通过继承实现了 is-a 的类关系,可以很好地进行代码复用。但是 go 可能又要让你失望了,你会 发现 go 并不直接支持 struct 之间的继承。
那如果我们想实现类似继承的功能该怎么办呢?其实 go 也有类似的解决方案,不过 go 使用的不是继承而是组合,go 作者推崇的 思想是『组合优于继承』。go 提供了结构体的嵌入(embedding)用来实现代码复用,比如如果我们想定义一个 Dog 结构体,Dog 也是一个 Animal,我们想复用 Animal 里的成员,可以在 Dog struct 里嵌入一个 Animal:
1 2 3 4 5 6 7 8 9 10 11 |
|
你会发现在 Dog 里嵌入了 Animal 以后,我们就可以使用 Animal 的成员和方法了,从而实现了代码复用,是不是实现起来很简单。 我们还可以重写 Dog 自己的 Sleep 方法,来覆盖掉 Animal 的 Sleep 方法,给 Dog 增加一个方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
类似的,如果嵌入的 struct 里的成员名字和当前 struct 同名冲突了,go 会优先使用当前 struct 的成员。 到这里我们就大概学习了 go 使用 struct 来实现 OOP 的方式,可以看得出和常用的编程语言 Java/C++/Python 等还是有不少的区别的。 总得来说,go 的设计就是大道至简,没有其他语言那么多复杂的概念和语法糖,甚至让人感觉比较『简陋』。但是用多了你会发现,go 的这种设计精简并且够用,并且大大简化了代码的学习和上手成本。
多态¶
到这里我们还有一个 OOP 中重要的概念没有介绍,就是多态的概念。简单的说,多态就是同一个接口,对于不同的实例执行不同的操作。 下一章我们将介绍下 go 的接口(interface),以及如何在 go 中实现多态。
序列化¶
如果想要通过网络进行传输,我们就需要使用序列化协议,将 go 的结构体按照一种方式编码成字节串。 常见的序列化方式有 json/protobuf 等,如果你编写 web 服务的话对它们不会陌生。在 go 里是通过给 struct 添加 tag 来实现的。 我们这里简单提一下,在编写 web 应用的时候你会频繁使用到它,注意看 Animal 每个字段后边我们都加上了名为 json 的 tag。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
练习¶
- 之前我们学过 go 的 map,但是 go 里边没有直接提供一个 set,请你使用 struct 封装一个 Set,并且提供 Add/Delete/Exist 方法
- 通过使用嵌入实现一个 Cat struct,加入一个数据成员叫做 Height,并且给你的 Cat 加上 Eat 方法。