Go 语言开发(用Go从零写编程语言!普通人也能搞定,4个阶段打通编译器)

Go 语言开发(用Go从零写编程语言!普通人也能搞定,4个阶段打通编译器)
用Go从零写编程语言!普通人也能搞定,4个阶段打通编译器



一、别再怕“编译器”!普通人用Go从零造语言,打破技术神话

提到Lexer(词法分析器)、AST(抽象语法树)、编译器流水线,很多程序员都会下意识退缩——总觉得这是编程大佬、语言研究者的专属领域,普通人连入门都难。但有一位开发者,仅凭Go语言、一个空白文件夹和一份好奇心,没靠任何框架、没用地任何编译器生成器,花了几周时间就造出了一款能正常运行的编程语言Bolt。

他的成功,不仅打破了“造语言=高难度”的固有认知,更给无数被底层技术劝退的程序员泼了一盆“清醒剂”:所谓的编译器核心,从来不是遥不可及的理论,而是可拆解、可落地的具体步骤。但很多人会疑惑:从零造语言真的这么简单?普通人能复刻这份成功吗?它背后的技术逻辑,到底藏着哪些普通人能复用的底层思维?

先给大家说清关键技术细节:这款名为Bolt的编程语言,是完全开源免费的,目前在GitHub上的星标数量稳步攀升(截至发文,已突破千星),任何人都可以下载源码、复刻开发过程,无需支付任何费用,也没有技术授权门槛——这也是它最吸引人的地方:不藏私、可落地,普通人只要掌握基础Go语法,就能跟着一步步搭建属于自己的简单编程语言。

二、核心拆解:4个阶段,手把手复刻Bolt语言搭建全过程

Bolt虽然小巧,但功能齐全,支持变量、算术表达式、条件判断、函数、闭包,还有交互式REPL(读取-求值-输出-循环),完全能满足基础编程需求。而它的搭建全过程,核心就4个阶段,每一步都有明确的目标和操作方法,跟着做就能落地。

先看Bolt语言实操效果,直观感受它的功能

在动手搭建前,先看看最终成果,更有动力推进:Bolt的语法和JavaScript、Go相似,简单易懂,比如:

// 变量定义与算术运算let x = 10 + 5;let y = x * 2;// 条件判断if (y > 20) {    return y;}// 函数定义与调用let add = fn(a, b) {    return a + b;};add(3, 4);   // 运行结果:7

更强大的是,它支持闭包——函数能捕获定义时所在作用域的变量,即使外层函数执行完毕,内层函数依然能调用这个变量,比如:

