宣告本系列文章不停留在Go语言的语法层面,而是更关注语言特性、学习使用中的问题以及一些思考造成的。思考为什么会有结构的问题?首先我们要明确一点,面向对象的思想包括了各种独立的、相互的调用,需要一个承载的数据结构,那么这个结构是什么呢?显然,在GO语言中,它是一个结构体。其次,结构体作为一种数据结构,在C、C++、Go中都发挥了极其重要的作用。另外,Go语言中其实并没有一个明确的面向对象的名词。如果我们真的想涉足,我们可以将结构与其他语言中的类进行比较。至于为什么不用class,可能是作者想划清界限,区别于其他语言。毕竟,Go在面向对象实现方面是极其轻量级的。我们简单看一下结构体的声明:typePoemstruct{Titlestring//属性的声明,开头的大写表示属性的访问权限Authorstringintrostring}func(poem*Poem)publish(){//和其他语言不同的是,golang的声明方式和普通方式一样,只是在funcfmt.Println("poempublish")之后增加了一个类似poemPoem的声明}结构比较如果结构的所有成员都是comparable,则结构也可以进行比较,在这种情况下,可以使用==或!=运算符比较两个结构。相等比较运算符==将比较两个结构的每个成员,因此以下两个比较表达式是等价的:funcmain(){typePointstruct{X,Yint}p:=Point{1,2}q:=Point{2,1}fmt.Println(p.X==q.X&&p.Y==q.Y)//"false"fmt.Println(p==q)//"false"}可比结构体类型可用于地图的关键类型就像任何其他可比较的类型一样。funcmain(){typeaddressstruct{namestringageint}hits:=make(map[address]int)hits[address{"nosay",8}]++fmt.Println(hits[address{"nosay",8}])//1}使用结构体时的一个技巧在结构体传递的过程中,如果考虑效率,通常会通过指针传入和返回较大的结构体。而如果要在函数内部修改结构体的成员,则需要传入指针;因为在Go语言中,所有的函数参数都是通过值拷贝传入的(如果结构体很大,会重新分配空间,浪费资源),调用函数时函数参数将不再是原来的变量。什么是接口?Go语言中的接口是一组方法签名,是Go语言的重要组成部分。使用接口可以让我们更好地组织和编写易于测试的代码。但其实接口的本质就是引入了一个新的中间层。调用者可以通过接口与具体实现分离,实现上下游解耦。上层模块不再需要依赖下层具体模块,只需要依赖约定的接口即可。我们日常使用的sql不就是一个接口吗?比如下图:GO语言的接口是隐式的,一个duck模型体现的很清楚,那么duck模型是什么?“当你看到一只鸟,它走路像鸭子,游泳像鸭子,叫声像鸭子,那么这只鸟就可以称为鸭子。”当您实现接口的所有方法时,它会反映在接口上。你会认为你实现了接口,而不是像其他语言一样显式声明我实现了这个接口。例如,在下面的例子中,Dog实现了Pet接口:Go语言,我们只需要实现接口中定义的所有方法,我们默认这个类型实现了接口。接口Go语言的数据结构根据接口类型“是否包含一组方法”对类型进行不同的处理,即分为空接口和带方法的接口。我们使用iface结构来表示包含方法的接口;eface结构表示不包含任何方法的interface{}类型。接下来,我们来看看这两个数据结构。eface:typeefacestruct{//16bytes_type*_typedataunsafe.Pointer}由于interface{}类型不包含任何方法,其结构比较简单,只包含两个指向底层数据和类型的指针。从上面的结构我们也可以推断——Go语言中的任何类型都可以转换为interface{}类型。另一个用来表示接口的结构是iface,iface内部维护了两个指针,tab指向一个itab实体,表示接口的类型和分配给接口的实体类型。data指向接口的具体值,一般来说就是指向堆内存的指针。iface:typeifacestruct{//16bytestab*itabdataunsafe.Pointer}接下来我们看看type和tab里面是什么:type:type_typestruct{sizeuintptr//该字段存放的是类型占用的内存空间,为分配内存空间提供信息ptrdatauintptrhashuint32//该字段可以帮助我们快速判断类型是否相等tflagtflag//类型标志,与反射相关alignuint8//内存对齐相关fieldAlignuint8kinduint8//类型编号,包括bool、slice、struct等。equalfunc(unsafe.Pointer,unsafe.Pointer)bool//该字段用于判断当前类型的多个对象是否相等。该字段是为了减小Go语言二进制包的大小gcdata*bytemigratefromtypeAlgstructure//gcrelatedstrnameOffptrToThistypeOff}tab:typeitabstruct{inter*interfacetype//typeofinterface_type*_type//typeofentitylink*itabhashuint32//type.hash的副本,用于比较目标类型和接口类型badbool//类型没有实现接口inhashbool//这个itab是否被添加到hash中?unused[2]bytefun[1]uintptr//placementandinterface方法对应的具体数据类型的方法地址。第一个方法的函数指针存放在这里。如果有更多的方法,它会在它之后继续存储在内存空间中}typeinterfacetypestruct{typ_type//实体类型pkgpathname//包名mhdr[]imethod//函数列表}因为eface和iface在结构上有一定的共性,这里我们只看iface数据结构图,eface只是稍微改动一下而已:这里有问题。iface结构维护了接口类型和实体类型之间的对应关系。我们经常在代码中多次实现接口,那么如何保存呢?答案是只要代码中存在引用关系,go就会在运行时为这对具体的生成itab信息。值方法和指针方法的区别我们都知道,方法的接收者类型必须是某种用户定义的数据类型,不能是接口类型或接口指针类型。所谓取值方法,就是接收者类型是自定义数据类型,不是指针的方法。那么,值方法和指针方法体现在哪里呢?我们看下面的代码:func(cat*Cat)SetName(namestring){cat.name=name}SetName方法的接收者类型是*Cat。Cat左边加*表示Cat类型的指针类型。这时候Cat就可以称为*Cat的基本类型了。您可以将此指针类型的值视为表示指向某种原始类型值的指针。那么,这个SetName就是一个指针方法。那么什么是价值方法呢?通俗地说,把Cat前面的*去掉就是取值方法。指针方法和值方法有什么区别?请看下面。值方法的接收者是该方法所属类型的值的副本。我们在这个方法中对副本的修改一般不会反映到原始值中,除非类型本身是引用类型(如切片或字典)的别名类型。指针方法的接收者是该方法所属的基本类型值的指针值的副本。我们在这样的方法中修改了副本指向的值,但是肯定会反映在原值上。这部分可能有点绕,但是如果你之前理解过函数传递切片,这部分也能理解。简而言之,一份是整个数据结构,一份是指向数据结构的地址。自定义数据类型的方法集合只包括它所有的值方法,而该类型的指针类型的方法集合包括前者的所有方法,包括所有值方法和所有指针方法。严格来说,我们只能对这种基本类型的值调用它的取值方法。不过Go语言会适时自动帮我们翻译,这样我们也可以在这样的值上调用它的指针方法。例如,也可以调用如下:string){dog.Class=name}funcmain(){a:=Dog{"grape"}a.SetName("nosay")//a会先获取地址然后调用指针方法//Dog{"葡萄”}。SetName("nosay")//因为是值类型,所以调用失败,不能调用Dog字面量上的指针方法,不能取Dog字面量的地址(&Dog{"grape"}).SetName("nosay")//Yes}稍后你会了解到一个类型的方法集中的哪些方法与它可以实现哪些接口类型密切相关。如果一个基本类型和它的指针类型有不同的方法集,那么它们实现的接口类型的数量也会不同,除非这两个数字都是零。比如一个指针类型实现了某个接口类型,但是它的基本类型不一定是接口的实现类型。例如:typePetinterface{SetName(namestring)Name()stringCategory()string}typeDogstruct{namestring}func(dog*Dog)SetName(namestring){dog.name=name}func(dogDog)Name()string{returndog.name}func(dogDog)Category()string{return"dog"}funcmain(){dog:=Dog{"小猪"}_,ok:=interface{}(dog).(Pet)fmt.Printf("DogimplementsinterfacePet:%v\n",ok)//false_,ok=interface{}(&dog).(Pet)fmt.Printf("*Dogimplementsinterfacepet:%v\n",ok)fmt.Println()//true}有几种情况编译器会调用这些方法失败:value方法指针方法结构体初始化变量passornot指针初始化变量后面讲了基础知识的疑惑,我们来看看GO是如何实现面向对象的三轴(继承、封装、多态)的;首先我们要明确一个概念,Go语言中没有继承的概念,具体原因官网上说的很清楚(参见Whythereisnoinheritance?,简单来说,面向对象编程中的继承就是实际上是通过牺牲一定的代码简洁性来换取可扩展性,而这种可扩展性是通过侵入的方式实现的,Go不需要管理或讨论类型层次结构,因为类型和接口之间没有明确的关系。那么,我们通过下面的例子来看看Go是如何通过嵌入组合实现继承的:Println("animal")}typeCatstruct{//继承动物的属性和方法Animal//猫自身的属性ageint}//cat类的特有方法func(cCat)Sleep(){fmt.Println("Sleep")}funcmain(){//创建一个动物类animal:=Animal{name:"Animal",subject:"AnimalSection"}animal.Eat("Meat")//创建一个猫类cat:=Cat{Animal:Animal{name:"cat",subject:"cat"},age:1}cat.Eat("fish")//调用了Animal的Eat方法,“继承”的体现cat.Sleep()}封装Go语言在包级别进行封装。以小写字母开头的名称仅在该包内可见。您可以隐藏私有包中的所有内容,只公开某些类型、接口和工厂函数。例如,要隐藏上面的Foo类型,只暴露接口,可以将其重命名为小写的foo并提供一个返回公共Fooer接口的NewFoo()函数:typefoostruct{}func(ffoo)Foo1(){fmt.Println("这里是Foo1()")}func(ffoo)Foo2(){fmt.Println("这里是Foo2()")}func(ffoo)Foo3(){fmt.Println("Foo3()here")}funcNewFoo()Fooer{return&Foo{}}然后来自另一个包的代码可以使用NewFoo()并访问由内部foo类型实现的Footer接口,当然要记住包括包名:f:=NewFoo()f.Foo1()f.Foo2()f.Foo3()多态性多态性是面向对象编程的本质:只要对象坚持实现相同的接口,Go语言就可以处理那些不同类型的对象.Go接口以一种非常直接和直观的方式提供了这种能力。这是一个精心准备的示例,其中创建了Ihello接口的多个实现并将其存储在一个切片中,然后轮询以调用Hello方法。您会注意到不同实例化对象的样式。typeIHellointerface{Hello(namestring)}funcHello(helloIHello){hello.Hello("hello")}typePeoplestruct{Namestring}func(people*People)Hello(saystring){fmt.Printf("人是%v,say%v\n",people.Name,say)}typeManstruct{People}func(man*Man)Hello(saystring){fmt.Printf("people是%v,say%v\n",man.Name,say)}typeWomenstruct{People}func(women*Women)Hello(saystring){fmt.Printf("thepeopleis%v,say%v\n",women.Name,say)}funcEcho(hello[]IHello){for_,val:=rangehello{val.Hello("helloworld")}}funcmain(){hello1:=&People{"people"}hello2:=&Man{People{Name:"xiaoming"}}hello3:=&Women{People{Name:"xiaohong"}}sli:=[]IHello{hello1,hello2,hello3}//人就是人,sayhelloworld//人民是小明,向世界问好//人民是小红,向世界问好Echo(sli)}下期预告【Go语言踩坑系列(七)】Goroutine关注我们欢迎对本对系列文章感兴趣的读者订阅我们的公众号,关注博主,下次不要再迷路了~