Golang编译器DIY,手搓 if err != nil { return err } 语法糖

news2025/3/17 16:56:55

前序

在go的社区里,下面这三行代码是被吐槽的最多的

if err != nil {
    return err
}

从代码之整洁美观的角度看,这样的写法也是让人不舒服的。尤其是 当有很多错误需要处理的时候,就会发现通篇都是这三行。
所以想着看看修改一下编译器,优化这一写法。
预期达到的效果如下:

func  Aaa(info string) (err error) {
   err = NewError(info)?
   
   //todo something
}

它等价于下面这段函数

func  Aaa(info string) (err error) {
   err = NewError(info)
   if err != nil {
       return
   }
   
   //todo something
}

思路

go的编译过程大概分为这么几个阶段:

  1. 扫描解析源文件
  2. 类型检查和AST生成
  3. 生成SSA中间代码,并进行一定优化
  4. 生成机器码

基于这个过程,我们的"?"号语法糖,只需要在编译器解析源码的时候,将?号扩展为if err != nil { return } 即可

拉取go的源代码

在正式开始开工之前,需要在本地先安装一个go的执行环境,并且尽量用最新的版本。然后clone go的源码包。
下面我们所有的操作,都相对于这个目录来完成。

git clone https://github.com/golang/go.git

注意,你本地已经安装的go的版本,尽量只比你要编译的版本小一个版本号。
比如我这里用go version go1.21.11 darwin/amd64编译go version go1.22.0 darwin/amd64

代码拉下来后,可以先编译一下,确定默认编译不会出问题。

  • all.bash 编译完成后,会自动进行测试,时间还是比较长的。并且给了多系统的命令文件 all.bat all.rc
  • make.bash 仅编译,我的电脑大约20s就能编译完成。
cd go/src

//编译并测试
./all.bash

//仅编译
./make.bash

编译完成后,可以在bin目录下看到go文件,运行下面的命令,打印版本号,表示编译成功。

  • 为了和本地环境区分开,需要指定GOROOT为我们下载的go源码的文件夹路径。
 GOROOT=<go path> bin/go version
 
 //输出:
 go version go1.22.0 darwin/amd64

增加 ? 标识符 和 具体语法节点

首先增加 ?号标识符,在syntax目录下,这个目录的主要功能就是做scanparser

  • 路径:src/cmd/compile/internal/syntax/tokens.go
  • 在token里面增加一个_RetErr用来表示?号。注释必须按照下面的格式写,自动生成需要。
 // go:generate stringer -type token -linecomment tokens.go

const (
   _    token = iota
   ...
   _Semi      // ;
   _RetErr    // ?
   _Colon     // :
   ...   
)

上边的注释 // go:generate stringer -type token -linecomment tokens.go表示我们需要go generate一下。

  • stringer 较新的版本中这个包是内置的,通常不需要安装,如果提示不存在,则手动安装一下。
//syntax目录下执行

go generate tokens.go

查看token_string.go文件,如下图,将我们新加的token也生成上去了。
在这里插入图片描述

我们还需要创建一个?号对应的具体语法树的节点,因为我们这里做的是一个语法糖,所以直接组合一下if对应的IfStmt结构就可以了,如下:

IfStmt struct {
   Init SimpleStmt
   Cond Expr
   Then *BlockStmt
   Else Stmt // either nil, *IfStmt, or *BlockStmt
stmt
}

RetErrStmt struct {
   IfStmt
}

简单介绍一下if结构的几个字段的作用:

  • Init:在执行if条件判断前的代码块,如:if _,ok:=get();ok{ todo }中,_,ok:=get()部分就是Init,它可以为空。
  • Cond:判断条件,
  • Then:条件为真时,执行这个代码。
  • Else:条件为假时,执行的代码,可以为空。

语法解析

go的源码解析主要有两个结构体:

  • scanner :负责按照token维度扫描源文件
  • parser:将扫描的token组装成具体语法树

我们先增加对于?号标识符的扫描识别,src/cmd/compile/internal/syntax/scanner.go

func (s *scanner) next() {
...
case ';':
   s.nextch()
   s.lit = "semicolon"
   s.tok = _Semi

case '?':
   s.nextch()
   s.tok = _RetErr
...
}

