1 封装

Java 中封装是基于类(Class),Golang 中封装是基于结构体(struct)

Golang 的开发中经常直接将成员变量设置为大写使用,当然这样使用并不符合面向对象封装的思想。

Golang 没有构造函数,但有一些约定俗成的方式:

  1. 提供 NewStruct(s Struct) *Struct 这样的函数
  2. 提供 (s *Struct) New() 这样的方法
  3. 也可以直接用传统的 new(struct) 或 Struct{} 来初始化,随后用 Set 方法对成员变量赋值
type People struct {
name string
age int
} func (p *People) SetName(name string) {
p.name = name
} func (p *People) GetName() string {
return p.name
} func (p *People) SetAge(age int) {
p.age = age
} func (p *People) GetAge() int {
return p.age
} func main() {
peo := new(People)
peo.SetName("张三")
peo.SetAge(13)
fmt.Println(peo.GetName(), peo.GetAge())
// 张三 13
}

2 继承 or 组合

Golang 不支持继承,支持组合,算是“组合优于继承”思想的体现。

虽然不支持继承,但 Golang 的匿名组合组合可以实现面向对象继承的特性。

2.1 非匿名组合和匿名组合

组合分为非匿名组合和匿名组合

非匿名组合不能直接使用内嵌结构体的方法,需要通过内嵌结构体的变量名,间接调用内嵌结构体的方法。

匿名组合可以直接使用内嵌结构体的方法,如果有多个内嵌结构体,可以直接使用所有内嵌结构体的方法。

非匿名组合实例:

type People struct {
name string
age int
} func (p *People) SetName(name string) {
p.name = name
} func (p *People) GetName() string {
return p.name
} func (p *People) SetAge(age int) {
p.age = age
} func (p *People) GetAge() int {
return p.age
} type Student struct {
people People
grade string
} func (s *Student) SetGrade(grade string) {
s.grade = grade
} func (s *Student) GetGrade() string {
return s.grade
} func main() {
stu := Student{}
stu.people.SetName("张三")
stu.people.SetAge(13)
stu.SetGrade("七年级")
fmt.Println(stu.people.GetName(), stu.people.GetAge(), stu.GetGrade())
// 张三 13 七年级
}

非匿名组合主要体现在 Student 结构体中对 People 的组合需要明确命名。

匿名组合实例:

type People struct {
name string
age int
} func (p *People) SetName(name string) {
p.name = name
} func (p *People) GetName() string {
return p.name
} func (p *People) SetAge(age int) {
p.age = age
} func (p *People) GetAge() int {
return p.age
} type Student struct {
People
grade string
} func (s *Student) SetGrade(grade string) {
s.grade = grade
} func (s *Student) GetGrade() string {
return s.grade
} func (s *Student) GetName() string {
return fmt.Sprintf("student-%s",s.People.GetName())
} func main() {
stu := Student{}
stu.SetName("张三")
stu.SetAge(13)
stu.SetGrade("七年级")
fmt.Println(stu.GetName(), stu.GetAge(), stu.GetGrade())
// student-张三 13 七年级
}

从上面实例可以看出,匿名组合中:

  1. Student 中的 People 没有显式命名
  2. Student 可以直接使用 People 的方法
  3. 注意 Student 的 GetName 方法,与 People 中的 GetName 方法重复,可以看做是面相对象编程中的重写(Overriding)
  4. Student 的 GetName 方法中使用 s.People.GetName() 调用People 中的 GetName 方法,这是对匿名组合的显式调用,类似 Java 中的 super 用法

可以看出,匿名组合的使用感官上类似面向对象编程的继承,可以说是一种『伪继承』的实现,但匿名组合并不是继承!

2.2 组合的使用方式

2.2.1 结构体中内嵌结构体

上面实例所用的就是结构体中内嵌结构体,不再多说。

2.2.2 结构体中内嵌接口

内嵌接口如下面例子:

