面向对象编程风格深受广大开发者喜欢,尤为是以 C++, Java 为典型表明的编程语言大行其道,十分流行!
有意思的是这两中语言几乎毫无心外都来源于 C 语言,却不一样于 C 的面向过程编程,这种面向对象的编程风格给开发者带来了极大的便利性,解放了劳动,松耦合,高内聚也成为设计的标准,从而让咱们可以更加愉快地复制粘贴,作代码的搬运工,不少第三方工具开箱即用,语义明确,职责清晰,这都是面向对象编程的好处!
Go 语言也是来源于 C 语言,不知道你是否也会好奇 Go 语言是否支持面向对象这种编程风格呢?
准确的说,Go 既支持面向对象编程又不是面向对象语言!
是也不是,难道像是薛定谔的猫同样具备不肯定性?
其实这个答案是官方的回答,并非我我的凭空杜撰而来的,如需了解详情可参考 Is Go an object-oriented language?
go-oop-about-schrodinger-cat.png
为何这么说呢?
Go 支持封装,却不支持继承和多态,因此严格按照面向对象规范来讲, Go 语言不是面向对象的编程语言.
可是,Go 提供的接口是一种很是简单上手且更加通用的方式,虽然和其余主流的编程语言表现形式上略有不一样,甚至不能实现多态,但 Go 的接口不只仅适用于结构体,也能够适用于任何数据类型,这无疑是很灵活的!
争议性比较大的当属继承,因为没有任何关键字支持继承特性,所以是找不到继承的痕迹.虽然的确存在着某些方式能够将类型嵌入到其余类型中以实现子类化,但那却不是真正的继承.
因此说,Go 既支持面向对象的编程风格又不彻底是面向对象的编程语言.
若是换个角度看问题的话,正是因为没有继承特性使得Go 相对于面向对象编程语言更加轻量化,不妨想想继承的特性,子类和父类的关系,单继承仍是多继承,访问控制权限等问题吧!
go-oop-about-go-lovely.png
若是按照面向对象的编程规范,实现封装特性的那部分应该是类和对象,但这种概念与实现语言的关键字class 是密不可分的,然而 Go 并无 class 关键字而是 C 语言家族的 struct 关键字,因此叫作类或对象也不是十分贴切,因此下面的讲解过程仍是采用结构体吧!
如何定义结构体
stuct 关键字声明结构体,属性之间回车换行.
好比下面示例中定义了动态数组结构体,接下来的示例中都会以动态数组结构体做为演示对象.
type MyDynamicArray struct {
ptr *[]int
len int
cap int
}
Go 语言中定义对象的多属性时使用直接换行方式而不是分号来分隔?为何和其余主流的编程语言不呢?
对于习惯分号结尾的开发者可能一时并不习惯 Go 的这种语法,因而决定探索一下 Go 的编程规范!
go-oop-about-redundant-semicolon.png
若是手动添加分号的话,编辑器则会提示分号重复,因此猜测是多是Go编译器已经自动添加了分号,并将分号做为语句声明的分隔符,手动添加分号后,Go 无论不顾仍是添加了分号,因而就有了上述的报错.
这样作有什么好处呢?
本身添加分号和编译器无条件添加分号结果不都是同样的吗,更况且其余主流的编程语言都是手动添加分号的啊!
存在多个属性时直接换行而不是添加分号做为分隔符,对于从未接触过编程语言的小白来讲,可能会省事儿,可是对于已有编程经验的开发者来讲,却须要特别记住不能添加分号,这一点确实有些闹腾!
若是多个属性所有写在一行时,没有换行符我看你还怎么区分,此时用逗号分隔仍是用分号分隔呢?
go-oop-about-semicolon-or-new-line.png
首先空格确定是不能分隔多个属性的,所以尝试分号或者逗号是否能够.
根据提示说须要分号或者新的换行符,而换行符是标准形式,因此接下来试一下分号能不能分隔?
go-oop-about-semicolon-in-one-line.png
编辑器此时没有报错或警告信息,所以在一行上多个属性之间应该用分号分隔,也就是说 Go 编译器识别多个属性仍然是同其余主流的编程语言同样,使用分号分隔,而开发者却不能用!
go-oop-about-semicolon-fire-by-official.jpeg
相似于上述的规则记忆很简单,验证也比较容易,难点在于理解为何?
Go 为何会这么设计?或者说如何理解这种设计思路所表明的语义?
Go 做为一门新的编程语言,不只体如今具体的语法差别上,更重要的是编程思想的特殊性.
正如面向对象中的接口概念同样,设计者只须要定义抽象的行为并不用关心行为的具体实现.
若是咱们也采用这种思路来理解不一样的编程语言,那么就能透过现象看本质了,不然真的很容易陷入语法细节上,进而可能忽略了背后的核心思想.
其实关于结构体的多属性分隔符问题上,实际上不论采用什么做为分隔符都行,哪怕就是一个逗号,句号都行,只要能让编译器识别到这是不一样的属性就行.
因为大多数主流的编程语言通常采用分号做为分隔符,开发者须要手动编写分隔号以供编译器识别,而 Go 语言却不这么认为,算了吧,直接换行,我同样能够识别出来(尽管底层 Go 编译器进行编译时仍然是采用分号表示换行的)!
go-oop-about-semicolon-ninja.png
添加或者不添加分号,对于开发者而言,仅仅是一种分隔多个属性的标志而已,若是能不添加就能实现,那为何还要添加呢?
go-oop-about-semicolon-ok.png
是什么,为何和怎么样是三个基本问题,若是是简单学习了解的话,学会是什么和怎么样就已经足够了,可是这样一来学着学着不免会陷入各自为政的场面,也就是说各个编程语言之间没有任何关系,每一种语言都是独立存在的?!
世界语言千千万,编程语言也很多,学了新语言却没有利用旧语言,那学习新语言时和纯小白有何差别?
学到是学会了,惋惜却对旧语言没什么帮助并无加深旧语言的理解,只是单纯的学习一种全新的语言罢了.
语言是演变创造出来的,不是空中楼阁,是创建在已有体系下逐渐发展演变而来,任何新语言都能或多或少找到旧语言的影子.
因此何不尝试一下,弄清楚新语言设计的初衷和以及设计时所面临的问题,而后再看该语言是如何解决问题的,解决的过程称之为实现细节,我想这种方式应该是一种比较好的学习方式吧!
虽然没法身处语言设计时环境,也不必定明白语言设计时所面临的挑战,但先问尝试着问一下为何,不这么设计行不行诸如此类的问题,应该是一种不错的开端.
因此接下来的文章都会采用语义性分析的角度,尝试理解 Go语言背后的设计初衷,同时以大量的辅助性的测试验证猜测,再也不是简单的知识罗列整理过程,固然必要的知识概括仍是很重要的,这一点天然也不会放弃.
go-oop-about-go-cheer.png
如今动态数组已经定义完毕,也就是做为设计者的工做暂时告一段落,那做为使用者,如何使用咱们的动态数组呢?
按照面向对象的说法,由类创造出对象的过程叫作实例化,然而咱们已经知道 Go 并非彻底的面向对象语言,所以为了尽量避免用面向对象的专业术语去称呼 Go 的实现细节,咱们暂时能够将其理解为结构体类型和结构体变量的关系,之后随着学习的深刻,可能会对这部分有更加深入的认识.
func TestMyDynamicArray(t *testing.T){
var arr MyDynamicArray
// {<nil> 0 0}
t.Log(arr)
}
上述写法并无特殊强调过,彻底是用前几篇文章中已经介绍过的语法规则实现的,var arr MyDynamicArray 表示声明类型为 MyDynamicArray 的变量 arr ,此时直接打印该变量的值,获得的是 {<nil> 0 0}.
后两个值都是 0,天然很好理解,由于在讲解 Go 语言中的变量时咱们就已经介绍过,Go 的变量类型默认初始化都有相应的零值,int 类型的 len cap 属性天然就是 0,而 ptr *[]int 是指向数组的指针,因此是 nil.
等等,有点不对劲,这里有个设计错误,明明叫作动态数组结果内部倒是切片,这算怎么回事?
先修正这个错误再说,因而可知,一时粗心影响多么恶劣以致于语义都变了,容我先改正过来!
go-oop-about-myDynamicArray-array-size-with-cap.png
咱们知道要使用数组必须指定数组的初始化长度,第一感受是使用 cap 表示的容量来初始化 *[cap]int 数组,然而并不能够,编辑器提示说必须使用整型数字.
虽然 cap 是 int 类型的变量,但内部数组 [cap]int 并不能识别这种方式,多是由于这两个变量时一块声明的,cap 和 [cap]int 都是变量,没法分配.
那若是指定初始化长度应该指定多少呢,若是是 0 的话,语义上正确但和实际使用状况不符合,由于这样一来内部数组根据就没办法插入了!
go-oop-about-myDynamicArray-array-size-with-zero.png
因此数组的初始化长度不能为零,这样解决了没法操做数组的问题,但语义上又不正确了,所以这种状况下须要维护两个变量 len 和 cap 的值来确保语义和逻辑正确,其中 len 表示真正的数组个数,cap 表示内部数组实际分配的长度,因为这两个变量相当重要,不该该被调用者随意修改,最多只能查看变量的值,因此必须提供一种机制保护变量的值.
接下来,咱们尝试用函数封装的思路来完成这种需求,代码实现以下:
type MyDynamicArray struct {
ptr *[10]int
len int
cap int
}
func TestMyDynamicArray(t *testing.T){
var myDynamicArray MyDynamicArray
t.Log(myDynamicArray)
myDynamicArray.len = 0
myDynamicArray.cap = 10
var arr [10]int
myDynamicArray.ptr = &arr
t.Log(myDynamicArray)
t.Log(*myDynamicArray.ptr)
}
var myDynamicArray MyDynamicArray 声明结构体变量后并设置告终构体的基本属性,而后操做了内部数组,实现了数组的访问修改.
go-oop-about-myDynamicArray-array-size-with-init.png
然而,咱们犯了一个典型的错误,调用者不该该关注实现细节,这不是一个封装该干的事!
具体实现细节应该由设计者完成,将有关数据封装成一个总体对外提供相应的接口,这样调用者才能安全方便地调用.
第一步,先将与内部数组相关的两个变量进行封装,对外仅提供访问接口不提供设置接口,防止调用者随意修改.
很显然这部分应该是函数来实现,因而乎有了下面的改造过程.
go-oop-about-myDynamicArray-method-inner.png
很遗憾,编辑器直接报错: 必须是类型名称或是指向类型名称的指针.
函数不能够放在结构体内,这一点却是像极了 C 家族,可是 Java 这种衍生家族会以为难以想象,无论怎么说,这意味着结构体只能定义结构而不能定义行为!
那咱们就把函数移动到结构体外部吧,但是咱们定义的函数名叫作 len,而系统也有 len 函数,此时可否正常运行呢?让咱们拭目以待,眼见为实.
go-oop-about-myDynamicArray-method-len.png
除了函数自己报错外,函数内部的 len 也报错了,是由于此时的函数和结构体还没有创建起任何联系,怎么可能访问到 len 属性呢,不报错才怪呢!
解决这个问题很简单,直接将结构体的指针传递给 len 函数不就行了,这样一来函数内部就能够访问到结构体的属性了.
go-oop-about-myDynamicArray-method-len-with-args.png
从设计的角度上来说,确实解决了函数定义的问题,可是使用者调用函数时的使用方法看起来和面向对象的写法有些不同.
func TestMyDynamicArray(t *testing.T) {
var myDynamicArray MyDynamicArray
t.Log(myDynamicArray)
myDynamicArray.len = 0
myDynamicArray.cap = 10
var arr [www.jintianxuesha.com ]int
myDynamicArray.ptr = &arr
t.Log(myDynamicArray)
t.Log(*myDynamicArray.ptr)
(*myDynamicArray.ptr)[0] = 1
t.Log(*myDynamicArray.ptr)
t.Log(len(&myDynamicArray))
}
面向对象的方法中通常都是经过点操做符 . 访问属性或方法的,而咱们实现的属性访问是 . 但方法倒是典型的函数调用形式?这看起来明显不像是方法嘛!
为了让普通函数看起来像是面向对象中的方法,Go 作了下面的改变,经过将当前结构体的变量声明移动到函数名前面,从而实现相似于面向对象语言中的 this 或 self 的效果.
func len(myArr *MyDynamicArray) int {
return myArr.len
}
go-oop-about-myDynamicArray-method-len-ahead-name.png
此时方法名和参数返回值又报错了,根据提示说函数名和字段名不能相同?
真的又是一件神奇的事情,难不成 Go 没法区分函数和字段?这就不得而知了.
那咱们只好修改函数名,改为面向对象中喜闻乐见的方法命名规则,以下:
func (myArr *MyDynamicArray) GetLen() int {
return myArr.len
}
简单说一下 Go 的访问性规则,大写字母开头的表示公开的 public 权限,小写字母开头表示私有的 private 权限,Go 只有这两类权限,都是针对包 package 而言,之后会再细说,如今先这么理解就好了.
按照实验获得的方法规则,继续完善其余方法,补充 GetCap 和 IsEmpty 等方法.
如今咱们已经解决了私有变量的访问性问题,对于初始化的逻辑尚未处理,通常来讲,初始化逻辑能够放到构造函数中执行,那 Go 是否支持构造函数呢,以及怎么才能触发构造函数?
go-oop-about-myDynamicArray-constuct.png
尝试按照其余主流的编程语言中构造函数的写法来编写 Go 的构造函数 , 没想到 Go 编译器直接报错了,提示从新定义了 MyDynamicArray 类型,以致于影响了其他部分!
若是修改方法名称的话,理论上能够解决报错问题,可是这并非构造函数的样子了,难不成 Go 不支持构造函数吗?
此时,面向对象形式的构造函数转变成自定义函数实现的构造函数,更加准确的说法,这是一种相似于工厂模式实现的构造函数方式.
func NewMyDynamicArray() *MyDynamicArray {
var myDynamicArray MyDynamicArray
return &myDynamicArray
}
难道 Go 语言真的不支持构造函数?
至因而否支持构造函数或者说应该如何支持构造函数,真相不得而知,随着学习的深刻,相信之后必定会有明确的答案,这里简单表达一下我的见解.
首先咱们知道 Go 的结构体中只能定义数据,而结构体的方法确定是在结构体外定义的,为了符合面向对象的使用习惯,也就是经过实例对象的点操做符来访问方法,Go 的方法只能是函数的变体,即普通函数中关于指向结构体变量的声明部分转移到函数名前面来实现方法,这种由函数转变成为方法的模式也符合 Go 一向的命名规则: 向来是按照人的思惟习惯命名,先有输入再有输出等逻辑.
结构体的方法从语法和语义的两个维度上支持了面向对象规范,那么构造函数想要实现面向对象应该如何作呢?
构造函数正如其名应该是函数,而不是方法,方法由指向自身的参数,这一点构造函数不该该有,不然都有实例对象了还构造毛线啊?
既然构造函数是普通函数,那么按照面向对象的命名习惯,方法名应该是结构体名,然而真的操做了,编辑器直接就报错了,因此这不符合面向对象的命名习惯!
如此一来,构造函数的名称可能并非结构体类型的名称,有多是其余特殊的名称,最好这个名称可以见名知义且具有实例化对象时自动调用的能力.
固然这个名称依赖于 Go 的设计者如何命名,这里靠猜想是很难猜对的,不然我就是设计者了啊!
除此以外,还有另一种可能,那就是 Go 并无构造函数,想要实现构造函数的逻辑只能另辟蹊径.
这么说有没有什么靠谱的依据呢?
我想大概是有的,构造函数虽然提供了自动初始化能力,可是若是真的在构造函数中加入复杂的初始化逻辑,无疑会增大之后出错的排查难度并给使用者带来必定的阅读障碍,因此说必定程度上,构造函数颇有可能被滥用了!
那是否就意味着不须要构造函数了呢?
也不能这么说,构造函数除了基本的变量初始化以及简单的逻辑外,在实际编程中仍是有必定用途的,为了不滥用而直接禁用,多少有点饮鸩止渴的感受吧?
所以,我的的见解是应该能够保留构造函数这种初始化逻辑,也能够换一种思路去实现,或者干脆直接放弃构造函数转而由编译器自动实现构造函数,正如编译器能够自动添加多字段之间的分号那样.
若是开发者真的有构造函数的需求,经过工厂模式或者单例模式等手段老是能够定制结构体初始化的逻辑,因此放弃也何尝不可!
最后,以上这些纯属我的猜测,目前并不知道 Go 是否存在构造函数,有了解的人,还请明确告诉我答案,我的倾向于不存在构造函数,最多只提供相似于构造函数初始化的逻辑!
如今,咱们已经封装告终构体的数据,定义告终构体的方法以及实现告终构体的工厂函数.那么接下来让咱们继续完善动态数组,实现数组的基本操做.
func NewMyDynamicArray() *MyDynamicArray {
var myDynamicArray MyDynamicArray
myDynamicArray.len = 0
myDynamicArray.cap = 10
var arr [10]int
myDynamicArray.ptr = &arr
return &myDynamicArray
}
func TestMyDynamicArray(t *testing.T) {
myDynamicArray := NewMyDynamicArray()
t.Log(myDynamicArray)
}
首先将测试用例中的逻辑提取到工厂函数中,默认无参的工厂函数初始化的内部数组长度为 10 ,后续再考虑调用者指定以及实现动态数组等功能,暂时先实现最基本的功能.
初始化的内部数组均是零值,所以须要首先提供给外界可以添加的接口,实现以下:
func (myArr *MyDynamicArray) Add(index, value int) {
if myArr.len =www.fengshen157.com= myArr.cap {
return
}
if index < 0 || index > myArr.len {
return
}
for i := myArr.len - 1; i >= index; i-- {
(*myArr.ptr)[www.chaoyul.com] = (*myArr.ptr)[i]
}
(*myArr.ptr)[index] = value
myArr.len++
}
因为默认的初始化工厂函数暂时是固定长度的数组,所以新增元素实际上是操做固定长度的数组,不过这并不妨碍后续实现动态数组部分.
为了操做方便,再提供插入头部和插入尾部两种接口,能够基于动态数组实现比较高级的数据结构.
func (myArr *MyDynamicArray) AddLast(value int) {
myArr.Add(myArr.len, value)
}
func (myArr *MyDynamicArray) AddFirst(value int) {
myArr.Add(0, value)
}
为了方便测试动态数组的算法是否正确,所以提供打印方法查看数组结构.
go-oop-about-myDynamicArray-print.png
因而可知,打印方法显示的数据结构和真实的结构体数据是同样的,接下来咱们就比较有信心继续封装动态数组了!
func (myArr *MyDynamicArray) Set(index, value int) {
if index < 0 || index >= myArr.len {
return
}
(*myArr.ptr)[www.yacuangpt.com] = value
}
func (myArr *MyDynamicArray) Get(index int) int {
if index < 0 || index >= myArr.len {
return -1
}
return (*myArr.ptr)[index]
}
这两个接口更加简单,更新数组指定索引的元素以及根据索引查询数组的值.
接下来让咱们开始测试一下动态数组的所有接口吧!
go-oop-about-myDynamicArray-test.png
动态数组暂时告一段落,不知道你是否好奇为何以动态数组为例讲解面向对象?
其实主要是为了验证上一篇文章中的猜测,也就是切片和数组的究竟是什么关系?
我以为切片的底层是数组,只不过语法层面提供了支持以致于看不出数组的影子,仙子阿既然学习了面向对象,那么就用面向对象的方式实现下切片的功能,虽然没法模拟语法级别的实现,可是功能特性彻底是能够模仿的啊!
下面仍是梳理总结一下本文的只要知识点吧,也就是封装的实现.
如何封装结构体
之因此称之为结构体是由于 Go 的关键字是 struct 而不是 class,也是面向对象编程风格中惟一支持的特性,继承和多态都不支持,到时候另开文章细说.
结构体是对数据进行封装所使用的手段,结构体内只能定义数据而不能定义方法,这些数据有时候被称为字段,有时候叫作属性或者干脆叫作变量,至于什么叫法不是特别重要,如何命名和所处的环境语义有关.
type MyDynamicArray struct {
ptr *[www.yongshenyuL.com]int
len int
cap int
}
这种结构体内就有三个变量,变量之间直接换行进行分隔而不是分号并换行的形式,刚开始以为有些怪,不过编辑器通常都很智能,假如习惯性添加了分号,会提示你进行删除,因此语法细节上没必要在乎.
结构体内不支持编写函数,仅支持数据结构,这样就意味着数据和行为是分开的,二者之间的关联是比较弱的.
func (myArr *MyDynamicArray) IsEmpty(www.51kunlunyule.com) bool {
return myArr.len =www.zhuyngyule.cn= 0
}
这种方式的函数和普通函数略有不一样,将包含结构体变量的参数提早到函数名前面,语义上也比较明确,表示的是结构体的函数,为了和普通函数有所区别,这种函数称之为方法.
其实,单纯地就实现功能上看,方法和函数并无什么不一样,无外乎调用者的使用方式不同罢了!
func IsEmpty(myArr *MyDynamicArray) bool {
return myArr.len == 0
}
之因此是这种设计方式,一方面体现了函数的重要性,毕竟是 Go 语言中的一等公民嘛!
另外一方面是为了实现面向对象的语法习惯,不论属性仍是方法,通通用点 . 操做符进行调用.
官方的文档中将这种结构体参数称之为接收者,由于数据和行为是弱关联的,这里的接收者充当的就是关联数据的做用,接收者顾名思义就是接受数据的人,那发送数据的人又是谁呢?
不言而喻,发送者应该是调用者传递的结构体实例对象,结构体变量将数据结构发送给接收者方法,从而数据和行为联系在一块儿了.
func TestMyDynamicArray(www.51dfyLgw.com *testing.T) {
myDynamicArray := NewMyDynamicArray()
fmt.Println(myDynamicArray.IsEmpty())
}
好了,以上就是面向对象初体验中的所有部分,仅仅是这么一小部分却耗费我整整三天的时间了,想说换种思惟不简单,写好一篇文章也不容易啊!
下篇文章中将继续介绍面向对象的封装特性,讲解更多干货,若是以为本文对你有所帮助,欢迎转发评论,感受你的阅读!算法