然后增加对?的语法解析,我们参考IfStmt的解析方法:

  • 路径:src/cmd/compile/internal/syntax/parser.go
  • stmtOrNil : 在这个函数中增加标识符的解析方法
  • retErr :实际上我们增加了一个IfStmt的语法糖。
func (p *parser) stmtOrNil() Stmt {
...
case _RetErr:
   return p.retErr()

case _If:
   return p.ifStmt()
...
}

func (p *parser) retErr() *RetErrStmt {
   if trace {
      defer p.trace("ifStmt")()
   }

   
   //判断条件:err != nil
   condExpr := &Operation{
      X:  &Name{Value: "err"},
      Op: Neq,
      Y:  &Name{Value: "nil"},
   }
   condExpr.pos = p.pos()

   // 表示 Then 块: return
   thenBlock := &BlockStmt{
      List: []Stmt{
         &ReturnStmt{
            //Results: &Name{Value: "err"},
         },
      },
      Rbrace: p.pos(),
   }
   //组装RetErrStmt
   s := new(RetErrStmt)
   s.pos = p.pos()
   s.Cond = condExpr
   s.Then = thenBlock
   //继续向下扫描
   p.next()
   //如果存在else则继续解析
   if p.got(_Else) {
      switch p.tok {
      case _If:
         s.Else = p.ifStmt()
      case _Lbrace:
         s.Else = p.blockStmt("")
      default:
         p.syntaxError("else must be followed by if or statement block")
         p.advance(_Name, _Rbrace)
      }
   }

   return s
}

边界判断

go对写法有比较严格的要求,尤其是换行和边际的判断,所以我们需要告诉编译器,?号不需要处理边际,因为已经解析成了其他的结构。

同样在parser.go文件中的stmtList函数:

func (p *parser) stmtList() (l []Stmt) {
   if trace {
      defer p.trace("stmtList")()
   }

   for p.tok != _EOF && p.tok != _Rbrace && p.tok != _Case && p.tok != _Default {
      s := p.stmtOrNil()
      p.clearPragma()
      if s == nil {
         break
      }
      l = append(l, s)
      //跳过RetErrStmt的检查
      if _, ok := s.(*RetErrStmt); ok {
         continue
      }
      //?;} 都属于正常的边界。
      if !p.got(_Semi) && p.tok != _RetErr && p.tok != _Rbrace {
         p.syntaxError("at end of statement")
         p.advance(_Semi, _Rbrace, _Case, _Default)
         p.got(_Semi) // avoid spurious empty statement
      }
   }
   return
}

至此,一个初步的 ? 号语法糖已经制作完成,用make.base重新编译项目。

测试

我么在这个go目录下,新建一个测试文件reterr.go,内容如下:

  • 因为源码中我们处理else的情况,所有理论上?else{}?else if xx {}也同样是支持的。
  • EasyRetError:简单返回错误,可以看到代码量明显减少
  • IfElseRetError和MulIfElseRetError:可以继续做else判断,并且不影响返回多个值
package main

import (
   "errors"
   "fmt"
)

func NewError(text string) error {
   if text == "" {
      return nil
   } else {
      return errors.New(text)
   }
}
func EasyRetError(info string) (err error) {
   err = NewError(info)?
   return
}
func IfElseRetError(info string)(str string, err error){
   err = NewError(info)?else{
      return info,nil
   }
}
func MulIfElseRetError(info string,ok bool)(str string, err error){
   err = NewError(info)?else if ok{
      return "test",nil
   }else{
      return "success",nil
   }
}

在main函数中,我们写一下预期的结果,不符合预期则 panic。

func main() {
   var err error
   var info = ""
   //简单情况测试
   if err = EasyRetError("Err");err == nil {
      panic("EasyRetError.Err")
   }
   if err = EasyRetError("");err != nil {
      panic("EasyRetError.nil")
   }
   //if else 分支情况测试
   if info,err = IfElseRetError(info); err != nil || info != "" {
      panic("IfElseRetError.err not nil")
   }
   info = "test"
   if info,err = IfElseRetError(info); err == nil || info != "" {
      panic("IfElseRetError.info = test")
   }
   // 多分支测试
   info = "test"
   if _,err = MulIfElseRetError(info,true); err == nil {
      panic("MulIfElseRetError.err not nil")
   }
   if info,err = MulIfElseRetError("",true); err != nil || info != "test" {
      panic("MulIfElseRetError.test")
   }
   if info,err = MulIfElseRetError("",false); err != nil || info != "success" {
      panic("MulIfElseRetError.success")
   }

   fmt.Println("success")
}