type Member interface {
SayHello() // 问候语
DoWork() // 开始工作
SitDown() // 坐下
} type Normal struct{} func (n *Normal) SayHello() {
fmt.Println("normal:", "大家好!")
} func (n *Normal) DoWork() {
fmt.Println("normal:", "记笔记")
} func (n *Normal) SitDown() {
fmt.Println("normal:", "坐下")
} type People struct {
name string
age int
} func (p *People) SetName(name string) {
p.name = name
} func (p *People) GetName() string {
return p.name
} func (p *People) SetAge(age int) {
p.age = age
} func (p *People) GetAge() int {
return p.age
} type Student struct {
Member
People
grade string
} func (s *Student) SetGrade(grade string) {
s.grade = grade
} func (s *Student) GetGrade() string {
return s.grade
} func (s *Student) SayHello() {
fmt.Println("student:", s.name, "说: 老师好!")
} type Teacher struct {
Member
People
subject string
} func (t *Teacher) SetSubject(subject string) {
t.subject = subject
} func (t *Teacher) GetSubject() string {
return t.subject
} func (t *Teacher) SayHello() {
fmt.Println("teacher", t.name, "说: 同学们好!")
} func (t *Teacher) DoWork() {
fmt.Println("teacher", t.name, "讲课!")
} func main() {
stu := &Student{Member: &Normal{}}
stu.SetName("张三")
stu.SetAge(13)
stu.SetGrade("七年级") tea := &Teacher{Member: &Normal{}}
tea.SetName("李四")
tea.SetAge(31)
tea.SetSubject("语文") var member Member
member = stu
member.SayHello()
member.SitDown()
member.DoWork()
// student: 张三 说: 老师好!
// normal: 坐下
// normal: 记笔记 member = tea
member.SayHello()
member.SitDown()
member.DoWork()
// teacher 李四 说: 同学们好!
// normal: 坐下
// teacher 李四 讲课!
}

从上面例子可以看出:

  1. Teacher 和 Student 结构体都没有完全实现 Member 的方法
  2. Normal 结构体实现了 Member 方法
  3. Teacher 和 Student 初始化时注入了 Normal 结构
  4. Teacher 和 Student 可以作为 Member 类型的接口使用,并默认使用 Normal 的实现。除非 Teacher 和 Student 有自己的实现。

Teacher 和 Student 并非没有实现 Member 接口。编译器自动为类型 *Teacher 和 *Student 实现了 Member 中定义的方法,类似:

func (t *Teacher) SitDown() {
t.Member.SitDown()
}

所以即使在初始化时没有指定 Normal,Teacher 和 Student 也可以赋值给 Member 类型的变量。但调用未实现的 Member 的方法时会报 panic

官方实践

可以参考 sort.Reverse 方法,reverse 结构体内嵌了 Interface 接口,并只实现了 Less 方法

type IntSlice []int

func (x IntSlice) Len() int           { return len(x) }
func (x IntSlice) Less(i, j int) bool { return x[i] < x[j] }
func (x IntSlice) Swap(i, j int) { x[i], x[j] = x[j], x[i] } type Interface interface {
// 长度
Len() int
// 对比两个数 i,j,返回结果作为排序的依据
Less(i, j int) bool
// 交换两个数 i,j
Swap(i, j int)
} type reverse struct {
Interface
} func (r reverse) Less(i, j int) bool {
return r.Interface.Less(j, i)
} func Reverse(data Interface) Interface {
return &reverse{data}
}

使用时:

lst := []int{4, 5, 2, 8, 1, 9, 3}
sort.Sort(sort.Reverse(sort.IntSlice(lst)))
fmt.Println(lst)
// 打印:[9 8 5 4 3 2 1]

可以看出,Reverse 就是用内嵌接口的方式,接收一个 Interface 接口,将 Less 方法的两个参数反转了。

2.2.3 接口中内嵌接口

是针对方法的组合。如 Golang 的 ReadWriter 接口就是 Reader 和 Writer 接口的组合。

type Reader interface {
Read(p []byte) (n int, err error)
} type Writer interface {
Write(p []byte) (n int, err error)
} // ReadWriter is the interface that groups the basic Read and Write methods.
type ReadWriter interface {
Reader
Writer
}

3 多态

多态的定义比较宽松:指一个行为具有多种不同的表现形式。

本质上多态分两种:编译时多态(静态)和运行时多态(动态)

  1. 编译时多态在编译期间,多态就已经确定。重载是编译时多态的一个例子。
  2. 运行时多态在编译时不确定调用哪个具体方法,一直延迟到运行时才能确定。

通常情况下,我们讨论的多态都是运行时多态。Golang 接口就是基于动态绑定实现的多态。

由于 Golang 结构体是『组合』而非『继承』,不能相互转换,所以只有基于接口的多态。

3.1 向上转型

向上转型是实现多态的必要条件。即用父类的引用指向一个子类对象,通过父类引用调用方法时会调用子类的方法。通过父类引用无法调用子类的特有方法,需要『向下转型』。

