本节继续介绍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。具体请看下图:

image-20220118072931563.png

# 赋值运算符

赋值运算符其实我们一直都在用,在为某个变量赋初值时使用的“=”便是最为简单的赋值运算符了。此外,还有一些更为简便的经过运算的赋值运算符,如:

Go
复制代码var exampleNumA int = 10
//exampleNumA = exampleNumA + 20
exampleNumA += 20
fmt.Println(exampleNumA)

在这段代码中,exampleNumA += 20与被注释掉的语句含义相同。

# 指针运算符

指针运算符包含 & 和 * 两个运算符,已经在本讲前面的“指针类型”介绍过了,有疑惑的朋友请学习“指针类型”部分。

❗️ 注意: 请大家结合本讲最后附录一的内容,亲自动手尝试每一种运算符的使用。

# 运算符的优先级

在实际开发中,通常会处理较为复杂的运算,通常会将多个变量与多种运算符一齐使用,如此便不可避免地出现先后次序地问题。

对于算术运算符,遵循数学上的运算顺序。 比如在既有乘法又有加法的情况下,会先进行乘法运算,再进行加法运算。当然,如果我们希望先进行加法运算,可以使用成对的小括号将加法部分包裹起来。这些做法在Go语言中也是相同的。

完整的运算符优先级顺序请参考文末附录一的相关内容。

💡 提示: 成对的小括号在Go语言中优先级最高,且可以嵌套使用。在日后的开发中,当不太明确运算符的优先级时,最为稳妥的方法就是加小括号来限定计算顺序。

# 类型转换

在某些特定的场景下,我们需要对数据进行类型转换才能继续后面的逻辑(如某个函数需要float64类型参数,需要将现有int64类型值传入其中时)。在Go语言中,进行类型转换有两个要注意的地方,分别是:

  1. 只能进行相同类别的转换,如将int32转换为int64。不同类别的转换将引发编译时错误,如将bool转换为string;
  2. 若将取值范围较大的类型转换为取值范围较小的类型,且实际值超过取值范围较小的类型时,将发生精度丢失的情况。

有关不同类型的取值范围,请参考上一讲 (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``()函数支持同时输出多个值,每个值之间使用英文的逗号(,)隔开。

# 小结

🎉 恭喜,您完成了本次课程的学习!

📌 以下是本次课程的重点内容总结:

  1. 指针类型
  2. 运算符及优先级
  3. 类型转换

借助指针,我们可以轻松且精准地直接访问和修改内存中的数据,还学会了使用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 ( )、[ ]、->

上表中,优先级值越大,优先级越高。