Go 语言词法单元
词法单元
词法分析是代码编译过程第一阶段,负责将源码转换为一系列词法单元,为后续语法分析阶段提供输入。Go 语言核心词法单元分为:字面量(Literals)、标识符(Identifiers)、操作符(Operators)和分隔符(Delimiters)。
假设有以下代码:
package main
import "fmt"
func main() {
// Prints "Hello, world!"
fmt.Println("Hello, world!")
}
词法分析器将上述代码分解为以下词法单元:
- 关键字:
package
,import
,func
- 标识符:
main
,fmt
,Println
- 字符串字面量:
"fmt"
,"Hello, world!"
- 运算符和分隔符:
(
,)
,{
,}
,"
,;
- 注释:
// Prints "Hello, world!"
字面量
字面量用来表示固定值,可以出现在两个地方:一是用于常量和变量初始化,二是用于表达式或作为函数实参。Go 语言字面量包括基本字面量和复合字面量。
基本字面量
基本类型字面量仅能表达基本类型的值,分为以下几类:
-
布尔型字面量:表示逻辑值。只能为
true
或false
。 -
整型字面量:表示整数,支持多种数制表示。例如:
123
、015321
、0x1A3F
。 -
浮点型字面量:表示小数,可以包含小数点或指数部分。例如:
0.1
、01.10
、35e-2
、.1234E+2
。 -
复数型字面量:表示复数,由实部和虚部组成。例如:
011i
、4.1e-18i
。 -
字符型字面量:表示 Unicode 码点,用单引号括起来的单个字符。例如:
'啊'
、'\t'
、'\u12e3'
。 -
字符串字面量:表示普通字符串或原始字符串。例如:
"Hello World"
、"你好\n,\"世界\""
。
复合字面量
复合类型字面量用于初始化数组、切片、映射、结构体等更复杂的数据结构。例如:[3]int{1, 2, 3}
、map[string]int{"one": 1, "two": 2}
。
标识符
高级语言通过一个标识符绑定一块特定内存,后续用标识符来替代指向内存进行操作。在 Go 语言中,标识符用于给变量、类型、函数、包等对象命名。标识符分为两类:
- 预定义标识符:Go 语言预定义了一些标识符,包括关键字和内置标识符(定义在
builtin.go
中)。 - 用户定义标识符:由用户创建的标识符,可以用来命名变量、常量、函数等。
创建标识符的过程也叫声明(Declaration)。
用户定义标识符
用户定义标识符基本规则:
- 必须以字母或下划线开头。
- 可以包含任意数量字母、数字(0-9)和下划线,不支持其他字符。
- 区分大小写。
- 不能使用内置关键字和保留字。
- 在同一个代码块中,标识符必须唯一。
- 标识符可见性由其命名决定,大写字母开头表示可导出的,小写字母开头表示不可导出的。
标识符命名风格约定:
- 小驼峰式命名法(lowerCamelCase):命名私有(未导出的)变量和函数时,第一个单词以小写字母开始,后续单词的首字母大写。例如:
localVariable
、myFunction
。 - 大驼峰式命名法(UpperCamelCase):命名公共(即在包外可见,导出的)变量和函数则使用大驼峰式,即所有单词首字母都大写,也叫做 Pascal 命名法。例如:
PublicVariable
、ExportedFunction
。 - 短名称:Go 语言推荐使用简短但描述性强的命名,标识符作用域范围越小,名称越短。只有作用域很大时,才使用更长且更有意义的名称。例如,使用
i
作为循环索引,err
表示错误。 - 避免下划线:在 Go 中,一般不使用下划线来分隔单词。
- 避免缩写:除非该缩写是广泛认可的专有名词,例如用
HTTP
代替HyperTextTransferProtocol
。其他情况下不要使用缩写。
关键字
关键字是有特定语法含义的保留词,总共有 25 个。
声明关键字(4 个):
关键字 | 用途 |
---|---|
var |
声明变量 |
const |
声明常量 |
type |
声明类型 |
func |
声明函数 |
流程控制关键字(11 个):
关键字 | 用途 |
---|---|
if |
条件语句 |
else |
条件语句中否则 |
for |
循环控制 |
range |
用于迭代数组、切片、字符串、映射或通道 |
switch |
选择语句 |
case |
选择语句中分支 |
default |
选择语句中默认分支 |
fallthrough |
选择语句中进入下一个分支 |
break |
中断当前循环语句 |
continue |
跳过本次循环 |
goto |
无条件跳转到标签 |
类型控制关键字(4 个):
关键字 | 用途 |
---|---|
struct |
定义结构体类型 |
interface |
定义接口类型 |
map |
定义映射类型 |
chan |
定义通道类型 |
函数控制关键字(4 个):
关键字 | 用途 |
---|---|
return |
从函数返回 |
defer |
延迟调用函数 |
go |
启动新协程 |
select |
用于通道多路复用 |
包管理关键字(2 个):
关键字 | 用途 |
---|---|
package |
定义包名 |
import |
导入包 |
数据类型
Go 是强类型(静态编译型)语言,在定义新类型或函数时,必须显式地带上数据类型标识符。预声明数据类型标识符有 22 个。
数值类型(15 个):
数值类型 | 说明 |
---|---|
int8 、int16 、int32 、int64 |
有符号整数 |
uint8 、uint16 、uint32 、uint64 |
无符号整数 |
int 、unit 、uintptr |
特殊整数型 |
float32 、float64 |
浮点型 |
complex64 、complex128 |
复数型 |
基础类型(4 个):
基础类型 | 说明 |
---|---|
bool |
布尔型 |
string |
字符串型 |
rune |
字符型 |
byte |
字节型 |
接口型(3 个):
接口类型 | 说明 |
---|---|
error |
错误处理接口 |
any |
空接口的别名 |
comparable |
安全比较接口 |
内置函数
内置函数大多实现于编译器内部,具有全局可见性,不需用使用 import
引入。内置函数有 15 个:
内置函数 | 用途 |
---|---|
close |
用于关闭通道 |
len |
用于计算某个类型的长度或数量 |
cap |
用于计算切片、数组和通道的容量 |
make 、new |
用于初始化分配内存 |
append 、copy |
用于修改和复制切片 |
delete |
用于从映射中删除键值对 |
panic 、recover |
用于错误处理机制 |
print 、println |
用于打印的底层函数 |
complex 、real 、imag |
用于创建和操作复数 |
在 Go 1.18 中引入了泛型概念,内置函数新增 3 例泛型函数:
泛型函数 | 用途 |
---|---|
max |
用于找出一组元素中最大值 |
min |
用于找出一组元素中最小值 |
clear |
用于清空指定切片或映射 |
常量值
表达特殊含义的内置常量值。共有 4 个:
常量值 | 用途 |
---|---|
true 、false |
表示布尔类型真和假 |
iota |
用在枚举类型声明中 |
nil |
指针或引用类型默认值 |
空白标识符
空白标识符只有 1 个,为下划线「_」,也叫匿名变量。匿名变量用于在各种情况下忽略值,例如忽略函数某个返回值、忽略导入包、范围语句中忽略索引或键,避免创建无用临时变量:
package main
import (
"fmt"
_ "strings" // 忽略导入
)
func main() {
// 忽略函数返回值
n, _ := fmt.Println(100)
// 忽略循环索引
for _, v := range []int{n} {
fmt.Println(v)
}
}
匿名变量只能被赋值,不占用命名空间,也不会分配内存空间,无法读取内容。
操作符
操作符用于进行各种运算和操作,包括算术运算符、比较运算符、逻辑运算符、位运算符、赋值操作符和其他特殊操作符。
操作符列表
在 Go 语言中,操作符和运算符经常被用来描述相同概念,这里不做区分,统一视作操作符。
算术运算符(5 个):都为二元算术运算符。
符号 | 说明 |
---|---|
+ |
相加(求和),操作数可以是字符串 |
- |
相减(求差) |
* |
相乘(求积) |
/ |
相除(求商),分母不能为 0,操作数为整数结果为整数,否则为浮点数 |
% |
取模(求余),操作数必须为整数,结果为整数 |
位运算符(6 个):都为二元位运算符,操作数为整数或字节型。
符号 | 说明 |
---|---|
| | 按位或操作 |
& |
按位与操作 |
^ |
按位异或操作。同时也是一元位符,表示按位取补码操作 |
<< |
按位左移操作。x<<n 等于 x*(2^n) |
>> |
按位右移操作。x>>n 等于 x/(2^n) ,向下取整 |
&^ |
按位清除操作(AND NOT) |
比较运算符(6 个):都为二元逻辑运算符。
符号 | 说明 |
---|---|
== |
等于判断 |
!= |
不等判断 |
< |
小于判断 |
<= |
小于等于判断 |
> |
大于判断 |
>= |
大于等于判断 |
逻辑运算符(3 个):操作元素都是布尔类型。
符号 | 说明 |
---|---|
|| | 逻辑或,二元逻辑运算符 |
&& |
逻辑与,二元逻辑运算符 |
! |
逻辑非,一元逻辑运算符,反转操作数逻辑状态 |
赋值和赋值复核运算符(13 个):
符号 | 说明 |
---|---|
= |
简单赋值运算符,用于给变量赋值 |
:= |
短变量声明操作符,用于声明并初始化局部变量 |
+= |
加法赋值运算符。将右侧操作数加到左侧操作数上,然后将结果赋值给左侧操作数 |
-= |
减法赋值运算符。从左侧操作数减去右侧操作数,然后将结果赋值给左侧操作数 |
*= |
乘法赋值运算符。将左侧操作数与右侧操作数相乘,然后将结果赋值给左侧操作数 |
/= |
除法赋值运算符。将左侧操作数除以右侧操作数,然后将结果赋值给左侧操作数 |
%= |
取模赋值运算符。将左侧操作数对右侧操作数取模,然后将结果赋值给左侧操作数 |
&= |
按位与赋值运算符。对左右两侧的操作数进行按位与操作,然后将结果赋值给左侧操作数 |
|= | 按位或赋值运算符。对左右两侧的操作数进行按位或操作,然后将结果赋值给左侧操作数 |
^= |
按位异或赋值运算符。对左右两侧的操作数进行按位异或操作,然后将结果赋值给左侧操作数 |
&^= |
按位清除赋值运算符。将左侧操作数的位中,对应右侧操作数位为 1 的部分设置为 0,然后将结果赋值给左侧操作数 |
>>= |
右移赋值运算符。将左侧操作数向右移动右侧操作数指定的位数,然后将结果赋值给左侧操作数 |
<<= |
左移赋值运算符。将左侧操作数向左移动右侧操作数指定的位数,然后将结果赋值给左侧操作数 |
自增自减操作符(2 个):属于语句而非表达式,没有运算符优先级。
符号 | 说明 |
---|---|
++ |
a++ 等同于 a = a + 1 。不支持前置增量操作 ++a |
-- |
a-- 等同于 a = a - 1 。不支持前置减量操作 --a |
其他操作符(3 个):&
和 *
同时也是一元地址运算符。
符号 | 说明 |
---|---|
<- |
用于通道中发送和接收值 |
& |
取地址操作符,用于获取变量的内存地址 |
* |
指针解引用操作符,用于获取指针指向变量的值 |
运算符规则
一元运算符优先级比二元运算符高。二元运算符优先级如下:
运算符 | 优先级 |
---|---|
* / % << >> & &^ |
最高 |
+ - | ^ |
较高 |
== != < <= > >= |
正常 |
&& |
较低 |
|| | 最低 |
其他运算符规则:
- 如果表达式中出现相同优先级的运算符,按照从左到右顺序依次操作;
- 所有运算符都受括号影响,括号内先操作。在复杂表达式中,为避免优先级模糊,可应用括号来明确操作顺序;
- Go 语言不支持运算符重载。例如大数类型,需要使用专用方法来执行加法运算。
分隔符
分隔符用于界定语句结构,帮助组织代码块、参数列表、数组元素等。Go 语言使用的分隔符可以分为两大类:
- 操作符:在 Go 语言中,操作符也充当分隔符。例如,在表达式
sum := a + b
中,:=
和+
同时扮演操作符和分隔符角色,将语句分割成五个独立基本元素。 - 语法符号:包括圆括号、方括号、花括号、点、逗号、分号和冒号,每个都有特定用途。
语句分隔符
在 Go 语言中,分号 ;
由编译器在扫描源代码过程中,作为语句结束符在每行结束时自动插入。通常仅在流程控制语句中使用分号,用来分隔初始化语句、条件表达式语句或步进表达式语句:
package main
import "fmt"
func main() {
// 用于在一行内写多条语句,不过一般会被自动格式化掉
a := 1; b := 2
// 用于流程控制语句分隔子语句
for i := 0; i < a+b; i++ {
fmt.Println(i)
}
}
访问分隔符
点号 .
用于访问结构体字段或调用对象方法,以及引用包中的具体函数、变量或类型:
package main
import "fmt"
type Person struct {
Name string
}
func (p Person) SayHello() {
// 引用包函数和结构体字段
fmt.Println("Hello,", p.Name)
}
func main() {
p := Person{Name: "John"}
// 调用对象方法
p.SayHello()
}
元素分隔符
逗号 ,
用于分隔同一行中的多个声明或调用参数。此外也用于分隔数组、切片或字典的元素:
package main
import "fmt"
func main() {
// 分隔多个元素
data := []string{"a", "b", "c"}
// 分隔多个声明变量
var a, b, c int
// 分隔多个函数参数
fmt.Println(a, b, c, data)
}
结构分隔符
结构分隔符指 Go 语言中三种括号:
- 圆括号
()
:用于分组表达式,封装函数调用、参数和返回类型,以及控制运算顺序。 - 方括号
[]
:用于指定数组和切片长度,访问数组和切片元素,以及声明映射类型。 - 花括号
{}
:用于定义复合类型的字面值,以及定义代码块。
下面代码应用这三种括号:
package main
import "fmt"
func f(x int, y int) (int, int) {
if x > y {
return x + y, (x - y) * (x + y)
}
return x + y, (y - x) * (x + y)
}
func main() {
var (
a = [5]int{1, 2, 3, 4, 5}
s = a[1:3]
m = map[string][]int{"key": s}
)
fmt.Println(f(a[0], m["key"][1]))
}
冒号分隔符
冒号 :
在 Go 语言中多个不同用途:
- 短变量声明:用于局部变量声明与初始化。
- 切片操作:用于切片时定义起始和结束索引。
- 映射操作:用于映射类型中分隔键和值。
- 选择语句:用于
case
语句后。
冒号这些用法都涉及到某种形式的分隔或赋值操作:
package main
import "fmt"
func main() {
// 映射中分隔键和值
m := map[string][]int{"k": {1, 2, 3}}
// 短变量声明切片中定义范围
s := m["k"][:2]
// 选择语句中使用
switch s[0] {
case 1:
fmt.Println("First element is 1")
}
}