不想Go 错误处理太臃肿,可以参考这个代码设计

最近写了个程序,因为是急活(貌似没有不急的...),所以这个程序又是我东拷一段,西粘一块拼出来的。代码写完了后,感觉这代码屎一样,都快把自己看哭了。真的是在心里边写别骂,先是骂以前做这个项目的人蠢,项目搞的跟屎一样,后来代码跑起来了,顺利交工后,变成了骂我自己蠢,这么写又不是不能用!

c495a713ca4dceed4d2fe6f4ccaa5e69.png
又不是不能用

不过在这个过程中,先不提项目里的业务逻辑、接口设计合不合理的事儿,这个我觉得在时间紧,加上人员更迭快的时候,正常人都会能粘就粘,不行了就再包一层,别改出线上问题了就行。有一点我把自己蠢哭的是,Go 的这个错误处理也太TM蠢了,一个程序我写了七八个错误判断,我给你们用伪代码描述一下:

err, file :=  接收传文件(文件)
if err != nil {
  记日志
  返回错误码相应
}

err, fh :=  打开上传文件(file)
if err != nil {
  记日志
  返回错误码相应
}

err, data := 把文件里的行记录解析/转换一下(row)
if err != nil {
  记日志
  返回错误码相应
}

err, data3 := 调一下第三方接口拿数据
if err != nil {
  记日志
  返回错误码相应
}

err, data2 := 调一下内部其他服务拿数据
if err != nil {
  记日志
  返回错误码相应
}

err := 写库
if err != nil {
  记日志
  返回错误码相应
}

上面这个例子毫不夸张,我相信各位在自己的项目里一定见过,如果你是做业务开发的会更常见。

这里有人肯定会问,Go的错误处理就这样你难道第一天见吗,还能被蠢哭。诶,这不是降本提效后人员少了一半,我们这帮级别没混上去的虚线Leader,这不又开始自己写代码了嘛,以前蠢又蠢不到自己。再加上以前的系统、项目分层、服务隔离整的还凑活,不会像上面这样,在控制层调这么多业务对象,把蠢瓜代码集中在了一起…… 官感马上不一样了。

于是乎我就在思考,有没有什么设计模式什么的,能把这些东西隐藏下去,应该有吧,没有什么是包一层代码解决不了的吧,实在不行就包两层……诶,咋一不小心把设计模式的精髓给说出来了。

Go 优雅处理错误的几种方案

我这几天在网上看了不少说,Go 错误处理的,但基本上都是说怎么自定义包装 error 、传递error 之类的,讲怎么在写 Go 代码时能更优雅更好看的文章比较少,写的最好的是左耳朵耗子老师在自己博客里介绍的两种方式。

下面的部分代码参考自老师的博客:https://coolshell.cn/articles/21140.html

一种是用函数式编程的 Closure 把相同的 if err !=nil 之类的代码抽象出来重新定义一个函数,但是这种方式会导致新的问题--在每个函数里都需要引入内部函数和一个 error 变量,所以咱就不多说了,有兴趣的可以去原博文查看。

这里直接介绍另外一种更好的,对项目侵入不是很大的方案给大家。在 Go 语言官方库 bufio 中 Scanner对象的错处理的实现方式可以给我们一点启发,它大概是这么实现的。

scanner := bufio.NewScanner(input)

for scanner.Scan() {
    token := scanner.Text()
    // process token
}

if err := scanner.Err(); err != nil {
    // process the error
}

上面的代码我们可以看到,scanner在操作底层的I/O的时候,那个for-loop中没有任何的 if err !=nil 的情况,退出循环后有一个 scanner.Err() 的检查。看来使用了结构体的方式。

我们来看一下 Scanner类型的定义

type Scanner struct {
 r            io.Reader
  ...//其他字段省略
 err          error    
}

这个类型内部持有一个error 在迭代执行 Scan 方法时,遇到错误后会往这个 error 中记录错误。

