Go语言定制指南

978-7-115-58441-0
作者: 柴树杉史斌丁尔男
译者:
编辑: 刘雅思

图书目录:

详情

Go语言语法树是Go语言源文件的另一种语义等价的表现形式,Go语言自带的go fmt和go doc等命令都是建立在Go语言语法树基础之上的分析工具。本书从Go语言语法树出发,重新审视Go语言源文件,阐述定制Go语言的核心技术。书中通过对go/ast、go/ssa等包的分析,一步步深入Go语言核心,最后简要介绍LLVM,读者可以结合LLVM和Go语言语法树按需定制,创造一个语法与Go语言语法类似的简单的编程语言及与其对应的编译器,达到掌握自制编程语言和编译器的目的。

图书摘要

版权信息

书名:Go语言定制指南

ISBN:978-7-115-58441-0

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

著    柴树杉 史 斌 丁尔男

责任编辑 刘雅思

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315


Go语言语法树是Go语言源文件的另一种语义等价的表现形式,Go语言自带的go fmt和go doc等命令都是建立在Go语言语法树基础之上的分析工具。本书从Go语言语法树出发,重新审视Go语言源文件,阐述定制Go语言的核心技术。书中通过对go/ast、go/ssa等包的分析,一步步深入Go语言核心,最后简要介绍LLVM,读者可以结合LLVM和Go语言语法树按需定制,创造一个语法与Go语言语法类似的简单的编程语言及与其对应的编译器,达到掌握自制编程语言和编译器的目的。

本书面向已经熟练掌握Go语言并在进行项目开发的程序员,也适合想深入了解Go语言底层运行机制的程序员阅读,同时可作为对编程语言/编译器有兴趣并想进行实际项目实践的程序员的参考书。


