本节继续介绍Go语言的基础语法知识,具体包括以下内容:
- 指针类型
- 运算符及优先级
- 类型转换
在上一讲中,我们介绍了Go语言的基本数据类型,学会了如何对变量进行声明和赋值,也知道了在这个过程中会在内存开辟空间方便它们“安家”。那如何才能找到这个“家”呢?这就涉及到了指针。
通过使用指针,开发者可以直接访问内存中的数据,从而可以实现对数据的精准管理以及运算。
如果说变量名是数据的“代号”,那么指针存放的则是数据的“实际地址”,我们可以通过这个地址获取或修改存放于这个地址的变量的值。
存放整数值的变量称为整型变量,存放布尔值的变量称为布尔变量……类似地,存放指针值的变量称为指针变量。
那么问题来了,想要获取或改变某个变量的值,直接通过变量名就可以实现了。而且像Java之类的编程语言几乎不会用到指针,那Go为何还要用指针呢?
❗️ 注意: 实际上,Java中的指针操作封装在JDK中,普通开发者一般不会接触到,所以会误认为Java没有指针。
# 指针类型
我们不妨先了解一下Go语言中的指针,它主要由两大核心概念构成:类型指针和切片指针。
- 类型指针:在传递数据时直接使用指针,可以避免创建数据的副本,节约内存开销。类型指针不能进行偏移和运算,可以避免非法修改为其它数据的风险,也更有利于垃圾回收机制及时找到并回收它们;
- 切片指针:切片由指向起始元素的指针、元素数量和总容量构成。当访问切片发生越界时,会发生宕机并输出堆栈信息。宕机是可以恢复的,而崩溃只能导致程序停止运行。
可见,使用指针更有利于程序运行的性能和稳定性。另外,在某些操作中,如使用反射修改变量的值,必须使用可寻址的变量(通过指针)。
在实际应用中,最为常用的便是获取变量的内存地址,以及获取某个地址对应的值。在Go语言中,前者使用“&”运算符,后者使用“*”运算符。它们互为反向操作,操作的对象也不同。具体请看下面的示例:
Go
复制代码//exampleNumberA变量(整数型变量)声明和赋值
var exampleNumberA int = 10
//获取exampleNumberA的地址,并赋值给exampleNumberAPtr变量(exampleNumberAPtr的类型是指针类型)
exampleNumberAPtr := &exampleNumberA
//输出exampleNumberAPtr变量的值(将输出内存地址)
fmt.Println(exampleNumberAPtr)
//获取exampleNumberAPtr(指针变量)表示的实际数据值,并赋值给exampleNumberAPtrValue变量(整数型变量)
exampleNumberAPtrValue := *exampleNumberAPtr
//输出exampleNumberAPtrValue变量(整数型变量)的值
fmt.Println(exampleNumberAPtrValue)
运行后,控制台输出:
0xc00001a088 10
💡 提示: 在练习时,如果内存地址输出与上述结果不符,或者即使是相同的程序,每次运行结果也不同,都是正常现象。因为内存的分配并不是固定的。
上面的代码示例演示了如何使用已有的变量创建指针类型变量。我们还可以使用new()函数直接创建指针变量,相当于在内存中创建了没有变量名的某种类型的变量。
这样做无需产生新的数据“代号”,取值和赋值转而通过指针变量完成。常用在无需变量名或必须要传递指针变量值的场景中。
new()函数的使用格式如下:
Go
复制代码new(type)
其中,type是所在地址存放的数据类型。一旦完成创建,便会在内存中“安家”,完成内存分配,即使没有赋值。
具体代码示例如下:
Go
复制代码//使用new()函数创建名为exampleNumberAPtr指针类型变量,表示int64型值
exampleNumberAPtr := new(int64)
//修改exampleNumberAPtr表示的实际数据值
*exampleNumberAPtr = 100
//获取exampleNumberAPtr表示的实际数据值
fmt.Println(*exampleNumberAPtr)
程序运行后,控制台将输出:
100
# 运算符与优先级
有了数据,下一步便是使用这些数据进行运算了。Go语言总共提供了6种常用的运算符,分别为算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符以及指针运算符。我们先从最易懂的算术运算符开始。
💡 提示: 附录一详细列出了Go语言中所有的运算符及其含义,以及运算符的优先级顺序。各位在学习时应对照参考,尝试每一种运算符的使用。
# 算术运算符
算术运算符的意义和使用和数学上的概念很类似,比如:
Go
复制代码var exampleNumA int = 10
var exampleNumB int = 20
var exampleNumC int = 30
var exampleNumD = exampleNumA + exampleNumB*exampleNumC
fmt.Println(exampleNumD)
上述代码运行后,控制台输出610。显然,exampleNumD的值就是10+20*30,即610。
需要注意的是,在做除法时,对于int类别结果只保留整数。即使无法整除,余数也会被丢弃。如:
Go
复制代码var exampleNumA int = 10
var exampleNumB int = 3
fmt.Println(exampleNumA / exampleNumB)
运行后,控制台的输出为:
3
若要获取余数,需要用到取余(%)运算符,请参考下面的代码:
Go
复制代码var exampleNumA int = 10
var exampleNumB int = 3
fmt.Println(exampleNumA / exampleNumB)
fmt.Println(exampleNumA % exampleNumB)
运行后,控制台将输出:
3 1
上述结果中,3仍然是除法的结果,1是余数。
❗️ 注意: 和数学中的除法限制一样,0不能作为被除数,否则将引发宕机。当我们误将0作为除数时,GoLand会以波浪线的形式给出警告提示。
另外,还有一种较为精简的自增(++)和自减(--)运算符,它们相当于加1和减1,然后再将计算结果赋值给自身变量,如:
Go
复制代码var exampleNumA int = 10
//exampleNumA = exampleNumA + 1
exampleNumA++
fmt.Println(exampleNumA)
这段代码的运行结果为:
11
exampleNumA++与被注释的代码作用相同。
# 关系运算符
关系运算符则用来判断二者的关系,当结果与判断条件一致时,返回true,反之则返回false。例如:
Go
复制代码var exampleNumA int = 10
var exampleNumB int = 20
fmt.Println(exampleNumA <= exampleNumB)
这段代码中,最后一行的 <= 便是关系运算符之一,表示小于或等于。显然,10比20要小,因此这段代码最终将输出:
true
# 逻辑运算符
逻辑运算符通常用来将两个条件组合,获得组合后的关系,最终将输出布尔类型值。其组合方式包括与“&&”、或“||”、非“!”。使用示例如下:
Go
复制代码var exampleBoolA bool = true
var exampleBoolB bool = false
//逻辑与运算。当exampleBoolA和exampleBoolB均为true时,结果为true;其他情况均为false。
fmt.Println(exampleBoolA && exampleBoolB)
//逻辑或运算。当exampleBoolA或exampleBoolB有一个为true时,结果为true;当exampleBoolA和exampleBoolB都是false时,结果为false。
fmt.Println(exampleBoolA || exampleBoolB)
//逻辑非运算。将某个布尔类型的值取反。
fmt.Println(!exampleBoolB)
程序运行后,控制台将输出:
false true true
# 位运算符
位运算符运用在整数型变量,在进行运算时,会首先将其它进制的数值转换为二进制的数值,然后使用二进制数值进行运算,最后以原始进制类型返回计算结果。例如:
Go
复制代码//十进制7转二进制结果为0111
var exampleNumA int = 7
//十进制5转二进制结果为0101
var exampleNumB int = 5
fmt.Println(exampleNumA & exampleNumB)
在这段代码中,&就是位运算符中的一个,表示按位与运算。由注释中的内容可知,7和5的按位与运算实际上就是0111和0101的按位与运算。当前后两个数对应位的数字都是1时,计算结果对应位的数字为1,否则为0。具体请看下图:
# 赋值运算符
赋值运算符其实我们一直都在用,在为某个变量赋初值时使用的“=”便是最为简单的赋值运算符了。此外,还有一些更为简便的经过运算的赋值运算符,如:
Go
复制代码var exampleNumA int = 10
//exampleNumA = exampleNumA + 20
exampleNumA += 20
fmt.Println(exampleNumA)
在这段代码中,exampleNumA += 20与被注释掉的语句含义相同。
# 指针运算符
指针运算符包含 & 和 * 两个运算符,已经在本讲前面的“指针类型”介绍过了,有疑惑的朋友请学习“指针类型”部分。
❗️ 注意: 请大家结合本讲最后附录一的内容,亲自动手尝试每一种运算符的使用。
# 运算符的优先级
在实际开发中,通常会处理较为复杂的运算,通常会将多个变量与多种运算符一齐使用,如此便不可避免地出现先后次序地问题。
对于算术运算符,遵循数学上的运算顺序。 比如在既有乘法又有加法的情况下,会先进行乘法运算,再进行加法运算。当然,如果我们希望先进行加法运算,可以使用成对的小括号将加法部分包裹起来。这些做法在Go语言中也是相同的。
完整的运算符优先级顺序请参考文末附录一的相关内容。
💡 提示: 成对的小括号在Go语言中优先级最高,且可以嵌套使用。在日后的开发中,当不太明确运算符的优先级时,最为稳妥的方法就是加小括号来限定计算顺序。
# 类型转换
在某些特定的场景下,我们需要对数据进行类型转换才能继续后面的逻辑(如某个函数需要float64类型参数,需要将现有int64类型值传入其中时)。在Go语言中,进行类型转换有两个要注意的地方,分别是:
- 只能进行相同类别的转换,如将int32转换为int64。不同类别的转换将引发编译时错误,如将bool转换为string;
- 若将取值范围较大的类型转换为取值范围较小的类型,且实际值超过取值范围较小的类型时,将发生精度丢失的情况。
有关不同类型的取值范围,请参考上一讲 (opens new window)的附录三。
举例来说,下面的代码实现了将float32类型值转换为float64与int32类型值:
Go
复制代码//声明float32型变量exampleFloat32并赋值
var exampleFloat32 float32 = 150.25
//将exampleFloat32转换为float64类型,并将结果赋值给exampleFloat64
exampleFloat64 := float64(exampleFloat32)
//输出exampleFloat64的类型和值
fmt.Println(reflect.TypeOf(exampleFloat64), exampleFloat64)
//将exampleFloat32转换为int32类型,exampleInt32
exampleInt32 := int32(exampleFloat32)
//输出exampleInt32的类型和值
fmt.Println(reflect.TypeOf(exampleInt32), exampleInt32)
运行后,控制台如下输出:
float64 150.25 int32 150
显然,由于float64比float32的取值范围更广,因此转换后不会损失精度;由于int32不包含小数位,因此原值的.25部分被丢弃。
💡 提示: reflect.TypeOf()是使用反射获取变量类型的函数;fmt.Print``ln``()函数支持同时输出多个值,每个值之间使用英文的逗号(,)隔开。
# 小结
🎉 恭喜,您完成了本次课程的学习!
📌 以下是本次课程的重点内容总结:
- 指针类型
- 运算符及优先级
- 类型转换
借助指针,我们可以轻松且精准地直接访问和修改内存中的数据,还学会了使用new()函数创建“匿名”的变量。虽然现在看上去并没有什么具体的使用场景,在后面的数组、切片等内容中,便能体会到指针的妙用了。
有了数据,下一步便是数据的运算了。本讲介绍了Go语言中的常用运算符及它们的结合顺序(优先级),在实际开发中使用很常用。
最后,在向某些函数传递数据时,需要按照参数列表的要求传入正确的类型。此时,借助类型转换,可以将数据类型转换为所要求的类型。当然,要充分考虑数据精度丢失的风险。
学习这一讲时,我特别建议大家动手实践,尤其对于编程0基础的同学来说,练习更为重要,切勿停留在理解层面。如果有问题的话,欢迎各位随时在微信群里发问。
➡️ 在下次课程中,我们会阐述如下内容:
- Go语言基础语法之流程控制结构,包括:
- 循环结构
- 流程控制语句
- 条件分支结构
# 附录一 Go语言中的运算符、含义及优先级
# 算术运算符
算术运算符 | 含义 |
---|---|
+ | 相加 |
- | 相减 |
* | 相乘 |
/ | 相除 |
% | 求余数 |
++ | 自增1 |
-- | 自减1 |
# 关系运算符
关系运算符 | 含义 |
---|---|
== | 相等 |
!= | 不相等 |
< | 小于 |
<= | 小于或等于 |
大于 | |
> = | 大于或等于 |
# 逻辑运算符
逻辑运算符 | 含义 |
---|---|
&& | 逻辑与(AND),当运算符前后两个条件均为true时,运算结果为true |
|| | 逻辑或(OR),当运算符前后两个条件其中有一个为true时,运算结果为true |
! | 逻辑非(NOT),对运算符后面的条件结果取反,当条件结果为true时,整体运算结果为false;反之则为true。 |
# 位运算符
位运算符 | 含义 |
---|---|
& | 按位与(AND)操作,其结果是运算符前后的两数各对应的二进位相与后的结果。 |
| | 按位或(OR)操作,其结果是运算符前后的两数各对应的二进位相或后的结果。 |
按位异或(XOR)操作,当运算符前后的两数各对应的二进位相等时,返回0;反之,返回1。 | |
&^ | 按位清空(AND NOT)操作,当运算符右侧某位为1时,运算结果中的相应位值为0;反之,则为运算符左侧相应位的值。 |
<< | 按位左移操作,该操作本质上是将某个数值乘以2的n次方,n即为左移位数。更直观地来看,其结果就是将某个数值的二进制每个位向左移了n个位置。超限的高位丢弃,低位补0。 |
>> | 按位右移操作,该操作本质上是将某个数值除以2的n次方,n即为左移位数。更直观地来看,其结果就是将某个数值的二进制每个位向右移了n个位置。超限的低位丢弃,高位补0。 |
# 赋值运算符
赋值运算符 | 含义 |
---|---|
= | 直接将运算符后面的值赋给左侧。 |
+= | 先将运算符左侧的值与右侧的值相加,再将相加和赋给左侧。 |
-= | 先将运算符左侧的值与右侧的值相减,再将相减差赋给左侧。 |
*= | 先将运算符左侧的值与右侧的值相乘,再将相乘结果赋给左侧。 |
/= | 先将运算符左侧的值与右侧的值相除,再将相除结果赋给左侧。 |
%= | 先将运算符左侧的值与右侧的值相除取余数,再将余数赋给左侧。 |
<<= | 先将运算符左侧的值按位左移右侧数值个位置,再将位移后的结果赋给左侧。 |
>>= | 先将运算符左侧的值按位右移右侧数值个位置,再将位移后的结果赋给左侧。 |
&= | 先将运算符左侧的值与右侧的值按位与,再将位运算后的结果赋给左侧。 |
^= | 先将运算符左侧的值与右侧的值按位异或,再将位运算后的结果赋给左侧。 |
|= | 先将运算符左侧的值与右侧的值按位或,再将位运算后的结果赋给左侧。 |
# 指针运算符
指针运算符 | 含义 |
---|---|
& | 获取某个变量在内存中的实际地址 |
* | 用于声明一个指针变量 |
# 运算符优先级
优先级 | 运算符 |
---|---|
1 | , |
2 | =、+=、-=、*=、/=、 %=、 >=、 <<=、&=、^=、|= |
3 | || |
4 | && |
5 | | |
6 | |
7 | & |
8 | ==、!= |
9 | <、<=、>、>= |
10 | <<、>> |
11 | +、- |
12 | *(乘号)、/、% |
13 | !、*(指针)、& 、++、--、+(正号)、-(负号) |
14 | ( )、[ ]、-> |
上表中,优先级值越大,优先级越高。