type Member interface {
SayHello() // 问候语
DoWork() // 开始工作
SitDown() // 坐下
} type People struct {
name string
age int
} func (p *People) SetName(name string) {
p.name = name
} func (p *People) GetName() string {
return p.name
} func (p *People) SetAge(age int) {
p.age = age
} func (p *People) GetAge() int {
return p.age
} type Student struct {
People
grade string
} func (s *Student) SetGrade(grade string) {
s.grade = grade
} func (s *Student) GetGrade() string {
return s.grade
} func (s *Student) SayHello() {
fmt.Println("student:", s.name, "说: 老师好!")
} func (s *Student) DoWork() {
fmt.Println("student:", s.name, "记笔记")
} func (s *Student) SitDown() {
fmt.Println("student:", s.name, "坐下")
} type Teacher struct {
People
subject string
} func (t *Teacher) SetSubject(subject string) {
t.subject = subject
} func (t *Teacher) GetSubject() string {
return t.subject
} func (t *Teacher) SayHello() {
fmt.Println("teacher", t.name, "说: 同学们好!")
} func (t *Teacher) DoWork() {
fmt.Println("teacher", t.name, "讲课!")
} func (t *Teacher) SitDown() {
fmt.Println("teacher:", t.name, "站着讲课,不能坐下!")
} func main() {
stu := &Student{}
stu.SetName("张三")
stu.SetAge(13)
stu.SetGrade("七年级") tea := &Teacher{}
tea.SetName("李四")
tea.SetAge(31)
tea.SetSubject("语文") var member Member
member = stu
member.SayHello()
member.SitDown()
member.DoWork()
// student: 张三 说: 老师好!
// student: 张三 坐下
// student: 张三 记笔记 member = tea
member.SayHello()
member.SitDown()
member.DoWork()
// teacher 李四 说: 同学们好!
// teacher: 李四 站着讲课,不能坐下!
// teacher 李四 讲课!
}

这里是基于接口实现的『向下转型』,没有父类、子类之分。而在 Java 中,当子类没有重写父类方法时,父类的引用会调用到父类的方法里。其实也有类似的实现,在上面已经有了,即2.2.2 结构体中内嵌接口

3.2 向下转型

上面提到,用父类的引用指向一个子类对象,通过父类引用无法调用子类的特有方法。但在某些情况下需要调用子类的特有方法,例如子类有一些特殊逻辑需要处理,这时就需要『向下转型』还原出子类的引用。

在 Java 里,通常用 User user = (User) people; 来向下转型。

Golang 向下转型通过类型断言。基本用法是t,ok := intefaceValue.(T)。

一个例子:

type Member interface {
SayHello() // 问候语
} type People struct {
name string
age int
} func (p *People) SetName(name string) {
p.name = name
} func (p *People) GetName() string {
return p.name
} func (p *People) SetAge(age int) {
p.age = age
} func (p *People) GetAge() int {
return p.age
} type Student struct {
People
grade string
} func (s *Student) SetGrade(grade string) {
s.grade = grade
} func (s *Student) GetGrade() string {
return s.grade
} func (s *Student) SayHello() {
fmt.Println("student:", s.name, "说: 老师好!")
} func main() {
stu := &Student{}
stu.SetName("张三")
stu.SetAge(13)
stu.SetGrade("七年级") var member Member = stu
member.SayHello()
// student: 张三 说: 老师好!
if student, ok := member.(*Student); ok {
student.SetName("王五")
student.SayHello()
// student: 王五 说: 老师好!
}
}
  1. 使用 Member 类型的引用,指向一个 Student 对象
  2. 想对 Student 对象设置一个新名称,使用类型断言向下转型,可以调用 Student 的 SetName 方法(严格的说,是 People 的 SetName 方法)

除了上面场景,还有一种场景需要类型断言:当有一个函数,其参数是 interface{} 类型时:

func SayHello(inter interface{}) {
if member, ok := inter.(Member); ok {
member.SayHello()
}
} func main() {
stu := &Student{}
stu.SetName("张三")
stu.SetAge(13)
stu.SetGrade("七年级") SayHello(stu)
// student: 张三 说: 老师好!
}

补充:除了类型断言,Golang 还有一种 type-switch 的方式可以用于对 interface 的类型探测,从而进行不同逻辑的处理。

4 参考资料

  1. Golang 中 struct 嵌入 interface
  2. 面向对象思考与 golang cobra 库实现原理
  3. 多态中的向上转型与向下转型

