在上一篇文章中,咱们聊了聊Golang中的一些基础的语法,如变量的定义、条件语句、循环语句等等。他们和其余语言很类似,咱们只须要看一看它们之间的区别,就差很少能够掌握了,因此做者称它们为“基础语法”。在这篇文章中,咱们将聊一聊Golang的一些语言特性,这也是Golang和其余语言差异比较大的地方。除此以外,还有一部份内容是关于Golang的并发,这一部分将在下一篇文章中介绍。html
在Java中,咱们已经体会过了面向对象的方便之处。咱们只须要将现实中的模型抽象出来,就成为了一个类,类里面定义了描述这个类的一些属性。编程
而在Golang中,则没有对象这一说法,由于Golang是一个面向过程的语言。可是,咱们又知道面向对象在开发中的便捷性,因此咱们在Golang中有了结构体这一类型。并发
结构体是复合类型,当须要定义类型,它由一系列属性组成,每一个属性都有本身的类型和值的时候,就应该使用结构体,它把数据汇集在一块儿。
组成结构体类型的那些数据成为字段(fields)。每一个字段都有一个类型和一个名字;在一个结构体中,字段名字必须是惟一的。函数
咱们能够近似的认为,一个结构体就是一个类,结构体内部的字段,就是类的属性。学习
注意,在结构体中也遵循用大小写来设置公有或私有的规则。若是这个结构体名字的第一个字母是大写,则能够被其余包访问,不然,只能在包内进行访问。而结构体内的字段也同样,也是遵循同样的大小写肯定可用性的规则。
对于结构体,他的定义方式以下:设计
type 结构体名 struct { 字段1 类型 字段2 类型 }
对于结构体的声明和初始化,有如下几种形式:指针
使用var关键字code
var s T s.a = 1 s.b = 2
注意,在使用了var
关键字以后不须要初始化,这和其余的语言有些不一样。Golang会自动分配内存空间,并将该内存空间设置为默认的值,咱们只须要按需进行赋值便可。htm
使用new函数对象
type people struct { name string age int } func main() { ming := new(people) ming.name = "xiao ming" ming.age = 18 }
使用字面量
type people struct { name string age int } func main() { ming := &people{"xiao ming", 18} }
上面咱们提到了几种结构体的声明的方法,但其实这几种是有些区别的。
先说结论,第一种使用var
声明的方式,返回的是该实例的结构类型,而第二第三种,返回的是一个指向这个结构类型的一个指针,是地址。
注意,这一部分做者能够保证是观点是正确的。可是做者的解释其实有些问题。这是由于做者能力有限,还没开始研究Golang的源码,因此不能很好的解释“返回的是实例的结构类型”这一句话。在做者的理解中,返回类型有两种,一种是具体的数值,一种是指向这个数值的指针。
因此,对于第二第三种返回指针的声明形式,在咱们须要修改他的值的时候,其实应该使用的方式是:
(*ming).name = "xiao wang"
也就是说,对于指针类型的数值,应该要先用*
取值,而后再修改。
可是,在Golang中,能够省略这一步骤,直接使用ming.name = "xiao wang"
。尽管如此,咱们应该知道这一行为的缘由,分清楚本身所操做的对象到底是什么类型,掌握这点对下面方法这一章节相当重要。
在上一节的内容中,咱们也提到了面向对象的优点,而Golang又是一种面向过程的语言。在上一章节中,提到了用结构体实现了对象这一律念。在这一章中,提到的是对象对应的方法。
在Go语言中有一个概念,它和方法有着一样的名字,而且大致上意思相同,Go的 方法是做用在接收器(receiver)上的一个函数,接收器是某种类型的变量,所以方法是一种特殊类型的函数。
说白了,方法就是函数,只不过是一种比较特殊的函数。
咱们都知道,在Golang中,定义一个函数是这样的:
func 函数名(args) 返回类型
而在此基础上,在func
和函数名
之间,加上接受者的类型,就能够定义一个方法。
type Vertex struct { X, Y float64 } func (v Vertex) Abs() float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y) } func main() { v := Vertex{3, 4} fmt.Println(v.Abs()) }
能够看到,咱们定义了一个Vertex
为接收者的方法。也就是说,这个方法,仅仅能够被Vertex
的结构体数值调用。
注意,接受者有两种类型,即指针接收者和非指针接受者。
咱们来看下面的代码:
type Vertex struct { X, Y float64 } func (v Vertex) test1(){ v.X++; v.Y++; } func (v *Vertex) test2(){ v.X++; v.Y++; }
在这里咱们定义了两个方法,test1
和test2
,他们惟一的区别就是方法名前面的接收者不一样,一个是指针类型的,一个是值类型的。
而且,执行这两个方法,也须要定义不一样的结构体类型。
v1 := Vertex{1, 1} v2 := &Vertex{1, 1} v1.test1() v2.test2() fmt.Println(v1) fmt.Println(v2)
执行以后咱们能够查看结果:
{1 1} &{2 2}
也就是说,只有指针接收者类型的方法,才能修改这个接收器的成员值,非指针接收者,方法修改的只是这个传入的指针接收者的一个拷贝。
那么为何会这样,咱们一样拿代码说话:
type Vertex struct { X, Y float64 } func (v Vertex) test1(){ fmt.Printf("在方法中的v的地址为:%p\n", &v) v.X++; v.Y++; } func main() { v1 := Vertex{1, 1} fmt.Printf("本身定义的v1内存地址为:%p\n", &v1) v1.test1() }
在上述的代码中,我定义了一个非指针类型接收者的方法,而后打印方法外的v1和方法内的v的内存地址,结果以下:
本身定义的v1内存地址为:0xc00000a0e0 在方法中的v的地址为:0xc00000a100
咱们能够看出,这两个结构体数值的内存地址是不同的。也就是说,就算咱们修改了方法内的数值,对方法外的原变量也不能起到任何的做用,由于咱们修改的只是一个结构体数值的拷贝,没有真正的修改的他原本的值。
可是,若是使用的是指针接收者,他们的内存地址就是同样的了,下面看代码:
type Vertex struct { X, Y float64 } func (v *Vertex) test2(){ fmt.Printf("在方法中的v的地址为:%p\n", v) v.X++; v.Y++; } func main() { v1 := &Vertex{1, 1} fmt.Printf("本身定义的v1内存地址为:%p\n", v1) v1.test2() }
执行以后的结果为:
本身定义的v1内存地址为:0xc00000a0e0 在方法中的v的地址为:0xc00000a0e0
因此咱们能够知道,使用指针接收器是能够直接拿到原数据所在的内存地址,也就是说能够直接修改原来的数值。这也和Java中的对象调用方法更加类似;而对于非指针,它是拷贝原来的数据。至于使用哪种,须要按照实际的业务来处理。
可是,若是是一个大对象,若是也采用拷贝的方式,将会耗费大量的内存,下降效率。
还有一点须要补充说明:不论是指针接收者仍是非指针接收者,他在接受一个对象的时候,会自动将这个对象转换为这个方法所须要的类型。也就是说,若是我如今有一个非指针类型的对象,去调用一个指针接收者的方法,那么这个对象将会自动被取地址而后再被调用。
换句话说,方法的调用类型不重要,重要的是方法是怎么定义的。
在聊接口怎么用以前,咱们先来聊聊接口的做用。
在做者看来,接口是一种规范,一种约定。举个例子:一个商品只要是符合某种种类的约定,遵循某种种类的规范,那么就能够认为这个商品是属于这个种类的,他会具备这个种类应有的一切功能。这样作的目的是为了把生产这个商品的生产者和使用这个商品的消费者分开。用编程里面的术语来说,咱们能够把实现和调用解耦。
下面举个鸭子模型的例子,来自于知乎,能够说特别的形象生动了。注意,在这里先不研究语法,语法的问题咱们后面会提到,你只须要跟随做者的思路去思考:
首先咱们定义一个规范,也就是说定义一个接口:
type Duck interface { Quack() // 鸭子叫 DuckGo() // 鸭子走 }
这个接口是鸭子的行为,咱们认为,做为一只鸭子,它须要会叫,会走。而后咱们再定义一只鸡:
type Chicken struct { }
假设这只鸡特别厉害,它也会像鸭子那样叫,也会像鸭子那样走路,那么咱们定义一下这只鸡的行为:
func (c Chicken) Quack() { fmt.Println("嘎嘎") } func (c Chicken) DuckGo() { fmt.Println("大摇大摆的走") }
注意,这里只是实现了 Duck 接口方法,并无将鸡类型和鸭子接口显式绑定。这是一种非侵入式的设计。
而后咱们让这只鸡,去叫,去像鸭子那样走路:
func main() { c := Chicken{} var d Duck d = c d.Quack() d.DuckGo() }
执行以后咱们能够获得结果:
嘎嘎 大摇大摆的走
也就是说,这只鸡,他能作到鸭子能作的全部事情,那么咱们能够认为,这只鸡,他就是一个鸭子。
这里牵涉到了一个概念,任何类型的数据,它只要实现了一个接口中方法集,那么他就属于这个接口类型。因此,当咱们在实现一个接口的时候,须要实现这个接口下的全部方法,不然编译将不能经过。
理解了接口是什么以后,咱们再来聊聊语法,首先是如何定义一个接口:
type 接口名 interface { 方法1(参数) 返回类型 方法2(参数) 返回类型 ... }
这一部分和结构体的定义很类似,可是里面的元素换成了函数,可是这个函数不须要func
关键字。这里和Java中的接口类似,不须要访问修饰符,只须要函数名参数返回类型。
定义完接口以后,咱们须要定义方法去实现这些接口。注意,这里新定义方法的方法名,参数,返回类型,必须和接口中所定义的彻底一致。
其次,这里的方法中的接收者,就是调用这个方法的对象类型。换句话说,这个方法想要被哪类对象执行,接收者就是谁。
还有最重要的一点,若是要实现某个接口,必需要实现这个接口的所有方法。
在调用接口的时候,咱们须要先声明这个接口类型的变量,如咱们上面定义了一个Duck
接口,就应该声明一个Duck
类型的变量。
var d Duck
而后咱们把实现了这个方法的接收器对象赋值给这个变量d
:
d := Chicken{}
随后,咱们就能够用这个变量d
,是调用那些方法了。
首先,仍是很感谢你能看到这里,谢谢你!(鞠躬
这是《Golang入门》系列的第三篇了,也差很少要讲完Golang的基本语法了。在这篇文章中是介绍了一些Golang的比较特殊的用法,但愿可以对你有所帮助。
固然了,做者也只是个初学者,也才刚刚开始学习Golang。在学习的过程当中,不免会有错误的理解,或者会有遗漏的地方。若是你发现了,请你留言指正我,谢谢!除此以外,若是有我讲的不清楚不明白的地方,也欢迎留言,咱们一块儿交流学习。
在下一篇中,做者将介绍一下Golang的并发。咱们下篇文章见。
再次感谢~
PS:若是有其余的问题,也能够在公众号找到做者。而且,全部文章第一时间会在公众号更新,欢迎来找做者玩~