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 语言字面量包括基本字面量和复合字面量。

基本字面量

基本类型字面量仅能表达基本类型的值,分为以下几类:

  • 布尔型字面量:表示逻辑值。只能为 truefalse

  • 整型字面量:表示整数,支持多种数制表示。例如:1230153210x1A3F

  • 浮点型字面量:表示小数,可以包含小数点或指数部分。例如:0.101.1035e-2.1234E+2

  • 复数型字面量:表示复数,由实部和虚部组成。例如:011i4.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):命名私有(未导出的)变量和函数时,第一个单词以小写字母开始,后续单词的首字母大写。例如:localVariablemyFunction
  • 大驼峰式命名法(UpperCamelCase):命名公共(即在包外可见,导出的)变量和函数则使用大驼峰式,即所有单词首字母都大写,也叫做 Pascal 命名法。例如:PublicVariableExportedFunction
  • 短名称: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 个)

数值类型 说明
int8int16int32int64 有符号整数
uint8uint16uint32uint64 无符号整数
intunituintptr 特殊整数型
float32float64 浮点型
complex64complex128 复数型

基础类型(4 个)

基础类型 说明
bool 布尔型
string 字符串型
rune 字符型
byte 字节型

接口型(3 个)

接口类型 说明
error 错误处理接口
any 空接口的别名
comparable 安全比较接口

内置函数

内置函数大多实现于编译器内部,具有全局可见性,不需用使用 import 引入。内置函数有 15 个:

内置函数 用途
close 用于关闭通道
len 用于计算某个类型的长度或数量
cap 用于计算切片、数组和通道的容量
makenew 用于初始化分配内存
appendcopy 用于修改和复制切片
delete 用于从映射中删除键值对
panicrecover 用于错误处理机制
printprintln 用于打印的底层函数
complexrealimag 用于创建和操作复数

在 Go 1.18 中引入了泛型概念,内置函数新增 3 例泛型函数:

泛型函数 用途
max 用于找出一组元素中最大值
min 用于找出一组元素中最小值
clear 用于清空指定切片或映射

常量值

表达特殊含义的内置常量值。共有 4 个:

常量值 用途
truefalse 表示布尔类型真和假
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")
	}
}