The official “go/*” packages are important components of the Go programming language’s tools for analyzing Go programs. They are a core part of programs like gofmt and go vet. Understanding these packages not only improves a gopher’s programming skills, but can lead to building embedded scripts based on these packages.

Both Shushan Chai (chai2010@github) and Ben Shi (benshi001@github) are Go contributors, who have made many good commits to Go’s master branch.

This book authored by them introduces the functionalities and also analyzes the implementation of the “go/*” packages.

I recommend that Chinese gophers read it and benefit from the content. What’s more, I hope more Chinese gophers will make contributions to Go after reading it.

官方提供的“go/*”包是Go语言的重要组件,主要用于解析Go程序,同时是gofmt和go vet的核心。理解这些包不仅能让Go语言程序员提升编程技能,还能帮助他们基于这些包构建自己的嵌入式脚本。

柴树杉和史斌是Go语言的代码贡献者,他们都为Go语言贡献了高质量的代码,并融入了Go的主干分支。

他们创作的这本书介绍了“go/*”包的功能并分析了它的实现。

我向中国的Go语言开发者推荐这本书,希望你们能从这本书中获益,更希望你们读过这本书之后能为Go语言贡献精彩的代码。

(Ian Lance Taylor)

Go核心开发者,GCC核心开发者,gccgo作者


我是领域特定语言(domain specific language,DSL)的推崇者,也开发过好几种领域语言甚至通用语言,其中包括文档生成语言(类似于Doxygen)、服务描述语言(SDL)(类似于微软的IDL)、Q语言(通用脚本语言,主要用于与Go语言便捷交互)、文本处理语言(TPL)、二进制处理语言(BPL)、Go+语言(与Go语言兼容的通用静态语言,主要用于数据科学领域)。

多数开发者可能觉得创建一门编程语言离自己很遥远。但是,从泛化的角度来说,领域特定语言就在每个开发者的身边。我的第一份工作是在金山软件做文字处理、电子表格、演示三套件。其实我认为它们也是领域特定语言,Word+VBA与HTML+JavaScript并没有本质上的不同。而我们程序员使用得很多的Markdown同样是一种领域特定语言。

我们需要领域特定语言。软件的开放性往往是由领域特定语言承载的。我们需要创建新的领域特定语言,新的领域特定语言极有可能就是新的生产力。例如,人们需不需要新的动画生成语言呢?非常需要。创建这样的领域特定语言需要有很强的领域知识。一旦这些领域知识被领域特定语言固化,就会成为极强大的生产力工具。

那么,你是否想基于Go语言创建新的领域特定语言呢?本书将带你进入语言创建之旅,你可以从中寻找自己的答案。

许式伟

上海七牛信息技术有限公司首席执行官


在武侠小说中,“天下武功出少林”,少林寺的“七十二绝技”名扬天下。但是真正能学会并掌握七十二绝技的人屈指可数,主要原因是学习周期太长。以七十二绝技中的“一指禅”为例,据说五代时期的法慧禅师花费36年学成,排名第一;南宋的灵兴禅师花费39年学成,排名第二;而韦爵爷的澄观师侄花费42年学成,排名第三。如果一项绝技真的需要几十年甚至上百年的时间才能掌握,那么只能说明这项绝技没有实用价值,或者学习它的人没有掌握科学的学习方法。

其实少林寺的七十二绝技是有科学、高效的学习方法的,这个方法由《天龙八部》中的鸠摩智发现并实践。鸠摩智经过研究发现,少林寺的七十二绝技招式虽然厉害,但是其内部的“驱动引擎”性能极低(预热就需要几十年时间),而稍微强一点儿的“易筋经引擎”又涉及知识产权问题不对外开放授权,因此,如何为七十二绝技定制一个合适的“内功”驱动引擎就成了一个关键问题。经过不懈努力,鸠摩智终于发现可以将“逍遥派”的“小无相功”作为驱动七十二绝技的内功引擎,从而开辟了一条武学“弯道超车”的新捷径。

在软件开发领域同样存在几个“圣地”——数据库、操作系统和编译器,其中编译器的开发技术被称为软件开发的“屠龙之技”,号称“龙书”的《编译原理》的封面正是一个骑士在和巨龙搏斗的画面。

编译器开发的相关理论类似于武侠小说中的“内功心法”,编译器界面的编程语言类似于少林寺的七十二绝技:二者都名扬天下,但学习周期太长,真正能够熟练掌握的人屈指可数。普通程序员以传统方式从头发明或实现一整套实用的编程语言难于登天:不仅要学习涉及诸多编译方面的理论,还要通过大量的编码工作解决各种细节问题。自制编程语言爱好者不只是想要掌握“龙书”的理论,更想要有一门自己可控的编程语言,因此我们同样需要寻找一条自制编程语言的捷径。

Go语言作为一门将自身的编译器内置到标准库的主流通用编译型编程语言,其与语法树相关的包的设计与实现堪称编程艺术和编译理论相结合的典范,是“Unix之父”等老一辈软件工程师毕生的艺术结晶。Go语言的语法比较简单(只有25个关键字),非常适合作为自制编程语言的基础参考语言。开源社区已经从Go语言语法树发展出了诸多扩展语言:GopherJS项目将Go语言带入了前端开发领域,TinyGo则将Go语言带入了单片机等微系统的开发领域,国内的七牛公司针对数据科学领域定制了Go+语言。这些基于Go语言的定制语言的一个共通之处就是都基于Go语言语法树进行再加工处理。因此,只要能熟练掌握Go语言语法树的使用方法,就能跨过繁杂的词法分析、语法分析等步骤,直接使用“龙书”中的高深理论,进入语言特性定制领域。这将极大地降低自定义编程语言的门槛。

为了真正开启自制编程语言的旅程,同时让Go语言语法树真正落地产生生产力,本书最后引入了我们定制的凹(读音“wā”)语言。凹语言的定制过程类似于自己组装一台计算机,在语言能够独立工作前并不自己创造新的核心模块,而是基于已有的软件模块进行改造和拼装,最终得到的依然是自主可控的语言。在语言可以初步工作之后,可以进一步根据需求优化局部细节或者对语言的语法做局部的重新设计,这样语言的每个阶段的实现难度都不会很大。我们的目标不只是制造一门“玩具语言”——凹语言的语法树解析和语义分析都是工业级的,如果我们可以在后端接入LLVM,就很容易将其进一步改造为实用的编程语言。

随着计算机的普及,我国程序员已经在追赶并紧跟世界前沿技术的发展。但是,目前与最古老的编程语言/编译相关的技术图书还停留在讲述几十年前的理论或者讲述如何构建一些缺乏实用价值的玩具语言阶段,理论和实践严重脱节。与经典著作“龙书”相比,“龙书”深刻地讲解了编译技术用到的理论知识;而本书立足于理论与实践的结合,教授读者利用现成的工具,快速创建一门实用的编程语言。希望本书可以为我国编程语言和编译器的自主化提供力所能及的帮助。

最后,希望各位读者能够定制自己的编程语言,并使用定制的语言快乐地编程。

柴树杉

蚂蚁集团高级软件技术专家


Go语言最初由谷歌公司的罗伯特· 格瑞史莫(Robert Griesemer)、肯·汤普森(Ken Thompson)和罗勃·派克(Rob Pike)这3位“大师”于2007年设计发明,目标是打造互联网和“多核时代”的C语言。正如C语言开创了“Unix时代”一样,Go语言通过Docker和Kubernetes等知名项目开创了“云计算时代”,目前已经成为云计算从业者必须掌握的编程语言。

随着Go语言在国内的普及,它已经不仅仅是一个普通的编程工具——许多国内前沿的互联网公司已经开始尝试通过定制Go语言运行时库和编译器工具链的方式来改进和完善这个工具。本书尝试以条分缕析的方式,从Go语言语法树开始,重新组装、定制一门属于自己的语言,从而开启自制编程语言之旅。

我们的目标不是制造一门玩具语言,Go语言的语法树解析和语义分析都是工业级的,后端再接入LLVM就很容易将其改造为实用的工程语言。Go编译器本身是一个大而复杂的应用程序,其语法树相关包的设计与实现堪称编程艺术和编译理论相结合的典范,是“Unix之父”等老一辈软件工程师毕生的艺术结晶,也非常值得我们深入分析、研究。

研究Go语言语法的实现方式,是学习软件设计和实现技术的最好方法。虽然大多数程序员从事的是应用程序或其他系统程序的开发工作,并不需要了解编译器的原理与实现,但是他们依然可以从本书受益,原因有以下几个。

(1)理解语言语法树的工作原理可以提升编程技能。Go语言语法树涵盖了常见的数据结构和算法,程序员通过深入学习能够更好地掌握语言本身及基础算法在现代计算机上的高效实现,进而应用到他们未来的开发工作中。

(2)编译器是Go语言反射技术的另一种形态。通过手动方式解析语法树可以得到远超反射技术可以获取的信息,从而在编译时可以灵活地输出更高效的辅助代码,极大地释放元编程的能力。例如,通过Go语言语法树可以很容易地从Go语言的结构体中提取出Kubernetes的CRD结构。

(3)本书通过类似组装计算机的方式避免初学者从刚开始就陷入浩瀚繁杂的编译理论。本书先基于Go语言语法树快速组装出可以马上运行的凹语言,帮助读者快速理解Go语言底层的运行机制,便于后面更深刻地理解Go语言的特性。

下面简单介绍本书各章的主要内容。

本书非常适合专业技术人员自学使用,也适合在校学生用作编译原理课程的课外阅读资料。本书弥补了传统编译原理教材的不足,让枯燥无味的理论学习之路变成更有趣味的自制编程语言之旅。

读者可以尝试像Go+语言那样给Go语言语法树增加更多的特性,也可以尝试为凹语言接入LLVM等更强大、实用的后端。这是一个可以持续提升Go语言“内功”的方向,希望大家能够喜欢这门技术并从中获益。

感谢Ian Lance Taylor为本书作序,他是Go语言第4位参与者,为社区做出了巨大贡献,靠一己之力完成了gccgo。感谢许式伟为本书写推荐序,他是七牛公司的创始人和CEO,也是大中华区首席Go语言布道者。感谢“Go语言之父”和每位为Go语言提交过补丁的朋友。感谢樊虹剑(Fango),他创作了第一本以Go语言为主题的网络小说《胡文Go.ogle》和第一本中文Go语言图书《Go语言·云动力》,他的分享带动了大家学习Go语言的热情。感谢Gopher China创始人谢孟军多年来的支持。感谢国内对社区做出贡献的每位伙伴,你们的奉献让社区更加壮大。最后感谢人民邮电出版社的杨海玲编辑,没有她本书就不可能出版。谢谢大家!

下面就开启各个章节的精彩之旅,欢迎读者提出宝贵意见并反馈给作者。


本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

本书提供配套源代码,请在异步社区本书页面点击,跳转到下载界面,按提示进行操作即可。注意:为保证购书者的权益,该操作会给出相关提示,要求输入提取码进行验证。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,点击“提交勘误”,输入勘误信息,点击“提交”按钮即可。本书的作者和编辑会对您提交的勘误信息进行审核,确认并接受您的建议后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频或者参与图书技术审校等工作,可以直接发邮件给本书的责任编辑(liuyasi@ptpress.com.cn)。

如果您来自学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接通过邮件发给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端和网络技术等。

异步社区

微信服务号


丰富多彩的世界是由100多种化学元素构成的,高级编程语言程序也是由多种基本元素构成的,这些基本元素就是词法单元(token)。词法单元构成表达式(expression)和语句(statement),表达式和语句构成函数(function),函数构成文件(source file),源文件最终构成软件工程项目(project)。本章的重点是介绍程序的基本元素——词法单元。

词法单元不仅包含关键字,还包含用户自定义的标识符、运算符、分隔符和注释等。词法单元有以下3个重要属性:

在所有词法单元中,注释和分号是比较特殊的:注释一般不影响程序的语义,因此在很多情况下可以忽略;分号用于分隔语句。本章介绍如何对Go程序的源代码进行词法分析,即把源代码转换成词法单元序列,并提炼出每个词法单元的3个重要属性。

Go语言中的词法单元可分为标识符(包括关键字)、运算符和分隔符等几类,其中标识符的语法规范如下:

identifier = letter { letter | unicode_digit } .
letter     = unicode_letter | "_" .

其中identifier表示标识符,标识符由字母和数字组成,第一个字符必须是字母。需要注意的是,在Go语言定义中,下划线(_)被判定为字母,因此标识符中可以包含下划线;而美元符号($)并不被判定为字母,因此标识符中不能包含美元符号。

有一类特殊的标识符被定义为关键字,用于引导特定的语法结构。Go语言的25个关键字及其作用如表1-1所示。

表1-1 Go语言的关键字及其作用

关键字

作用

关键字

作用

break

跳出循环

default

多重分支语句的默认匹配项

func

定义函数

interface

定义接口

select

选择通信通道

case

多重分支语句匹配项

defer

登记析构代码

go

启动协程

map

字典类型

struct

定义结构体

chan

定义通信通道

else

条件分支语句

goto

跳转语句

package

定义包名

switch

多重分支语句

const

定义常量

if

条件分支语句

var

定义变量

range

遍历字典/数组/切片等复合类型

type

定义类型

continue

循环继续

for

循环语句

import

引入包

return

函数返回

fallthrough

多重分支语句匹配项间代码共享

除了标识符和关键字,词法单元还包含运算符和分隔符。下面是Go语言定义的47个符号:

+    &     +=    &=     &&    ==    !=    (    )
-    |     -=    |=     ||    <     <=    [    ]
*    ^     *=    ^=     <-    >     >=    {    }
/    <<    /=    <<=    ++    =     :=    ,    ;
%    >>    %=    >>=    --    !     ...   .    :
&^   &^=

当然,除了用户自定义的标识符、25个关键字、47个运算符和分隔符,程序中还包含其他类型的词法单元,例如一些字面值、注释和空白符。要解析一个Go语言程序,第一步就是要解析这些词法单元。

go/token包中,词法单元用枚举类型token.Token表示,不同的枚举值表示不同的词法单元:

// Token is the set of lexical tokens of the Go programming language
type Token int

所有的词法单元被分为4类,即特殊词法单元、基础字面值、运算符和关键字,如图1-1所示。

图1-1 词法单元分类

特殊词法单元有错误、文件结束和注释3种:

// The list of tokens
const (
    // Special tokens
    ILLEGAL Token = iota
    EOF
    COMMENT
    ...
)

遇到无法识别的词法单元时统一返回ILLEGAL,这样可以简化词法分析时的错误处理。

Go语言规范定义的基础字面值主要有以下几类:

需要注意的是,在Go语言规范中,布尔值truefalse并不在基础字面值之列。但是,为了方便词法解析,go/token包将truefalse等对应的标识符也作为字面值词法单元处理。

下面是字面值词法单元列表:

// The list of tokens
const (
    ...
    literal_beg
    // Identifiers and basic type literals
    // (these tokens stand for classes of literals)
    IDENT  // main
    INT    // 12345
    FLOAT  // 123.45
    IMAG   // 123.45i
    CHAR   // 'a'
    STRING // "abc"
    literal_end
    ...
)

其中,literal_begliteral_end并非实体词法单元,主要用于限定字面值类型的值域范围,因此判断词法单元枚举值是否在literal_begliteral_end之间就可以确定词法单元是否为字面值类型。

运算符和分隔符类型的词法单元数量最多,具体如表1-2所示。

表1-2 运算符和分隔符类型的词法单元的枚举值与符号

枚举值

符号

枚举值

符号

枚举值

符号

枚举值

符号

ADD

+

SUB

-

MUL

*

QUO

/

REM

%

AND

&

OR

|

XOR

^

SHL

<<

SHR

>>

AND_NOT

&^

ADD_ASSIGN

+=

SUB_ASSIGN

-=

MUL_ASSIGN

*=

QUO_ASSIGN

/=

REM_ASSIGN

%=

AND_ASSIGN

&=

OR_ASSIGN

|=

XOR_ASSIGN

^=

SHL_ASSIGN

<<=

SHR_ASSIGN

>>=

AND_NOT_ASSIGN

&^=

LAND

&&

LOR

|

ARROW

<-

INC

++

DEC

--

EQL

==

LSS

<

GTR

>

ASSIGN

=

NOT

!

NEQ

!=

LEQ

<=

GEQ

>=

DEFINE

:=

ELLIPSIS

...

LPAREN

(

LBRACK

[

LBRACE

{

COMMA

,

PERIOD

.

RPAREN

)

RBRACK

]

RBRACE

}

SEMICOLON

;

COLON

:

除算术运算符之外,运算符还包括逻辑运算符、位运算符和比较运算符等二元运算符(其中二元运算符还可以与赋值运算符再次组合),以及少量的一元运算符,如取地址符、管道读取符等。而分隔符主要包括圆括号、方括号、花括号,以及逗号、圆点、分号和冒号。

Go语言的25个关键字刚好对应25个枚举值,如表1-3所示。

表1-3 Go语言的25个关键字与对应的枚举值

枚举值

关键字

枚举值

关键字

枚举值

关键字

枚举值

关键字

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

从这一词法分析角度看,关键字和普通的标识符并无差别。但是,关键字在语法分析(后续章节介绍)中有重要作用。

词法单元对编程语言而言就像26个字母对英文一样重要,它是组成更复杂的逻辑代码的基本元素,因此我们需要熟悉词法单元的分类和属性。

在定义好词法单元之后,我们就可以手动对源代码进行简单的词法分析。不过如果希望以后能够复用词法分析的代码,则需要仔细设计和源代码相关的接口。在Go语言中,多个文件组成一个包,多个包链接为一个可执行程序;所以单个包对应的多个文件可以看作Go语言的基本编译单元。因此go/token包还定义了FileSetFile对象,用于描述文件集和文件。

FileSetFile对象的对应关系如图1-2所示。

图1-2 FileSetFile对象的对应关系

每个FileSet表示一个文件集合,底层抽象为一个一维数组,而Pos类型表示数组的索引位置。FileSet中的每个File元素对应底层数组的一个区间,不同的File之间没有交集,相邻的File之间可能存在填充空间。

每个File对象主要由文件名、basesize组成,其中base对应FileFileSet中的Pos索引位置,因此basebase+size定义了FileFileSet数组中的开始位置和结束位置。在每个File内部可以通过offset定位索引,通过offset+File.base可以将File内部的offset转换为Pos,因为PosFileSet的全局偏移量。反之也可以通过Pos查询对应的File,以及对应File内部的offset

词法分析的每个词法单元位置信息由Pos定义,通过PosFileSet可以轻松地查询到对应的File,然后通过File对应的源文件和offset计算出对应的行号和列号(实现中File只保存了每行的开始位置,并没有保存原始的源代码文本)。Pos类型底层是int类型,它和指针类型的语义类似,因此零值被定义为NoPos,表示无效的Pos,类似于空指针。

Go语言标准库go/scanner包提供了Scanner来实现词法单元扫描,它在FileSetFile抽象文件集合的基础上进行词法分析。

scanner.Scanner的公开接口定义如下:

type Scanner struct {
    // public state - ok to modify
    ErrorCount int // number of errors encountered
    // Has unexported fields
}

func (s *Scanner) Init(
    file *token.File, src []byte,
    err ErrorHandler, mode Mode,
)
func (s *Scanner) Scan() (
    pos token.Pos, tok token.Token, lit string,
)

Init方法用于初始化扫描器,其中file参数表示当前的文件(不包含代码数据),src参数表示要分析的代码,err参数表示用户自定义的错误处理函数,mode参数可以控制是否扫描注释部分。

Scan方法扫描一个词法单元,3个返回值分别表示词法单元的位置、词法单元的值和词法单元的文本表示。

要构造一个简单的词法扫描器测试程序,首先要构造Init方法的第一个参数所需的File对象。但是,File对象没有公开的构造函数,只能通过FileSetAddFile方法间接构造File对象。

下面是一个简单的词法分析程序:

package main

import (
    "fmt"
    "go/scanner"
    "go/token"
)

func main() {
    var src = []byte(`println("你好,世界")`)

    var fset = token.NewFileSet()
    var file = fset.AddFile("hello.go", fset.Base(), len(src))

    var s scanner.Scanner
    s.Init(file, src, nil, scanner.ScanComments)

    for {
        pos, tok, lit := s.Scan()
        if tok == token.EOF {
            break
        }
        fmt.Printf("%s\t%s\t%q\n", fset.Position(pos), tok, lit)
    }
}

其中,src是要分析的代码字符串。

首先通过token.NewFileSet方法创建一个文件集。这是因为词法单元的位置信息必须通过文件集定位,并且需要通过文件集创建扫描器的Init方法所需的file参数。

然后调用fset.AddFile方法向fset文件集添加一个新的文件,文件名为hello.go,文件的长度就是要分析的代码src的长度。

接着创建scanner.Scanner对象,并且调用Init方法初始化扫描器。Init方法的第一个参数file表示刚刚添加到fset的文件对象,第二个参数src表示要分析的代码,第三个参数nil表示没有自定义的错误处理函数,最后的scanner.ScanComments参数表示不忽略注释。

因为要解析的代码中有多个词法单元,所以我们在一个循环中调用s.Scan方法依次解析每个词法单元。如果返回的是token.EOF,则表示扫描到了文件末尾,否则输出扫描返回的结果。输出前,我们需要将扫描器返回的pos参数转换为更详细的带文件名和行列号的位置信息,可以通过fset.Position(pos)方法完成。

运行以上程序的输出结果如下:

hello.go:1:1    IDENT   "println"
hello.go:1:8    (        ""
hello.go:1:9    STRING  "\"你好,世界\""
hello.go:1:26   )        ""
hello.go:1:27   ;        "\n"

输出结果的第一列表示词法单元所在的文件和行列号,中间一列表示词法单元的枚举值,最后一列表示词法单元在源文件中的原始内容。

go/token包中的Position表示更详细的位置信息,它被定义为一个结构体:

type Position struct {
    Filename    string // filename, if any
    Offset      int    // offset, starting at 0
    Line        int    // line number, starting at 1
    Column      int    // column number, starting at 1 (byte count)
}

其中,Filename表示文件名,Offset对应文件内的字节偏移量(从0开始),LineColumn分别对应行列号(从1开始)。比较特殊的是Offset成员,它用于从文件数据定位代码,但是输出时会将偏移量转换为行列号输出。

输出位置信息时,根据文件名、行号和列号共有6种组合:

func main() {
    a := token.Position{Filename: "hello.go", Line: 1, Column: 2}
    b := token.Position{Filename: "hello.go", Line: 1}
    c := token.Position{Filename: "hello.go"}

    d := token.Position{Line: 1, Column: 2}
    e := token.Position{Line: 1}
    f := token.Position{Column: 2}

    fmt.Println(a.String())
    fmt.Println(b.String())
    fmt.Println(c.String())
    fmt.Println(d.String())
    fmt.Println(e.String())
    fmt.Println(f.String())
}

实际输出结果如下:

hello.go:1:2
hello.go:1
hello.go
1:2
1
-

行号从1开始,是必需的信息,如果缺少行号则输出“-”,表示无效的位置。

本章简单介绍了go/token包的用法。词法单元解析是常见编译或解释流程的第一个步骤,通过将输入的数据流转化为词法单元流,简化后续语法解析的处理流程。为了提高效率,词法单元一般被定义为整数类型,Go语言的词法单元更是通过分组的方式提高了词法单元类型判断的效率。此外,go/token包通过Pos抽象将文件的行列号位置映射为可排序的整数,不仅可以简化目标文件中符号位置信息的存储,也可以为位置和区间提供更高的二分查找性能。


字面值是在程序代码中直接表示的值。例如,表达式x+2*y中的2就是字面值。Go语言规范明确定义了基础字面值(basic literal)。需要特别注意的是,布尔值truefalse并不是普通的字面值,而是内置的布尔型标识符(可以被重新定义为其他变量)。但是,从Go语言用户的角度看,truefalse也是预定义的字面值类型,因此它们也被归为字面值(在literal_begliteral_end之间)一类。Go语言中,非零初始值只能由字面值常量或常量表达式生成。在本章中我们主要介绍基础字面值。

基础字面值有整数字面值、浮点数字面值、复数字面值、符文字面值和字符串字面值5种,同时标识符也作为字面值类型。在go/token包中,基础字面值也被定义为独立的词法单元,如图2-1所示。

图2-1 字面值类型

图2-1中没有导出的literal_begliteral_end之间的枚举值对应的词法单元都是基础字面值。

整数字面值定义如下:

int_lit        = decimal_lit | binary_lit | octal_lit | hex_lit .
decimal_lit    = "0" | ( "1" … "9" ) [ [ "_" ] decimal_digits ] .
binary_lit     = "0" ( "b" | "B" ) [ "_" ] binary_digits .
octal_lit      = "0" [ "o" | "O" ] [ "_" ] octal_digits .
hex_lit        = "0" ( "x" | "X" ) [ "_" ] hex_digits .

整数字面值分为十进制整数字面值、二进制整数字面值、八进制整数字面值和十六进制整数字面值4种。需要注意的是,整数字面值并不支持科学计数法形式,同时数字中间可以添加下划线来分隔数字。

数值型字面值中除了整数字面值,还有浮点数字面值。浮点数字面值又分为十进制浮点数字面值和十六进制浮点数字面值,它们的语法规范如下:

float_lit         = decimal_float_lit | hex_float_lit .

decimal_float_lit = decimal_digits "." [ decimal_digits ] [ decimal_exponent ] |
                    decimal_digits decimal_exponent |
                    "." decimal_digits [ decimal_exponent ] .
decimal_exponent  = ( "e" | "E" ) [ "+" | "-" ] decimal_digits .

hex_float_lit     = "0" ( "x" | "X" ) hex_mantissa hex_exponent .
hex_mantissa      = [ "_" ] hex_digits "." [ hex_digits ] |
                    [ "_" ] hex_digits |
                    "." hex_digits .
hex_exponent      = ( "p" | "P" ) [ "+" | "-" ] decimal_digits .

其中,decimal_float_lit表示十进制浮点数,又分为普通十进制和科学计数法两种表示形式。科学计数法形式的字面值中不仅有十进制形式,还有十六进制形式。十六进制浮点数字面值在C语言的C99标准中就已经存在,在C++的C++ 17标准开始支持,Java等语言也已经支持,而Go语言是在Go 1.13开始支持的。十六进制浮点数字面值的优势是可以完美配合IEEE 754定义的二进制指数的浮点数表达,使浮点数字面值和浮点数变量的值精确一致。

除了整数字面值和浮点数字面值,数值型字面值还包含复数字面值,其定义如下:

imaginary_lit = (decimal_digits | int_lit | float_lit) "i" .

复数字面值的定义比较简单,是在整数字面值或浮点数字面值后增加一个i作为后缀。例如,0i123i就分别表示将0和123转换为复数形式。

除了数值型字面值,还有符文字面值和字符串字面值,它们的定义如下:

rune_lit               = "'" ( unicode_value | byte_value ) "'" .
unicode_value          = unicode_char | little_u_value | big_u_value | escaped_char .
byte_value             = octal_byte_value | hex_byte_value .

string_lit             = raw_string_lit | interpreted_string_lit .
raw_string_lit         = "`" { unicode_char | newline } "`" .
interpreted_string_lit = `"` { unicode_value | byte_value } `"`

符文类似于一个只包含一个字符的字符串,由一对单引号(')括起来,而字符串由一对双引号(")或反引号()括起来,其中可以包含多个字符,但是不能跨行。普通的符文和字符串都可以包含由反斜杠(`)引导的特殊符号。用反引号括起来的字符串表示原生字符串,它可以跨多行但不支持转义字符,因此其内部是无法表示反引号这个字符的。

Go语言的抽象语法树(abstract syntax tree,AST)由go/ast包定义。go/ast包中的ast.BasicLit结构体表示一个基础字面值常量,它的定义如下:

type BasicLit struct {
    ValuePos    token.Pos   // literal position
    Kind         token.Token // token.INT, token.FLOAT, token.IMAG, token.CHAR, or token.STRING
    Value        string      // literal string; e.g. 42, 0x7f, 3.14, 1e-9, 2.4i, 'a', '\x7f',
"foo" or `\m\n\o`
}

其中,ValuePos表示该词法单元开始的字节偏移量(并不包含文件名、行号和列号等信息),Kind表示字面值的类型(只有数值类型、字符和字符串这3类),Value表示字面值的原始代码。

在了解了基础字面值的语法树结构之后,我们可以手动构造简单的基础字面值。例如,用下面的代码构造一个整数9527的字面值:

package main

import (
    "go/ast"
    "go/token"
)

func main() {
    var lit9527 = &ast.BasicLit{
        Kind:  token.INT,
        Value: "9527",
    }
    ast.Print(nil, lit9527)
}

其中,token.INT表示基础字面值的类型是整数类型,值用整数的十进制字符串表示。如果把token.INT改为token.FLOAT则变成浮点数的9527,如果改成token.STRING则变成字符串字面值"9527"

在前面的例子中,我们通过ast.BasicLit结构体直接构造了字面值。手动构造ast.BasicLit甚至是完整的语法树都是可以的,从理论上说,可以为任何Go语言程序手动构造等价的语法树结构。但手动构造语法树毕竟太烦琐,好在Go语言的go/parser包可以帮我们解析Go语言代码并自动构造语法树。

下面的例子通过parser.ParseExpr函数从输入的十进制数9527生成ast.BasicLit结构体:

func main() {
    expr, _ := parser.ParseExpr(`9527`)
    ast.Print(nil, expr)
}

go/parser包提供了parser.ParseExpr函数用于简化表达式的解析,返回ast.Expr类型的expr和一个错误,expr为表达式的语法树。然后通过go/ast包提供的ast.Print函数输出语法树。

输出结果如下:

0  *ast.BasicLit {
1  .  ValuePos: 1
2  .  Kind: INT
3  .  Value: "9527"
4  }

也可以解析字符串字面值"9527"

func main() {
    expr, _ := parser.ParseExpr(`"9527"`)
    ast.Print(nil, expr)
}

输出结果如下:

0  *ast.BasicLit {
1  .   ValuePos: 1
2  .   Kind: STRING
3  .   Value: "\"9527\""
4  }

基础字面值在语法树中一定是以叶节点的形式存在的,在递归遍历语法树时遇到基础字面值节点递归就会返回。同时,通过基础字面值、指针、结构体、数组和映射(map)等其他语法结构的相互嵌套和组合就可以构造出无穷无尽的复合类型。

类似于基础字面值类型,go/ast包定义了ast.Ident结构体,用于表示标识符类型:

type Ident struct {
    NamePos    token.Pos // identifier position
    Name        string     // identifier name
    Obj         *Object    // denoted object; or nil
}

其中,NamePos表示标识符的位置,Name表示标识符的名字,Obj表示标识符的类型(用于获取其扩展信息)。对内置的标识符字面值来说,我们主要关注标识符的名字即可。

go/ast包同时提供了NewIdent函数,用于创建简单的标识符:

func main() {
    ast.Print(nil, ast.NewIdent(`x`))
}

输出结果如下:

0  *ast.Ident {
1  .  NamePos: 0
2  .  Name: "x"
3  }

如果从表达式解析标识符,则会通过Obj成员描述标识符额外的信息:

func main() {
    expr, _ := parser.ParseExpr(`x`)
    ast.Print(nil, expr)
}

输出表达式中x标识符信息如下:

0  *ast.Ident {
1  .  NamePos: 1
2  .  Name: "x"
3  .  Obj: *ast.Object {
4  .  .  Kind: bad
5  .  .  Name: ""
6  .  }
7  }

ast.Object是一个相对复杂的结构,其中Kind用于描述标识符的类型:

const (
    Bad ObjKind = iota // for error handling
    Pkg                   // package
    Con                   // constant
    Typ                   // type
    Var                   // variable
    Fun                   // function or method
    Lbl                   // label
)

其中,Bad表示未知的类型,其他的分别对应Go语言中的包、常量、类型、变量、函数和标号等语法结构。标识符中更具体的类型(例如是整型还是布尔型)则由ast.Object的其他成员描述。

本章介绍了Go语言字面值对应的ast.BasicLit结构体,其中包含字符串形式表示的字面值的原始形式,也包含语法树节点对应的位置信息和字面值对应的类型。此外,还介绍了用于绑定值的标识符ast.Ident结构体。字面值对应编译时的常量,Go语言中所有与const相关的值均需要通过ast.BasicLitast.Ident结构体产生。第3章将介绍表达式的结构。


相关图书

Rust游戏开发实战
Rust游戏开发实战
仓颉编程快速上手
仓颉编程快速上手
深入浅出Go语言编程从原理解析到实战进阶
深入浅出Go语言编程从原理解析到实战进阶
Go语言编程指南
Go语言编程指南
JUnit实战(第3版)
JUnit实战(第3版)
Scala速学版(第3版)
Scala速学版(第3版)

相关文章

相关课程