Go语言入门——进阶语法篇(三)

进阶语法

指针

基本指针

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中指的引用类型,是指特定的几个类型,分别是slicemapchaninterface,通常它们在内部封装了真实数据的指针,因此这些类型并不是值类型,称呼为引用类型。人们习惯于把指针指向真实数据的内存空间这一现象称呼为引用,即表示对值所在的内存空间的一种引用。

高级指针

为了使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则不会回收这个内存对象

指针总结

  • Go指针主要用于传递结构体地址,防止不必要的内存拷贝,提升性能
  • Go语言使用nil表示空指针
  • Go无需手动管理堆内存,Go编译器会基于逃逸分析来确定内存分配在堆上还是栈上,因此无需对指针进行释放,堆上的内存由GC处理

面向对象

概述

所谓面向对象,是相对于面向过程而言的。那什么是面向过程呢?C语言就是一种典型的面向过程的编程语言。其实过程,也就是所谓的步骤。有一个经典例子是这样的,如何把大象放进冰箱?

  1. 把冰箱门打开
  2. 把大象放进去
  3. 把冰箱门关上

有些人可能会觉得荒诞,大象怎么能放得进冰箱呢?然而这就是面向过程的思维方式,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和方法名之间,它的类型就是需要添加方法的结构类型,该参数通常使用结构体指针,参数名任意,不过推荐使用thisself,这里接收者的作用相当于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接口,它具有两个空方法CallSendMessage,再定义一个具体的结构体HuaWei,然后给HuaWei结构体绑定两个Phone接口的具体实现方法,这时候HuaWei结构体即隐式的实现了Phone接口,我们就可以说HuaWeiPhone接口的一个具体实现。当然,除了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("未知类型")
		}
	}
}

接口总结

  • 接口中的方法必须是空实现,没有方法体
  • 接口中不能声明变量
  • 接口不能创建实例,只能通过赋值初始化。赋值对象可以是实现了接口的自定义类型,也可以是一个接口类型
  • Go接口只能隐式实现,即实现接口包含的全部方法
  • 除了结构体,其他任意自定义类型都能实现接口
  • 所有类型,包括基本类型都默认实现空接口
  • 接口是引用类型,其零值为nil

面向对象三大特性

面向对象有三大特性,分别是封装继承多态,如果不能支持这三大特性,那么就不能说这门编程语言支持面向对象。

封装

即将类中抽象出来的属性和对属性的操作封装在一起,并把数据保护在内部,仅对其他包提供有限的操作权限。封装能隐藏实现细节,提供对数据的验证。

我们知道Java有四种包访问权限,C++也有privatepublic,而在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接口继承了AB接口,此时要想实现C接口,就必须将ABC中的方法全部实现

多态

实例对象具有多种形态,可以按照统一的接口来调用多种不同的实现,即面向对象所谓的多态。

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])
	}
}

欢迎关注我的公众号:编程之路从0到1

编程之路从0到1

猜你喜欢

转载自blog.csdn.net/yingshukun/article/details/98959344