用我们重新编译的go,运行上边的代码:

GOROOT=~/project/work/github/go bin/go run reterr.go

测试成功

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2316720.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

图解多头注意力机制:维度变化一镜到底

目录 一、多头注意力机制概述二、代码实现1. pyTorch 实现2. tensorFlow实现 三、维度变化全流程详解1. 参数设定2. 维度变化流程图3. 关键步骤维度变化 四、关键实现细节解析1. 多头拆分与合并2. 注意力分数计算3. 掩码处理技巧 五、完整运行示例六、总结与常见问题1. 核心优势…

[ISP] 人眼中的颜色

相机是如何记录颜色的&#xff0c;又是如何被显示器还原的&#xff1f; 相机通过记录RGB数值然后显示器显示RGB数值来实现颜色的记录和呈现。道理是这么个道理&#xff0c;但实际上各厂家生产的相机对光的响应各不相同&#xff0c;并且不同厂家显示器对三原色的显示也天差地别&…

解锁MySQL 8.0.41源码调试:Mac 11.6+CLion 2024.3.4实战指南

文章目录 解锁MySQL 8.0.41源码调试&#xff1a;Mac 11.6CLion 2024.3.4实战指南前期准备环境搭建详细步骤安装 CLion安装 CMake 3.30.5准备 MySQL 8.0.41 源码配置 CMake 选项构建 MySQL 项目 调试环境配置与验证配置 LLDB 调试器启动调试验证调试环境 总结与拓展 解锁MySQL 8…

关于xcode Project navigator/项目导航栏的一些说明

本文基于 xcode12.4 版本做说明 首先要明确一点&#xff0c;导航栏这里展示的并不是当前工程在电脑硬盘中的文件结构&#xff0c;它展示的是xxxxxx.xcodeproj/project.pbxproj文件(后文简.pbxproj文件)中的内容。我们在导航栏中的操作就是修改该文件&#xff0c;有些操作会修…

深度解析扣减系统设计:从架构到实践

背景 在当今数字化业务蓬勃发展的时代&#xff0c;扣减系统在众多业务场景中扮演着关键角色。无论是电商平台的库存扣减&#xff0c;还是金融领域的资金扣减、积分系统的积分扣减&#xff0c;一个高效、可靠且数据一致的扣减系统都是业务稳健运行的基石。本文将深入探讨扣减系…

视觉定位项目中可以任意修改拍照点位吗?

修改拍照点位不是那么简单 1. 背景2. 修改拍照点位意味着什么&#xff1f;3. 如何解决这个问题&#xff1f; 1. 背景 在视觉定位的项目中&#xff0c;会遇到这么一种情况&#xff1a;完成三步&#xff08;9点标定&#xff0c;旋转中心标定&#xff0c;示教基准&#xff09;之…

深度学习常用操作笔记

