修养》:

文章插图
词法分析
通过前面的例子,我们知道,Go 程序文件在机器看来不过是一堆二进制位 。我们能读懂,是因为 Goland 按照 ASCII 码(实际上是 UTF-8)把这堆二进制位进行了编码 。例如,把 8个 bit 位分成一组,对应一个字符,通过对照 ASCII 码表就可以查出来 。
当把所有的二进制位都对应成了 ASCII 码字符后,我们就能看到有意义的字符串 。它可能是关键字,例如:package;可能是字符串,例如:“Hello World” 。
词法分析其实干的就是这个 。输入是原始的 Go 程序文件,在词法分析器看来,就是一堆二进制位,根本不知道是什么东西,经过它的分析后,变成有意义的记号 。简单来说,词法分析是计算机科学中将字符序列转换为标记(token)序列的过程 。
我们来看一下维基百科上给出的定义:
【万字长文详解 Go 程序是怎样跑起来的?】词法分析(lexical analysis)是计算机科学中将字符序列转换为标记(token)序列的过程 。进行词法分析的程序或者函数叫作词法分析器(lexical analyzer,简称lexer),也叫扫描器(scanner) 。词法分析器一般以函数的形式存在,供语法分析器调用 。
.go 文件被输入到扫描器(Scanner),它使用一种类似于 有限状态机的算法,将源代码的字符系列分割成一系列的记号(Token) 。
记号一般分为这几类:关键字、标识符、字面量(包含数字、字符串)、特殊符号(如加号、等号) 。
例如,对于如下的代码:
slice[i] = i * (2 + 6)总共包含 16 个非空字符,经过扫描后:记号类型 slice标识符[左方括号i标识符]右方括号=赋值i标识符*乘号(左圆括号2数字+加号6数字)右圆括号 上面的例子源自《程序员的自我修养》,主要讲解编译、链接相关的内容,很精彩,推荐研读 。
Go 语言(本文的 Go 版本是 1.9.2)扫描器支持的 Token 在源码中的路径:
src/cmd/compile/internal/syntax/token.go感受一下:var tokstrings = [...]string{ // source control_EOF: "EOF",// names and literals_Name: "name",_Literal: "literal",// operators and operations_Operator: "op",_AssignOp: "op=",_IncOp: "opop",_Assign: "=",_Define: ":=",_Arrow: "<-",_Star: "*",// delimitors_Lparen: "(",_Lbrack: "[",_Lbrace: "{",_Rparen: ")",_Rbrack: "]",_Rbrace: "}",_Comma: ",",_Semi: ";",_Colon: ":",_Dot: ".",_DotDotDot: "...",// keywords_Break: "break",_Case: "case",_Chan: "chan",_Const: "const",_Continue: "continue",_Default: "default",_Defer: "defer",_Else: "else",_Fallthrough: "fallthrough",_For: "for",_Func: "func",_Go: "go",_Goto: "goto",_If: "if",_Import: "import",_Interface: "interface",_Map: "map",_Package: "package",_Range: "range",_Return: "return",_Select: "select",_Struct: "struct",_Switch: "switch",_Type: "type",_Var: "var",}还是比较熟悉的,包括名称和字面量、操作符、分隔符和关键字 。而扫描器的路径是:
src/cmd/compile/internal/syntax/scanner.go其中最关键的函数就是 next 函数,它不断地读取下一个字符(不是下一个字节,因为 Go 语言支持 Unicode 编码,并不是像我们前面举得 ASCII 码的例子,一个字符只有一个字节),直到这些字符可以构成一个 Token 。func (s *scanner) next{// ……redo:// skip white spacec := s.getrfor c == ' ' || c == 't' || c == 'n' && !nlsemi || c == 'r' {c = s.getr}// token starts.line, s.col = s.source.line0, s.source.col0if isLetter(c) || c >= utf8.RuneSelf && s.isIdentRune(c, true) {s.identreturn}switch c {// ……case 'n':s.lit = "newline"s.tok = _Semicase '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':s.number(c)// ……default:s.tok = 0s.error(fmt.Sprintf("invalid character %#U", c))goto redoreturnassignop:if c == '=' {s.tok = _AssignOpreturn}s.ungetrs.tok = _Operator}代码的主要逻辑就是通过 c:=s.getr 获取下一个未被解析的字符,并且会跳过之后的空格、回车、换行、tab 字符,然后进入一个大的 switch-case 语句,匹配各种不同的情形,最终可以解析出一个 Token,并且把相关的行、列数字记录下来,这样就完成一次解析过程 。
推荐阅读
- 详解3种区别Linux服务器是物理机或者虚拟机的方法
- 详解Docker可视化管理工具shipyard--部署教程及功能展示
- 绝对经典,看了必会 linux中部署mysql主从同步示例详解
- 详解Oracle数据库物理设计--表和索引设计建议
- 详解微服务技术中进程间通信
- ”什么是内网穿透“详解
- 局域网中NAT具体工作过程详解
- MySQL执行计划命令EXPLAIN详解
- 静态路由详解
- 计算机启动过程详解