let makeAdder = fn(x) {    fn(y) { x + y; } // 内层函数捕获外层x的值};let addFive = makeAdder(5);addFive(3);   // 运行结果:8

了解完效果,就进入核心步骤,每一步都附具体实现逻辑和关键代码,普通人也能看懂、能复用。

阶段1:Lexer(词法分析器)——把源代码“拆成”可识别的最小单元

我们写的代码,计算机无法直接理解,第一步就是让程序“读懂”每一个字符的含义。Lexer的作用,就是读取原始源代码,把它拆分成一个个有明确分类的“令牌”(tokens),这些令牌就是计算机能识别的最小单元,比如标识符、数字、运算符、关键字。

举个例子,代码“let x = 10 + 5;”,经过Lexer处理后,会变成这样的令牌序列:

[LET] [IDENT:x] [ASSIGN] [INT:10] [PLUS] [INT:5] [SEMICOLON]

这里要注意,此时程序还不懂语法和结构,只是单纯“分类标注”。Bolt的Lexer实现很简单,用两个指针扫描输入字符串:一个指针跟踪当前处理的字符,另一个指针“窥探”下一个字符,这样就能轻松识别“==”“!=”这类多字符运算符——比如看到“=”,就检查下一个字符是不是“=”,再决定生成哪种令牌。

关键代码(Go语言实现):

// 定义令牌类型type TokenType string// 定义令牌结构,包含类型和原始文本type Token struct {    Type    TokenType    Literal string}// 常见令牌类型const (    LET        TokenType = "LET"        // 关键字let    IDENT      TokenType = "IDENT"      // 标识符(变量名、函数名)    ASSIGN     TokenType = "ASSIGN"     // 赋值运算符=    INT        TokenType = "INT"        // 整数    PLUS       TokenType = "PLUS"       // 加法运算符+    SEMICOLON  TokenType = "SEMICOLON"  // 分号;    // 可根据需求补充其他令牌类型(如==、!=、IF、FN等))// Lexer核心逻辑:扫描输入字符串,生成令牌流func (l *Lexer) NextToken() Token {    var tok Token    l.skipWhitespace() // 跳过空格、换行等空白字符    switch l.currentChar {    case '=':        // 检查下一个字符,判断是=还是==        if l.peekChar() == '=' {            currentChar := l.currentChar            l.readChar()            tok = Token{Type: EQ, Literal: string(currentChar) + string(l.currentChar)}        } else {            tok = newToken(ASSIGN, l.currentChar)        }    case '+':        tok = newToken(PLUS, l.currentChar)    case ';':        tok = newToken(SEMICOLON, l.currentChar)    // 补充其他字符的处理逻辑(如数字、字母、其他运算符)    default:        if isLetter(l.currentChar) {            // 处理标识符或关键字(如let、fn、if)            tok.Literal = l.readIdentifier()            tok.Type = LookupIdent(tok.Literal) // 判断是否为关键字            return tok        } else if isDigit(l.currentChar) {            // 处理整数            tok.Type = INT            tok.Literal = l.readNumber()            return tok        } else {            tok = newToken(ILLEGAL, l.currentChar)        }    }    l.readChar()    return tok}

核心要点:Lexer的核心是“模式识别”,只要能区分关键字、标识符、运算符、数字,就能完成这一步,无需复杂的算法。

阶段2:Parser(语法分析器)——用AST搭建程序的“逻辑骨架”

Lexer生成的令牌流,只是一堆“零散零件”,还没有结构。Parser的作用,就是分析这些令牌的顺序,按照语法规则,构建出一棵Abstract Syntax Tree(抽象语法树,简称AST),这棵树就是程序的逻辑骨架,能体现代码的执行逻辑和层级关系。

最关键的一点:Parser要理解“运算符优先级”。比如表达式“2 + 3 * 4”,计算机必须知道先算乘法再算加法,也就是“2 + (3 * 4)”,而不是“(2 + 3) * 4”,这就是AST的作用——通过层级结构,明确执行顺序。

举个例子,代码“let x = 10 + 5;”,生成的AST结构如下(概念示意):

LetStatement(变量声明语句) ├── Name: Identifier("x")(变量名x) └── Value: InfixExpression(中缀表达式) ├── Left: IntegerLiteral(10)(左边 operand:10) ├── Operator: "+"(运算符+) └── Right: IntegerLiteral(5)(右边 operand:5)

Bolt的Parser采用“Pratt解析法”(也叫自上而下运算符优先级解析法),这种方法的核心是给每个令牌分配两种行为:前缀行为(令牌作为表达式开头时)和中缀行为(令牌在两个表达式之间时),通过递归解析,自动按照运算符优先级构建AST。

关键代码(Go语言实现,核心逻辑):

Go 语言开发(用Go从零写编程语言!普通人也能搞定,4个阶段打通编译器)

// 定义AST节点接口type Node interface {    TokenLiteral() string}// 语句节点(如let声明、return语句)type Statement interface {    Node    statementNode()}// 表达式节点(如算术表达式、函数调用)type Expression interface {    Node    expressionNode()}// 变量声明语句节点type LetStatement struct {    Token Token    Name  *Identifier    Value Expression}// 中缀表达式节点(如a + b、x * y)type InfixExpression struct {    Token    Token    Left     Expression    Operator string    Right    Expression}// Parser核心逻辑:解析令牌流,生成ASTfunc (p *Parser) ParseProgram() *Program {    program := &Program{}    program.Statements = []Statement{}    for !p.curTokenIs(EOF) {        stmt := p.parseStatement()        if stmt != nil {            program.Statements = append(program.Statements, stmt)        }        p.nextToken()    }    return program}// 解析let声明语句func (p *Parser) parseLetStatement() *LetStatement {    stmt := &LetStatement{Token: p.curToken}    // 解析变量名(IDENT)    if !p.expectPeek(IDENT) {        return nil    }    stmt.Name = &Identifier{Token: p.curToken, Value: p.curToken.Literal}    // 解析赋值运算符=    if !p.expectPeek(ASSIGN) {        return nil    }    p.nextToken()    // 解析赋值右侧的表达式    stmt.Value = p.parse_Expression(LOWEST)    // 跳过分号    if p.peekTokenIs(SEMICOLON) {        p.nextToken()    }    return stmt}

核心要点:AST的核心是“结构化表达”,把零散的令牌,按照语法规则组织成有层级的节点,为后续执行打下基础。

阶段3:Evaluator(求值器)——执行AST,输出结果

有了AST这个“逻辑骨架”,下一步就是让程序“跑起来”,这就是Evaluator(求值器)的作用。求值器会遍历AST的每一个节点,递归计算每个节点的值,同时管理变量的作用域(通过环境变量存储变量名和对应的值)。

比如,当求值器遇到“5 + 3”这个中缀表达式节点时,会先递归计算左边的5和右边的3,再执行加法运算,得到结果8;当遇到“let x = 10”时,会把变量x和值10存储到环境中,后续代码就能调用这个变量。

而闭包的实现,也依赖求值器的环境管理:当执行函数时,会创建一个新的局部环境,同时保留对外部环境的引用,这样内层函数就能访问外层函数的变量,即使外层函数已经执行完毕。

关键代码(Go语言实现,核心逻辑):

// 环境变量:存储变量名与值的映射type Environment struct {    store map[string]interface{}    outer *Environment // 外部环境,用于实现闭包}// 创建新环境func NewEnvironment() *Environment {    s := make(map[string]interface{})    return &Environment{store: s, outer: nil}}// 创建子环境(用于函数调用,保留外部环境引用)func NewEnclosedEnvironment(outer *Environment) *Environment {    env := NewEnvironment()    env.outer = outer    return env}// 查找变量(优先查找当前环境,找不到则查找外部环境)func (e *Environment) Get(name string) (interface{}, bool) {    val, ok := e.store[name]    if !ok && e.outer != nil {        val, ok = e.outer.Get(name)    }    return val, ok}// 存储变量func (e *Environment) Set(name string, val interface{}) interface{} {    e.store[name] = val    return val}// 求值器核心逻辑:递归求值AST节点func (e *Evaluator) Eval(node Node, env *Environment) interface{} {    switch node := node.(type) {    // 求值整数字面量    case *IntegerLiteral:        return node.Value    // 求值标识符(变量)    case *Identifier:        return e.evalIdentifier(node, env)    // 求值中缀表达式(如a + b)    case *InfixExpression:        left := e.Eval(node.Left, env)        right := e.Eval(node.Right, env)        return e.evalInfix_Expression(node.Operator, left, right)    // 求值let声明语句    case *LetStatement:        val := e.Eval(node.Value, env)        env.Set(node.Name.Value, val)        return nil    // 补充函数调用、闭包等其他节点的求值逻辑    default:        return nil    }}

核心要点:求值器的核心是“递归遍历+环境管理”,只要能正确处理每个AST节点的逻辑,就能实现代码的执行,而闭包的关键,就是保留外部环境的引用。

阶段4:REPL——交互式体验,实时看到运行结果

最后一步,就是给编程语言加上REPL(Read–Eval–Print–Loop,读取-求值-输出-循环),让用户能实时输入代码、看到结果,提升使用体验。REPL的核心逻辑很简单,就是循环执行4个步骤:读取用户输入→求值输入的代码→输出结果→等待下一次输入。

Bolt的REPL有一个关键特点:全局共享一个环境,也就是说,用户在一行定义的变量,在后续的输入中依然能使用,比如:

输入“let x = 5;”→ 输出无(仅存储变量) 输入“x + 3;”→ 输出8 输入“let add = fn(a) { a + x; };”→ 输出无 输入“add(2);”→ 输出7

关键代码(Go语言实现,核心逻辑):

// REPL入口函数func StartRepl() {    scanner := bufio.NewScanner(os.Stdin)    env := object.NewEnvironment() // 全局环境    fmt.Println("Bolt语言交互式REPL(输入exit退出)")    for {        fmt.Print("> ")        scanned := scanner.Scan()        if !scanned {            return        }        line := scanner.Text()        if line == "exit" {            fmt.Println("再见!")            return        }        // 1. 词法分析:生成令牌流        l := lexer.NewLexer(line)        // 2. 语法分析:生成AST        p := parser.NewParser(l)        program := p.ParseProgram()        // 3. 错误检查        if len(p.Errors()) != 0 {            printParserErrors(p.Errors())            continue        }        // 4. 求值并输出结果        evaluated := evaluator.Eval(program, env)        if evaluated != nil {            fmt.Println(evaluated.Inspect())        }    }}// 打印语法分析错误func printParserErrors(errors []string) {    fmt.Println("语法错误:")    for _, msg := range errors {        fmt.Printf("  - %s\n", msg)    }}

核心要点:REPL的核心是“循环+复用环境”,代码逻辑不复杂,但能极大提升语言的实用性和交互性,也是一款编程语言必不可少的功能。

三、辩证分析:从零造语言,价值与局限并存

这位开发者用Go从零搭建Bolt的经历,无疑是一次成功的技术突破——它不仅让开发者自己吃透了编译器底层逻辑,更给无数程序员提供了一个可落地的学习案例,打破了“底层技术高不可攀”的神话,这是它最大的价值:把复杂的理论,变成了普通人能上手的实践。

但我们也不能盲目吹捧“从零造语言”:Bolt本质上是一款“树遍历解释器”,直接遍历AST求值,这种方式的优点是简单、易实现,适合学习,但缺点也很明显——执行效率低,无法应对复杂的业务场景。而我们日常使用的Go、Python、JavaScript等语言,大多采用“编译+虚拟机”的架构(先把代码编译成字节码,再由虚拟机执行),效率远高于树遍历解释器。

更值得思考的是:普通人真的有必要从零造语言吗?对于大多数程序员来说,日常工作中几乎用不到编译器开发,花费几周时间造一款简单的语言,到底是“提升技术”还是“浪费时间”?其实答案很简单:关键不在于“造语言”本身,而在于造语言的过程——通过拆解、实现每一个环节,掌握的词法分析、语法分析、环境管理等底层逻辑,能迁移到任何编程语言的学习和开发中,这才是最核心的收获。

四、现实意义:掌握底层逻辑,才是程序员的核心竞争力

很多程序员都会陷入一个误区:只会用框架、调API,却不懂底层原理,遇到稍微复杂的问题(比如框架报错、性能优化)就束手无策。而这位开发者的经历,恰恰给我们提了个醒:底层逻辑,才是程序员的“护城河”。

从零造语言的过程,看似是“无用功”,实则是对编程思维的极致锻炼:你需要把复杂的需求(比如闭包、运算符优先级)拆解成一个个小问题,再逐个解决,这种拆解能力、逻辑思维,能迁移到任何开发场景中。比如,掌握了Lexer的原理,你就能轻松理解正则表达式的匹配逻辑;掌握了AST的原理,你就能更好地理解代码混淆、静态分析工具的工作机制。

更重要的是,它打破了“技术焦虑”——很多程序员看到“编译器”“AST”就退缩,觉得自己永远学不会,但Bolt的案例证明:只要敢动手、能坚持,普通人也能搞定这些“高深”的技术。对于想要提升自己的程序员来说,不用一开始就追求“造一款成熟的语言”,哪怕只是跟着复刻Bolt的4个阶段,也能收获远超预期的成长。

另外,Bolt的开源特性,也给我们提供了一个很好的学习载体——无需从零开始,下载源码,逐行分析Lexter、Parser、Evaluator的实现逻辑,对照着自己动手修改、优化,就能快速掌握底层技术,这比单纯看理论书籍高效得多。

五、互动话题:你敢尝试从零造一款编程语言吗?

看完这位开发者的经历,相信很多程序员都有了一丝心动:原来造语言并没有那么难,原来底层技术也能如此落地。

不妨来聊聊你的看法:你觉得普通人从零造语言,是提升技术的好方法,还是浪费时间?你有没有尝试过开发一些“小众工具”来锻炼自己的底层能力?如果让你造一款编程语言,你会给它加上什么独特的功能?

评论区留下你的观点,和同行一起交流学习,也可以说说你在学习底层技术时遇到的困惑,我们一起探讨解决!

文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有