深度学习常用操作笔记 指令报错cannot import name Config from mmcvImportError: cannot import name print_log from mmcvImportError: cannot import name init_dist from mmengine.runnerWARNING: Retrying (Retry(total4, connectNone, readNone, redirectNone, statusNon…

C++学习内存管理

1.概念的介绍 总括&#xff1a; 1. 栈&#xff08;Stack&#xff09; 存储内容&#xff1a; 局部变量&#xff08;包括函数参数、非静态局部变量&#xff09;。 函数调用的上下文信息&#xff08;如返回地址、寄存器状态等&#xff09;。 特点&#xff1a; 内存由编译器自动…

git使用。创建仓库,拉取分支,新建分支开发

文章目录 安装 git自己新建仓库&#xff0c;进行代码管理合作开发的流程拉去主分支代码查看本地分支的状态查看远程分支查看远程的仓库信息本地分支切换切换并创建分支提交代码 made by NJITZX git 是一个版本控制工具&#xff0c;真正开发项目中是多个人开发一个项目的&#…

itsdangerous加解密源码分析|BUG汇总

这是我这两天的思考 早知道密码学的课就不旷那么多了 纯个人见解 如需转载&#xff0c;标记出处 目录 一、官网介绍 二、事例代码 源码分析&#xff1a; 加密函数dump源码使用的函数如下&#xff1a; 解密 ​编辑 ​编辑 关于签名&#xff1a; 为什么这个数字签名没有…

不像人做的题————十四届蓝桥杯省赛真题解析(上)A,B,C,D题解析

题目A&#xff1a;日期统计 思路分析&#xff1a; 本题的题目比较繁琐&#xff0c;我们采用暴力加DFS剪枝的方式去做&#xff0c;我们在DFS中按照8位日期的每一个位的要求进行初步剪枝找出所有的八位子串&#xff0c;但是还是会存在19月的情况&#xff0c;为此还需要在CHECK函数…

JavaScript 中 call 和 apply 的用法与区别

文章目录 前言一、 call 方法1.1 基本用法1.2 传递多个参数 二、apply 方法2.1 基本用法2.2 传递数组参数 三、call 和 apply 的区别四、实际应用场景4.1 借用方法4.2 继承与构造函数 五、总结 前言 在 JavaScript 中&#xff0c;call 和 apply 是两个非常重要的函数方法&…

面试系列|蚂蚁金服技术面【1】

哈喽&#xff0c;大家好&#xff01;今天分享一下蚂蚁金服的 Java 后端开发岗位真实社招面经&#xff0c;复盘面试过程中踩过的坑&#xff0c;整理面试过程中提到的知识点&#xff0c;希望能给正在准备面试的你一些参考和启发&#xff0c;希望对你有帮助&#xff0c;愿你能够获…

使用傅里叶变换测量声卡的频率失真

文章目录 一、说明二、关于声卡的技术详述三、实验代码获取四、结论 一、说明 假如我希望使用我的声卡来模拟软件无线电&#xff0c;利用声音而不是射频信号。我的声卡能胜任这项任务吗&#xff1f;本文将研究一种技术来找出答案。另外&#xff0c;需要了解音频技术的读者也可…

【HTML5】01-HTML摆放内容

本文介绍HTML5摆放标签的知识点。 目录 1. HTML概念 2. HTML骨架 3. 标签的关系 4. 标题标签 5. 段落标签 6. 换行和水平线 7. 文本格式化标签 8. 图像标签 图像 - 属性 9. 路径 相对路径 绝对路径 10. 超链接标签 11. 音频标签 12. 视频标签 1. HTML概念 HTM…

内存管理:

我们今天来学习一下内存管理&#xff1a; 1. 内存分布&#xff1a; 我们先来看一下我们下面的图片&#xff1a; 这个就是我们的内存&#xff0c;我们的内存分为栈区&#xff0c;堆区&#xff0c;静态区&#xff0c;常量区&#xff1b; 我们的函数栈帧开辟消耗的内存就是我们…

设计模式使用Java案例

代码设计要有可维护性&#xff0c;可复用性&#xff0c;可扩展性&#xff0c;灵活性&#xff0c;所有要使用设计模式进行灵活设计代码 创建型 简单工厂模式&#xff08;Simple Factory&#xff09; 简单工厂模式&#xff08;Simple Factory Pattern&#xff09;是一种创建型…

模运算的艺术:从基础到高阶的算法竞赛应用

在算法竞赛中&#xff0c;模运算&#xff08;取模运算&#xff09;是一个非常重要的概念&#xff0c;尤其在处理大数、防止溢出、以及解决与周期性相关的问题时。C 中的模运算使用 % 运算符&#xff0c;但它的行为和使用场景需要特别注意。 1. 模运算的基本概念 模运算是指求一…

ST电机库电流采样 三电阻单ADC

一、概述 下图是三电阻采样的电路结构 其中流过三相系统的电流I1、I2、I3遵循以下关系: 因此,为了重建流过普通三相负载的电流,在我们可以用以上公式计算的情况下,只需要对三相中的两相进行采样即可。 STM32的ADC可以很灵活的配置成同步采集两路ADC数据,…

现代密码学 | 具有保密和认证功能的安全方案

1.案例背景 1.1 2023年6月&#xff0c;微软云电子邮件泄露 事件描述&#xff1a; 2023年6月&#xff0c;属于多家美国政府机构的微软云电子邮件账户遭到非法入侵&#xff0c;其中包括了多位高级政府官员的电子邮件。据报道&#xff0c;美国国务院的10个邮件账户中共有6万封电…