本系列文章并不会停留在Go语言的语法层面,更关注语言特性、学习和使用中出现的问题以及引发的一些思考。golang
首先,咱们须要明确面向对象的思想是包含各类独立而又互相调用,这就须要一个承载的数据结构,那么这个结构是什么呢?很显然,在GO语言中就是结构体。
其次,结构体做为一种数据结构,不管是在C仍是C++仍是Go都发挥了极其重要的做用。另外,在Go语言中其实并无明确的面向对象的说法,实在要扯上的话,咱们能够将struct比做其它语言中的class。至于为何不用class,多是做者想要划清和其余语言不一样的界限,毕竟Go在面向对象实现这方面是极其轻量的。咱们简单看一下结构体的声明:sql
type Poem struct { Title string //声明属性,开头大小写表示属性的访问权限 Author string intro string } func (poem *Poem) publish() { //和其它语言不同,golang声明方法和普通方法一致,只是在func后增长了poem Poem这样的声明 fmt.Println("poem publish") }
若是结构体的所有成员都是能够比较的,那么结构体也是能够比较的,那样的话两个结构体将可使用==或!=运算符进行比较。相等比较运算符==将比较两个结构体的每一个成员,所以下面两个比较的表达式是等价的:编程
func main() { type Point struct{ X, Y int } p := Point{1, 2} q := Point{2, 1} fmt.Println(p.X == q.X && p.Y == q.Y) // "false" fmt.Println(p == q) // "false" }
可比较的结构体类型和其余可比较的类型同样,能够用于map的key类型。segmentfault
func main() { type address struct { name string age int } hits := make(map[address]int) hits[address{"nosay", 8}]++ fmt.Println(hits[address{"nosay", 8}]) // 1 }
在结构体传递过程当中,若是考虑效率的话,较大的结构体一般会用指针的方式传入和返回。并且若是要在函数内部修改结构体成员的话,用指针传入是必须的;由于在Go语言中,全部的函数参数都是值拷贝传入的(结构体较大的话会从新分配空间,浪费资源),函数参数将再也不是函数调用时的原始变量。数据结构
Go 语言中的接口就是一组方法的签名,它是 Go 语言的重要组成部分。使用接口可以让咱们更好地组织并写出易于测试的代码。但其实接口的本质就是引入一个新的中间层,调用方能够经过接口与具体实现分离,解除上下游的耦合,上层的模块再也不须要依赖下层的具体模块,只须要依赖一个约定好的接口。咱们平常使用的sql又未尝不是一个接口呢?例以下图:函数
GO语言接口是隐式的,一种鸭子模型很明确的体现,那么鸭子模型是什么?“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就能够被称为鸭子。”在接口上体现就是当你实现了接口的全部方法的时候就会认为你实现了接口,而不用像其余语言同样去显示声明我实现了这个接口。例以下边这个例子Dog就实现了Pet接口:学习
type Pet interface { SetName(name string) } type Dog struct { Class string } func (dog *Dog) SetName(name string) { dog.Class = name }
在Go语言中,只须要实现全部接口中定义的方法,咱们就默认这个类型实现了接口。测试
Go 语言根据接口类型『是否包含一组方法』对类型作了不一样的处理,也就是分为空接口和有方法的接口。咱们使用 iface 结构体表示包含方法的接口;使用 eface 结构体表示不包含任何方法的 interface{} 类型。接下来咱们来看看这两种数据结构。ui
eface: type eface struct { // 16 bytes _type *_type data unsafe.Pointer }
因为 interface{} 类型不包含任何方法,因此它的结构也相对来讲比较简单,只包含指向底层数据和类型的两个指针。从上述结构咱们也能推断出 — Go 语言中的任意类型均可以转换成 interface{} 类型。this
另外一个用于表示接口的结构体就是 iface,iface 内部维护两个指针,tab 指向一个 itab 实体, 它表示接口的类型以及赋给这个接口的实体类型。data 则指向接口具体的值,通常而言是一个指向堆内存的指针。
iface: type iface struct { // 16 bytes tab *itab data unsafe.Pointer }
接下来咱们分别来看看type和tab里边又是什么内容:
type: type _type struct { size uintptr //字段存储了类型占用的内存空间,为内存空间的分配提供信息 ptrdata uintptr hash uint32 //字段可以帮助咱们快速肯定类型是否相等 tflag tflag //类型的 flag,和反射相关 align uint8 // 内存对齐相关 fieldAlign uint8 kind uint8 //类型的编号,有bool, slice, struct 等等等等 equal func(unsafe.Pointer, unsafe.Pointer) bool //字段用于判断当前类型的多个对象是否相等,该字段是为了减小 Go 语言二进制包大小从 typeAlg 结构体中迁移过来的 gcdata *byte //gc相关 str nameOff ptrToThis typeOff } tab: type itab struct { inter *interfacetype //接口的类型 _type *_type //实体的类型 link *itab hash uint32 // type.hash的拷贝,用于比较目标类型和接口类型 bad bool // type does not implement interface inhash bool // has this itab been added to hash? unused [2]byte fun [1]uintptr // 放置和接口方法对应的具体数据类型的方法地址,这里存储的是第一个方法的函数指针,若是有更多的方法,在它以后的内存空间里继续存储 } type interfacetype struct { typ _type //实体类型 pkgpath name //包名 mhdr []imethod //函数列表 }
由于eface和iface结构上有必定的共性,咱们这里就只看一下iface数据结构的图解,eface只是稍微变化一下就能够:
在这里有个问题,一个iface结构体维护一个接口类型和实体类型的对应关系,咱们在代码中经常会去屡次实现接口,那怎么存呢?答案就是只要在代码中存在引用关系, go 就会在运行时为这一对具体的 <Interface, Type> 生成 itab 信息。
咱们都知道,方法的接收者类型必须是某个自定义的数据类型,并且不能是接口类型或接口的指针类型。所谓的值方法,就是接收者类型是非指针的自定义数据类型的方法。那么,值方法和指针方法体如今哪里呢?咱们看下边的代码:
func (cat *Cat) SetName(name string) { cat.name = name }
方法SetName的接收者类型是*Cat。Cat左边再加个*表明的就是Cat类型的指针类型,这时,Cat能够被叫作*Cat的基本类型。你能够认为这种指针类型的值表示的是指向某个基本类型值的指针。那么,这个SetName就是指针方法。那么什么是值方法呢?通俗的讲,把Cat前边的*去掉就是值方法。指针方法和值方法究竟有什么区别呢?请看下文。
而指针方法的接收者,是该方法所属的那个基本类型值的指针值的一个副本。咱们在这样的方法内对该副本指向的值进行修改,却必定会体如今原值上。这块可能有点绕,但若是以前函数传切片那块理解的话这块也能够想明白,总之就是一个拷贝的是整个数据结构,一个拷贝的是指向数据结构的地址。
严格来说,咱们在这样的基本类型的值上只能调用到它的值方法。可是,Go 语言会适时地为咱们进行自动地转译,使得咱们在这样的值上也能调用到它的指针方法。
例以下边这种也是能够调用的:
type Pet interface { Name() string } type Dog struct { Class string } func (dog Dog) Name() string{ return dog.Class } func (dog *Dog) SetName(name string) { dog.Class = name } func main() { a := Dog{"grape"} a.SetName("nosay") //a会先取地址而后去调用指针方法 //Dog{"grape"}.SetName("nosay") //由于是值类型,调用失败,cannot call pointer method on Dog literal,cannot take the address of Dog literal (&Dog{"grape"}).SetName("nosay") //能够 }
在后边你会了解到,一个类型的方法集合中有哪些方法与它能实现哪些接口类型是息息相关的。若是一个基本类型和它的指针类型的方法集合是不一样的,那么它们具体实现的接口类型的数量就也会有差别,除非这两个数量都是零。
好比,一个指针类型实现了某某接口类型,但它的基本类型却不必定可以做为该接口的实现类型。例如:
type Pet interface { SetName(name string) Name()string Category()string } type Dog struct { name string } func (dog *Dog) SetName(name string) { dog.name = name } func(dog Dog) Name()string{ return dog.name } func (dog Dog)Category()string{ return "dog" } func main() { dog:=Dog{"little pig"} _,ok:=interface{}(dog).(Pet) fmt.Printf("Dog implements interface Pet: %v\n", ok) //false _, ok = interface{}(&dog).(Pet) fmt.Printf("*Dog implements interface Pet: %v\n", ok) fmt.Println() //true }
对于编译器在什么状况下调用这些方法会调用失败有如下几种状况:
值方法 | 指针方法 | |
---|---|---|
结构体初始化变量 | 经过 | 不经过 |
结构体指针初始化变量 | 经过 | 经过 |
说完基础知识的疑惑,接下来咱们具体举例看看GO如何实现面向对象的三把斧(继承,封装,多态的);
首先,咱们须要明确一个概念,Go语言中是没有继承的概念的,具体缘由在官网上是明确做出声明的(参见为何没有继承?,简单的说,面向对象编程中的继承,实际上是经过牺牲必定的代码简洁性来换取可扩展性,并且这种可扩展性是经过侵入的方式来实现的,而Go由于类型和接口之间没有明确的关系,因此不须要管理或讨论类型层次结构。
那么,咱们经过下边一个例子来看一下Go是怎么经过嵌入组合来实现继承的:
type Animal struct { name string subject string } // 动物的公共方法 func (a *Animal) Eat(food string) { fmt.Println("动物") } type Cat struct { // 继承动物的属性和方法 Animal // 猫本身的属性 age int } // 猫类独有的方法 func (c Cat) Sleep() { fmt.Println("睡觉") } func main() { // 建立一个动物类 animal := Animal{name:"动物", subject:"动物科"} animal.Eat("肉") // 建立一个猫类 cat := Cat{Animal: Animal{name:"猫", subject:"猫科"},age:1} cat.Eat("鱼") //调用的Animal的Eat方法,“继承”的体现 cat.Sleep() }
Go语言在包的级别进行封装。 以小写字母开头的名称只在该程序包中可见。 你能够隐藏私有包中的任何内容,只暴露特定的类型,接口和工厂函数。
例如,在这里要隐藏上面的Foo类型,只暴露接口,你能够将其重命名为小写的foo,并提供一个NewFoo()函数,返回公共Fooer接口:
type foo struct { } func (f foo) Foo1() { fmt.Println("Foo1() here") } func (f foo) Foo2() { fmt.Println("Foo2() here") } func (f foo) Foo3() { fmt.Println("Foo3() here") } func NewFoo() Fooer { return &Foo{} }
而后来自另外一个包的代码可使用NewFoo()并访问由内部foo类型实现的Footer接口,固然要记得引入包名:
f := NewFoo() f.Foo1() f.Foo2() f.Foo3()
多态性是面向对象编程的本质:只要对象坚持实现一样的接口,Go语言就能处理不一样类型的那些对象。 Go接口以很是直接和直观的方式提供这种能力。
这里有一个精心准备的例子,实现Ihello接口的多个实现被建立并存储在一个slice中,而后轮询调用Hello方法。 你会注意到不一样实例化对象的风格。
type IHello interface { Hello(name string) } func Hello(hello IHello) { hello.Hello("hello") } type People struct { Name string } func (people *People) Hello(say string) { fmt.Printf("the people is %v, say %v\n", people.Name, say) } type Man struct { People } func (man *Man) Hello(say string) { fmt.Printf("the people is %v, say %v\n", man.Name, say) } type Women struct { People } func (women *Women) Hello(say string) { fmt.Printf("the people is %v, say %v\n", women.Name, say) } func Echo(hello []IHello) { for _,val := range hello { val.Hello("hello world") } } func main() { hello1 := &People{"people"} hello2 := &Man{People{Name: "xiaoming"}} hello3 := &Women{People{Name: "xiaohong"}} sli := []IHello{hello1, hello2, hello3} //the people is people, say hello world //the people is xiaoming, say hello world //the people is xiaohong, say hello world Echo(sli) }
【Go语言踩坑系列(七)】Goroutine
欢迎对本系列文章感兴趣的读者订阅咱们的公众号,关注博主下次不迷路~