Golang 面向对象深入理解的更多相关文章

  1. Golang面向对象编程-struct(结构体)

    Golang面向对象编程-struct(结构体) 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.什么是面向对象编程 面向对象编程(Object Oriented Program ...

  2. javascript面向对象的理解(一)

    第一次在园子发文: 关于js面向对象的理解: 工厂方式是什么?构造函数是什么?原形链?对象的引用? 1.对象是什么? 在js接触的比较多的就是对象了,比如: var arr = []; arr.num ...

  3. 对面向对象的理解—— SAP电面(1)

    对于C++面向对象的理解 面向对象是在结构化设计方法出现很多问题的情况下应运而生的.结构化设计方法求解问题的基本策略是从功能的角度审视问题域.它将应用程序看成实现某些特定任务的功能模块,其中子过程是实 ...

  4. JavaScript面向对象的理解

    JavaScript面向对象的理解  笔记链接: http://pan.baidu.com/s/1c0hivuS 1:JavaScript 中分两种对象,函数对象和普通对象new Function() ...

  5. golang面向对象和interface接口

    一. golang面向对象介绍 1.golang也支持面向对象编程,但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言.2.golang没有类(class),golang语言的结合体(struc ...

  6. golang 面向对象编程

    概述 Golang语言的面向对象与c++,py等语言有所不同,是由于Golang不支持继承:与上述支持聚合和继承的面向对象的语言不同,Golang只支持聚合(也叫做组合)和嵌入.聚合和嵌入的区别: t ...

  7. 谈个人对avascript面向对象的理解

    javascript,不但是javascript或者是别的语音,大多数都有一句经典的话:一切皆对象. 下面谈谈我个人对面向对象的理解,为什么要用面向对象来写js,这话我思考了很久,最后得出的结论就是: ...

  8. javascript javascript面向对象的理解及简单的示例

    javascript面向对象的理解及简单的示例 零.本节重点: 1.封装: 2.继承: 壹.下面理解: 一. javascript面向对象概念: 为了说明 JavaScript 是一门彻底的面向对象的 ...

  9. go---weichart个人对Golang中并发理解

    个人觉得goroutine是Go并行设计的核心,goroutine是协程,但比线程占用更少.golang对并发的处理采用了协程的技术.golang的goroutine就是协程的实现. 十几个gorou ...

  10. js面向对象怎么理解

    js面向对象怎么理解 <一>. 认识对象.首先要认识对象:在编程中,对象(object)是具体的某一个实例,唯一的某一个个体.如:电脑就是一个统称,而你面前的这一台电脑就是对象.而电脑的统 ...

随机推荐

  1. MiniNK WEB 选拔题 by F12

    Start 除了梦想外一无所有的我们,将会和蔑视与困境做最后的斗争,这是最后一舞 N0wayBack 联合战队成立以来一直致力于信息安全技术的研究,作为联合战队活跃在各大 CTF (信息安全竞赛)赛事 ...

  2. 【转载】Linux虚拟化KVM-Qemu分析(十一)之virtqueue

    转载自: 作者:LoyenWang 出处:https://www.cnblogs.com/LoyenWang/ 公众号:LoyenWang 版权:本文版权归作者和博客园共有 转载:欢迎转载,但未经作者 ...

  3. Kubernetes亲和性学习笔记

    欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 本篇概览 本文是欣宸在学习Kubernetes调度器的 ...

  4. Django message组件

    使用message组件要在seetings中配置 ①INSTALLED_APPS   (项目需要什么功能都放在这 既可以有数据库,又可以写代码,html文件,和自己写的APP一个道理) ②MIDDLE ...

  5. 2022-1-10 WPF基础知识点温习

    一.window Icon :允许自定义窗口图标 ResizeMode:调整控制大小,默认为CanResize. CanMinimize允许用户最小化窗口. NoResize没有格式.  ShowIn ...

  6. [python]为指定目录下的文件名批量加前缀

    前言 功能描述:批量重命名指定目录下的文件,文件名加前缀,默认格式为"目录名_原文件名". 示例代码 import argparse import os import sys im ...

  7. IE浏览器不支持TextDecoder()的问题

    IE浏览器不支持TextDecoder()方法,因此在进行Arrbuffer转string或中文时,出现未定义的错误.通过网上查找方法,可以通过引用第三方库进行解决. github地址:https:/ ...

  8. WPF如何构建MVVM+模块化的桌面应用

    为何模块化 模块化是一种分治思想,不仅可以分离复杂的业务逻辑,还可以进行不同任务的分工.模块与模块之间相互独立,从而构建一种松耦合的应用程序,便于开发和维护. 开发技术 .Net 6 + WPF + ...

  9. aspnetcore 注册中心 consul

    consul启动 . http://192.168.1.6:8500/ #以server方式启动,UI可以访问,boot引导自己选为leader,网段内PC可访问 consul agent -serv ...

  10. Laf & 中大猫谱:让每一只流浪猫都有家

    猫谱简介 中大猫谱是一款辅助校园流浪猫救助的开源小程序项目,服务端使用 Laf 云开发. 猫谱主要功能包括:猫咪信息登记.照片分享.拍照识猫.公告和留言等.项目创立的初衷,是解决校园猫猫交流群里的一个 ...