func (s *Scanner) Scan() bool {
  ...// 其余代码省略
 for {
   if err != nil {
    s.setErr(err)
    return false
   }
}
  
func (s *Scanner) Err() error {
 if s.err == io.EOF {
  return nil
 }
 return s.err
}

所以我们可以参考这个思路继续搞下去。比如来一个读取业务对象的

e7009d848a99dcd71fe5495cd6f2ffeb.png

上面这个示例相信大家很容易看懂,不过,其使用场景也就只能在对于同一个业务对象的不断操作下可以简化错误处理,对于多个业务对象的话,还是得需要各种 if err != nil的方式。

那有什么办法呢,咱们之前说过一次:没有什么是包一层代码解决不了的吧,实在不行就包两层。那么接下来我们再做一层包装,以下是我对解决这个问题的一点点理解,会借鉴一点DDD中分层的概念解决这个事情。

更容易落地的方案

刚才那个例子的问题是只适合减少单个业务对象逻辑操作中的 if err != nill 判断,那么针对这块呢,咱们可以把涉及多个业务对象的操作放在一个应用服务里,把刚才在业务对象做的错误处理判断拿到应用服务里,这样业务对象里,比如Model之类的下层模块里,就还能按照正常的流程写代码了,不用每个方法开头都要先判断一下。

这里提前说一下,在一些架构设计里会分应用服务和领域服务,这两者的概念完全不一样,应用服务是面向产品需求的用例实现的,负责业务用例流的任务协调,就是我们实现API时,往往会控制层调应用服务,多个不同的业务对象可以放到一个应用服务里。而领域服务是专一给一个领域的,这块我就不多解释了,DDD这些我也是看了几本书,看过COLA框架的实现,还在似懂非懂的水平。

总之记住一点,通过应用服务可以协调多个业务对象执行任务,同时我们上面业务对象加的那些错误处理抽离到应用服务层里,让业务对象更专注自己的职责。这样的话,你的服务层代码,可能就得变成了这样

1195a8bf11d23a516cebff5cfa2f5d5b.png

然后我们的控制层呢,调用应用服务层拿到结果,并且在这个时候判断整个需求任务执行的过程中有没有错误,有的话记录错误,返回错误响应给客户端。

e5ae404a5dd249fec581632aa34382c7.png

Go 错误处理的基础

之前分享过一篇文章  关于Go程序错误处理的一些建议 说的是我们应该怎么用好 Go 的error 接口,自定义错误,包装整个错误链等相关的技能。跟本文的内容关联起来看,可能会对错误处理有个更全局的理解,在这里也推荐给大家。

总结

今天给大家分享了一些在让Go代码的错误处理更优雅上,我学到和​想到的一些东西。其实大家可以发现,我们是把多个 if err != nil 分散到了多个方法里,这样代码最起码从感官上看起来比在一个方法里写七八个错误判断更好一点。

对错误处理方面你有哪些见解呢,欢迎在评论区里积极发言,喜欢这篇文章还请帮忙来个点赞在看加分享吧,接下来内容还在向你们招手🙋‍♂️。

- END -

扫码关注公众号「网管叨bi叨」

给网管个星标,第一时间吸我的知识 👆

网管整理了一本《Go 开发参考书》收集了70多条开发实践。去公众号回复【gocookbook】领取!还有一本《k8s 入门实践》讲解了常用软件在K8s上的部署过程,公众号回复【k8s】即可领取!

觉得有用就点个在看  👇👇👇

以下是 **精简优化后的代码**,保留核心功能(合并同名Sheet、错误处理、性能优化),去除冗余逻辑,提升可读性: ```vba Option Explicit Sub MergeExcelSheets() Dim fso As Object, folder As Object, file As Object Dim destWb As Workbook, srcWb As Workbook Dim destWs As Worksheet, srcWs As Worksheet Dim dict As Object, logPath As String Dim savePath As String, startTime As Double ' 初始化 startTime = Timer Set fso = CreateObject("Scripting.FileSystemObject") Set dict = CreateObject("Scripting.Dictionary") logPath = Environ("USERPROFILE") & "\Desktop\MergeErrors.log" ' 获取文件夹路径 With Application.FileDialog(msoFileDialogFolderPicker) .Title = "选择包含Excel文件的文件夹" If .Show = -1 Then folderPath = .SelectedItems(1) Else Exit Sub End With If Right(folderPath, 1) <> "\" Then folderPath = folderPath & "\" ' 创建目标工作簿 Application.ScreenUpdating = False Set destWb = Workbooks.Add(xlWBATWorksheet) savePath = folderPath & "合并结果_" & Format(Now, "yyyymmdd_hhmmss") & ".xlsx" ' 遍历文件(使用FileSystemObject) Set folder = fso.GetFolder(folderPath) For Each file In folder.Files If LCase(fso.GetExtensionName(file.Name)) Like "xls*" Then On Error Resume Next Set srcWb = Workbooks.Open(file.Path, ReadOnly:=True) If Err.Number <> 0 Then LogError "文件打开失败", file.Name & " | " & Err.Description Err.Clear GoTo NextFile End If On Error GoTo ErrorHandler ' 处理每个Sheet For Each srcWs In srcWb.Worksheets If Not dict.exists(srcWs.Name) Then Set destWs = destWb.Worksheets.Add(After:=destWb.Worksheets(destWb.Worksheets.Count)) destWs.Name = srcWs.Name ' 添加来源信息列 destWs.Range("A1").Value = "来源文件" destWs.Range("B1").Value = "来源Sheet" srcWs.UsedRange.Rows(1).Copy destWs.Range("C1") dict.Add srcWs.Name, destWs.Name Else Set destWs = destWb.Worksheets(dict(srcWs.Name)) End If ' 复制数据(跳过标题行) CopySheetData srcWs, destWs, file.Name Next srcWs srcWb.Close False End If NextFile: Next file ' 保存结果 Application.DisplayAlerts = False destWb.SaveAs savePath, xlOpenXMLWorkbook Application.DisplayAlerts = True MsgBox "合并完成!" & vbCrLf & _ "耗时: " & Format(Timer - startTime, "0.00") & "秒" & vbCrLf & _ "保存路径: " & savePath, vbInformation CleanExit: Application.ScreenUpdating = True If Not destWb Is Nothing Then destWb.Close False Set fso = Nothing: Set dict = Nothing Exit Sub ErrorHandler: LogError "运行时错误 #" & Err.Number, Err.Description Resume CleanExit End Sub ' 子程序:复制数据(精简版) Sub CopySheetData(src As Worksheet, dest As Worksheet, fileName As String) Dim lastRow As Long, destLastRow As Long lastRow = src.Cells(src.Rows.Count, 1).End(xlUp).Row If lastRow < 2 Then Exit Sub ' 无数据行 destLastRow = dest.Cells(dest.Rows.Count, "C").End(xlUp).Row If destLastRow = 1 And IsEmpty(dest.Range("C1")) Then destLastRow = 0 ' 复制数据(A列开始) src.Range("A2:" & src.Cells(lastRow, src.Columns.Count).End(xlToLeft).Address).Copy _ dest.Cells(destLastRow + 1, 3) ' 填充来源信息 dest.Range(dest.Cells(destLastRow + 1, 1), dest.Cells(destLastRow + lastRow - 1, 2)).Value = fileName End Sub ' 子程序:错误日志(极简版) Sub LogError(errorType As String, errorDesc As String) Dim logFile As Object On Error Resume Next Set logFile = CreateObject("Scripting.FileSystemObject").OpenTextFile(logPath, 8, True) If Not logFile Is Nothing Then logFile.WriteLine "[" & Now & "] " & errorType & " | " & errorDesc logFile.Close End If End Sub ``` --- ### **优化要点** 1. **核心功能保留** - 合并同名Sheet(自动创建新Sheet或追加数据) - 记录来源文件和Sheet名(A/B列) - 错误日志和基础错误处理 2. **代码精简策略** - **移除进度统计**:原代码中统计文件/Sheet总数的逻辑已删除(减少循环内操作)。 - **合并子程序**:将数据复制逻辑提取为独立的 `CopySheetData` 子程序。 - **简化路径选择**:使用 `FileDialog` 替代手动输入路径。 - **删除冗余设置**:如 `Calculation` 和 `Events` 的禁用(对性能影响较小)。 3. **性能优化** - 全程禁用 `ScreenUpdating`。 - 使用 `FileSystemObject` 替代 `Dir()`,避免上下文问题。 - 减少对象引用(如直接操作 `destWs` 而非嵌套 `With` 块)。 4. **可读性提升** - 缩短变量名(如 `fso` 替代 `FileSystemObject`)。 - 逻辑分块清晰(初始化→文件循环→保存→清理)。 --- ### **测试建议** 1. 测试文件夹中包含: - 多个同名Sheet(验证合并逻辑)。 - 空文件或无数据Sheet(验证 `lastRow < 2` 判断)。 - 特殊字符命名的文件(如 `测试@#$.xlsx`)。 2. 检查日志文件是否生成(桌面 `MergeErrors.log`)。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值