Go语言虽然存在指针,但是远比C语言指针简单,且Go语言基本指针不能直接进行指针运算。
Go语言基本指针用法和C语言几乎相同
&
取地址符*
解引用运算符func main() {
var num int
// 声明指向int类型的一级指针 ptr
var ptr *int
// 声明指向一个指针的二级指针 pptr
var pptr **int
// 取变量地址,赋值给指针
ptr = &num
// 取指针地址,赋值给二级指针
pptr = &ptr
// 通过指针修改变量的值
*ptr = 10
fmt.Printf("num = %d\n", num )
fmt.Printf("解引用:*ptr = %d\n", *ptr )
fmt.Printf("解引用:**pptr = %d\n", **pptr)
}
人们极容易对值类型、引用类型、指针类型三个概念混淆,特别是将引用和指针混淆。指针是指一个保存内存地址的变量;而值是指的数据本身,值类型表示这个变量代表的就是数据本身,而不是数据的内存地址。引用则最容易产生歧义的说法,在C++中存在一种引用类型,它表示的是变量的别名,因此C++中的引用和指针是两种不同的类型。而我们在Go中指的引用类型,是指特定的几个类型,分别是slice
、map
、chan
、interface
,通常它们在内部封装了真实数据的指针,因此这些类型并不是值类型,称呼为引用类型。人们习惯于把指针指向真实数据的内存空间这一现象称呼为引用,即表示对值所在的内存空间的一种引用。
为了使Go语言的指针也能像C语言那样能直接操作内存,Go提供了unsafe
包,正如其名,它是不安全的,官方不推荐的用法,不到万不得已不建议使用。
unsafe
包主要提供了两种类型,三个函数
unsafe.Pointer
:通用指针类型,主要用于转换不同类型的指针,不能进行指针运算uintptr
:主要用于指针运算,uintptr
无法持有对象,uintptr
类型的目标会被GC回收函数
Alignof
Offsetof
Sizeof
以上三个函数中,主要说一下Sizeof
,它类似于C语言的sizeof
运算符,获取一个变量所占据的字节数。
package main
import (
"fmt"
"unsafe"
)
func main() {
var price float64
var pi float32
var num int
var ptr *int
ptr = &num
fmt.Printf("float64 size is %d\n", unsafe.Sizeof(price))
fmt.Printf("float32 size is %d\n", unsafe.Sizeof(pi))
fmt.Printf("int size is %d\n", unsafe.Sizeof(num))
fmt.Printf("*int size is %d\n", unsafe.Sizeof(ptr))
}
打印结果:
float64 size is 8
float32 size is 4
int size is 8
*int size is 8
转化指针,强行进行指针运算
package main
import (
"fmt"
"unsafe"
)
func main() {
var arr [5]int = [5]int{1, 2, 3, 4, 5}
var p *int = &arr[0]
// 将普通指针转为 Pointer类型指针
var tmp unsafe.Pointer = unsafe.Pointer(p)
// 将Pointer类型转为uintptr后做指针加法运算
// 运算完成后,需将uintptr类型重新转为Pointer
// 再将Pointer重新转为普通类型 *int指针,最后解引用
fmt.Printf("arr[1] = %d\n", *(*int)(unsafe.Pointer(uintptr(tmp) + unsafe.Sizeof(p))))
fmt.Printf("arr[2] = %d\n", *(*int)(unsafe.Pointer(uintptr(tmp) + unsafe.Sizeof(p)*2)))
fmt.Printf("arr[3] = %d\n", *(*int)(unsafe.Pointer(uintptr(tmp) + unsafe.Sizeof(p)*3)))
}
打印结果:
arr[1] = 2
arr[2] = 3
arr[3] = 4
可以看到,想要在Go语言中进行指针运算,是相当麻烦的,Go语言本身也不建议我们使用指针运算。
另外一定要注意,只有uintptr
才能做指针运算,且GC并不把uintptr
当做指针,所以uintptr
不能持有对象, 它可能会被GC回收, 导致出现无法预知的错误。而Pointer
类型指针指向一个对象时, GC则不会回收这个内存对象
nil
表示空指针所谓面向对象,是相对于面向过程而言的。那什么是面向过程呢?C语言就是一种典型的面向过程的编程语言。其实过程,也就是所谓的步骤。有一个经典例子是这样的,如何把大象放进冰箱?
有些人可能会觉得荒诞,大象怎么能放得进冰箱呢?然而这就是面向过程的思维方式,C语言代码如下
void openDoor(){}
void put(void *){}
void closeDoor(){}
int main(){
// 打开门
openDoor();
// 放进去
put(obj);
// 关上门
closeDoor();
}
每一个步骤对应到代码其实就是一个函数,每一个函数实现一个功能,然后分步调用这些函数。这些函数可能是我们自己写的,也可能是别人写的,函数实际上是一个黑盒模型,这个盒子是封闭的,我们不知道里面有什么,只知道这个盒子有一个入口和一个出口,就如同ATM机,我们把卡插到入口,另一边出口就冒钱出来了,至于具体的,钱是怎么冒出来的,这不是我们关心的。回到上面的例子,大象能不能放进冰箱,这也不是我们关心的,总之这个函数就是把大象放进冰箱的函数,只管调用它就好了。
有了这种面向过程的思维方式,编程就变得简单清晰有条理了,我们可以先把整个架子先搭起来,所有的函数先空实现,整个架子建好了,再慢慢去实现这一个个函数的具体细节。这跟建房子一样,先把钢结构架子搭起来,然后再慢慢码砖砌墙,最后才是室内装修。
随着软件业的发展,需求越来越复杂,人们发现面向过程的思维模型太简单了,已经无法胜任日益复杂的软件需求了,于是就出现了面向对象的思维方式。面向对象既是一种思维模型,也是一种代码的组织形式。
面向对象核心的载体是类和对象。那什么是类?什么是对象呢?
要说清楚这个问题,得先解释什么是对象,不然还怎么去面向对象呢。在面向对象的哲学里,有一句话是“一切皆对象!”对象一词实际上是从英语翻译过来的,这个翻译其实是不准确的,最重要的就是没有指明这个概念的内涵。它的英文object
实际上表达的是具体事物,客观事物,客体的意思。其实就是将具体事物抽象化,用一句星爷电影《功夫》中的台词来解释就是“那个谁”的意思,就是将一切的具体事物,抽象出一个共同的指代模型,你也可以说“那个东西”、“那个事物”,你在说这句话时,一定是指的一个具体存在的东西,而不是一个空泛的虚无的东西,这就是对象的特点。
了解了对象,我们不禁要问,编程中怎么创建对象,怎么运用对象呢?可以试想一下,假设我们现在想要描述猫这种动物,该怎么做?
首先可以观察具体的猫,然后将所有猫都具备的特征提取出来,抽象出来,这个抽象出来的模型也就是类。例如,猫都有尾巴,有毛,圆眼竖瞳,喜欢睡觉,昼伏夜出,会抓老鼠,会喵喵叫,喜欢吃鱼等等。这里我们就提取几个特征,形成一个猫类
有了类,我们就可以判断一只猫是否属于猫类,也可以根据这个类批量创造猫。可以看出,类其实就是一个设计蓝图,或者说是一个模具,所有依据这个蓝图创造的具体的猫都是这个类的一个对象。类就是一个图纸,对象就是这个图纸的具体事物。
类所包含的特征,我们通常分为两种类型,属性和行为。属性是静态的描述,行为是动态的特征。以上面的猫类为例
属性 | 行为 |
---|---|
圆眼竖瞳 | 吃鱼 |
有皮毛 | 抓老鼠 |
睡觉 |
行为往往是以动词开头,在编程中用使用函数来表示,而属性则使用变量来表示。纯粹的面向对象编程语言是Java和C#,其次支持面向对象的还有C++和Python等。Go与这些编程语言不同,它没有在语法层面完全支持面向对象,譬如它没有类的概念,Go只能像C语言一样,使用结构体来模拟类,但是Go语言的结构体与C++中的结构体不同,C++的结构体并不是真正的结构体,它实际上就是一个类,C++中结构体与类的差别不大,而Go语言的结构体,更接近C语言的结构体。
Go语言的结构体类似于C语言的结构体,Go语言使用结构体来模拟类,因此,我们可以简单的将Go的结构体看做是一个类,通过这个结构体生成的也就是该类的对象。
// 定义学生结构体,即等同于学生类
type Student struct{
id uint64
name string
age int
score float64
}
func main() {
// 声明结构体变量 stu
var stu Student
// 四种创建结构体对象的型式,即创建对象
stu1 := new(Student)
stu2 := Student{}
// 创建时初始化。按属性顺序初始化
stu3 := Student{1001, "Alice", 18, 259.5}
// 声明式初始化
stu4 := Student{id:1003,name: "Tom", age: 19}
// 结构体对象的属性访问与赋值
stu2.name = "John"
stu2.id = 1002
stu4.score = 190.5
}
定义结构体的格式,注意,定义结构体属性时,不要使用var
关键字
type 结构体名 struct{
字段(属性)
}
在Go语言中,未进行显式初始化的变量都会被初始化为该类型的零值,结构体的属性字段也是一样
另外要注意一点,在C语言中,结构体指针调用成员变量时,使用->
操作符,而Go语言中都是使用.
操作符,Go语言会对结构体指针做自动转换然后再访问成员
// 结构体指针
pStu := &Student{}
pStu.name = "John"
// 等价于以下调用。Go会先解引用然后在访问成员
(*pStu).name = "John"
方法就是一种特殊的函数,对应到面向对象类的概念中,也就是所谓的行为。在Go语言中,方法和函数最显著的区别是多了一个接收者的参数。
package main
import (
"fmt"
"math"
)
// 定义结构体
type Point struct{
X,Y float64
}
// 为结构体添加SetX方法
func(this *Point)SetX(x float64){
this.X = x
}
// 为结构体添加SetY方法
func(this *Point)SetY(y float64){
this.Y = y
}
// 为结构体添加GetDistance方法
func(this *Point)GetDistance() float64{
return math.Sqrt(this.X*this.X + this.Y*this.Y)
}
func main() {
p := Point{3,4}
// 使用结构体对象调用方法
fmt.Println(p.GetDistance())
}
定义结构体方法格式:
func(接收者)方法名(参数列表) 返回值列表 {
}
方法与函数唯一的区别就是多了接收者,它位于关键字func
和方法名之间,它的类型就是需要添加方法的结构类型,该参数通常使用结构体指针,参数名任意,不过推荐使用this
或self
,这里接收者的作用相当于C++中的this
指针,或者Python中的self
。
在Go语言中,不仅仅是结构体有方法,所有自定义类型都可以添加方法
// 将int 声明为新类型Integer
type Integer int
func(this Integer)Add(a int) int{
return int(this) + a
}
func main() {
var num Integer = 21
fmt.Println(num.Add(9))
}
在Go1.9版本中引入了新特性类型别名。在此之前,type
关键字只能用于定义新类型,1.9之后,可以用于定义类型别名。
// 定义新类型
type 新类型名 原类型名
// 定义类型别名
type 类型别名=原类型名
那么定义新类型和定义类型别名有什么区别呢?
// 定义类型别名
type Integer1=int
// 定义新类型
type Integer2 int
func main() {
var num int = 1
var a Integer1
a = num //不会报错
var b Integer2
b = num //报错
}
类型别名与原类型是完全等同的,而定义的新类型与原类型是不同的,因此将原类型直接赋值给新类型会报错,相应的,定义新类型都可以绑定方法,而使用类型别名则不一定,如上例中,原类型int
是不能绑定方法的,因此Integer1
也是不能绑定方法的。在C语言中,typedef
关键字正是用于定义类型别名的,因此要注意Go语言的区别。
结构体是没有所谓的构造方法的,因此说Go语言的面向对象不是纯粹的面向对象。通常的,可以创建一个名为NewXXX
的工厂函数用来专门创建结构体的实例对象。
func NewPoint(x,y float64) *Point{
return &Point{x,y}
}
其实接口是我们生活中常接触的概念,最具代表性的是我们手机的充电接口。在智能手机之前的时代,不同的手机都有专用充电器,每一种的插口都是不同的,这给我们生活造成了很大不便,如果一家人出行,得带一大堆充电器,有手机、数码相机、mp3等等电子产品,后来
Mini USB
接口开始流行,各大电子厂商都遵循这种接口标准,包括按摩仪、剃须刀、电动牙刷等等,从此开始,充电器变得可以通用了,再之后安卓智能手机流行,出现了新的Micro USB
接口,到今天仍然是安卓手机最主流的数据、充电接口。目前,新一代手机数据接口type-C
也开始逐渐普及。
从上例的物理接口中我们可以得到启示,接口实质上是一种通用的标准或协议,它规范了某种行为特征,而规范接口的好处在于可以即插即用,非常方便。假设手机没电了,我们只需要借一个与手机接口匹配的充电器即可,我们不再关心充电器的具体情况,比如电压、电流等参数,在我们的意识里,只要接口能对上就是可以用的。
实际上,面向对象开发中的所谓接口,其概念正是来自生活中,它的特点跟优势与上例中的物理接口是类似的。Go语言中的接口可以用来定义一组不用实现的方法。如同Java中的抽象方法,C++中的虚函数。与Java等语言不同的是,Go的接口不需要显式的实现。
// 声明接口
type Phone interface{
// 声明一个打电话的方法
Call(number string)bool
// 声明一个发短信的方法
SendMessage(number, text string)bool
}
// 声明一个结构体,并隐式实现Phone 接口
type HuaWei struct{
}
func(this *HuaWei)Call(number string)bool{
fmt.Println("呼叫:"+number)
return true
}
func(this *HuaWei)SendMessage(number, text string)bool{
fmt.Println("发送给:"+number+" , "+text)
return true
}
func main() {
// 声明一个接口类型变量 phone
var phone Phone
// 创建结构体对象
h := &HuaWei{}
// 通过赋值,初始化接口类型变量
phone = h
// 使用接口变量调用方法
phone.Call("123456")
phone.SendMessage("10086","查询话费")
}
我们从手机中抽象出两个功能,分别是打电话和发短信,只要具有这两个功能的电子产品,我们就认为它是手机。声明一个Phone
接口,它具有两个空方法Call
和SendMessage
,再定义一个具体的结构体HuaWei
,然后给HuaWei
结构体绑定两个Phone
接口的具体实现方法,这时候HuaWei
结构体即隐式的实现了Phone
接口,我们就可以说HuaWei
是Phone
接口的一个具体实现。当然,除了HuaWei
还有很多其他品牌手机,我们还可以定义更多不同的结构体来实现Phone
接口,总之,只要实现了Phone
接口,它就是手机。我们在使用的时候,将具体的结构体对象赋值给接口类型对象,然后使用接口类型对象去调用方法,而不是使用具体的结构体HuaWei
的实例对象去调方法。举个例子,当我们需要打电话发短信时,根本不关心具体是什么手机,只要能打出去,能发出去就可以了,这是手机通用的功能,甚至非智能手机都能做到,这种思想也就是面向对象编程中常说的解耦合,通用的功能不要和特定的对象关联起来,如上例中使用具体结构体的h
变量调用方法,这就是和特定对象关联了。在Go语言中,正是使用接口来实现解耦合。
格式
type 接口名 interface{
方法声明1
方法声明2
}
注意,接口中的方法声明不需要func
关键字,不需要声明接收者,也不需要方法体(不需要花括号),其他的和普通的函数声明一致。
即
方法名 + 函数签名
Go中的接口实现是一种隐式实现,即某个自定义类型中包含全部的接口方法的实现,则这个自定义类型自动实现该接口。因此要注意,除了结构体可以实现接口,通过type
关键字创建的新类型都可以实现接口。另外的,一个自定义类型是可以实现多个接口的,只要实现了多个接口的所有方法,它就会自动实现这些接口。
在面向对象编程中,通常有超类的概念,即所有的类都默认继承某个类,例如Java和Python中Object,而在Go语言中,也有一个所有类型都默认实现的接口——空接口。Go语言目前没有泛型的概念,通常就需要使用空接口来实现类似泛型的功能。
空接口是一个匿名的接口,它不包含任何方法
interface{}
Go语言中的数组和切片只能存放相同的数据类型,我们知道Python中的列表是可以存放任意类型的数据的,那我们如何让数组方法不同的数据类型的元素呢?答案就是借助空接口,声明一个空接口类型的数组
type MyType struct{
}
// 声明一个interface{}类型的数组,它的长度是5,
objs := [5]interface{}{1,"abc",1.5,[1]int{0},MyType{}}
所有类型都默认实现空接口,包括基本数据类型,这表示所有类型都是interface{}
类型的子类型,因此interface{}
类型数组就可以装下所有类型的数据。
假如一个数组或切片是interface{}
类型的,那么我们遍历这个数组时,怎么判断该数据的具体类型是什么呢?
在Go语言中,可以使用多种方式判断一个变量的具体类型或是否实现了某接口,这里主要说明一下类型断言与类型查询
类型断言
func main() {
objs := [5]interface{}{1,"abc",1.5,[1]int{0},MyType{}}
for _,it := range objs{
// 类型断言,如何匹配括号中的类型,则ok为true
if o,ok := it.(string); ok{
fmt.Printf("string类型:%s\n",o)
}
if o,ok := it.(int);ok{
fmt.Printf("int类型:%d\n",o)
}
if o,ok := it.(interface{});ok{
fmt.Printf("interface{}类型:%T\n",o)
}
}
}
类型查询
func main() {
objs := [5]interface{}{1,"abc",1.5,[1]int{0},MyType{}}
for _,it := range objs{
// 接口查询,使用switch结构
switch v := it.(type){
case string:
fmt.Printf("string类型:%s\n",v)
case int:
fmt.Printf("int类型:%d\n",v)
case [1]int:
fmt.Printf("[1]int类型:%T\n",v)
case interface{}:
fmt.Printf("interface{}类型:%T\n",v)
default:
fmt.Println("未知类型")
}
}
}
nil
面向对象有三大特性,分别是封装、继承和多态,如果不能支持这三大特性,那么就不能说这门编程语言支持面向对象。
即将类中抽象出来的属性和对属性的操作封装在一起,并把数据保护在内部,仅对其他包提供有限的操作权限。封装能隐藏实现细节,提供对数据的验证。
我们知道Java有四种包访问权限,C++也有private
和public
,而在Go语言中却并未提供关键字来设置访问权限,它更类似于Python,对包外仅提供可见与不可见两种权限,属性名(包括方法名)首字母大写,则包外可访问,小写则不可访问。Go语言主要通过结构体方法、包访问权限来实现封装特性。大家会发现,Go语言标准库提供的所有函数都是大写字母开头的,这就是为了让包外可访问。相比于其他语言,Go的封装格外别扭。
继承的主要目的就是为了代码复用,更简单说就是为了少写代码,同也更容易构建类与类之间的结构化关系。
type Animal struct{
age int
}
func(this *Animal)Eat(){
fmt.Println("吃东西……")
}
func(this *Animal)Sleep(){
fmt.Println("睡觉……")
}
type Cat struct{
Animal // 内嵌匿名结构体表示继承
}
func main() {
cat := Cat{}
cat.age = 10
cat.Eat()
cat.Sleep()
}
上例中,定义了动物结构体,然后定义Cat
结构体,并让它继承于动物结构体,可以看到,在Cat
结构体中并未声明age
属性,也未绑定任何方法,但是Cat
继承了Animal
的属性和方法,因此它也具备了这些属性和方法。
当一个结构体与它继承的结构体存在同名属性或方法时,可以使用显式的方式访问
type A struct{
Name string
id int64
}
type B struct{
A
Name string
num int
}
func main() {
b := B{}
b.A.Name = "xx"
b.Name = "b"
b.A.id = 1001
}
Go的结构体也可以多继承,多继承时存在同名字段,可以显式访问
type C struct{
A
B
}
func main() {
c := C{}
c.A.Name = "xx"
c.B.Name = "b"
c.B.num = 100
}
除了结构体,接口也可以继承
type A interface{
Method1()
}
type B interface{
Method2()
}
type C interface{
A
B
Method3()
}
如上,C
接口继承了A
、B
接口,此时要想实现C
接口,就必须将A
、B
和C
中的方法全部实现
实例对象具有多种形态,可以按照统一的接口来调用多种不同的实现,即面向对象所谓的多态。
Go语言的多态主要体现在两方面,函数参数多态和数组元素多态上面,而数组元素多态,就如同接口一节的interface{}
类型数组的例子。
// 声明一个宠物接口
type Pet interface{
// 声明一个遛宠物功能函数
Walk()
}
// 声明猫结构体
type Cat struct{
}
func(this *Cat)Walk(){
fmt.Println("遛猫……")
}
// 声明狗结构体
type Dog struct{
}
func(this *Dog)Walk(){
fmt.Println("遛狗……")
}
// 声明熊结构体
type Bear struct{
}
func(this *Bear)Walk(){
fmt.Println("战斗民族遛熊……")
}
// 定义和宠物一起玩的函数
func PlayWithPets(p Pet){
// 调用遛宠物的功能
p.Walk()
}
func main() {
// 声明并初始化一个宠物数组,其元素分别是三种不同的结构体
// 这就是多态的数组,实质就是一个泛型数组
var pets [3]Pet = [3]Pet{&Cat{}, &Dog{}, &Bear{}}
for i := 0;i < 3;i++ {
// 函数参数上的多态,传入的实际上是三个不同的结构体对象
PlayWithPets(pets[i])
}
}