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