为什么不建议使用goto

news/2024/11/8 23:51:40/文章来源:https://www.cnblogs.com/hujingnb/p/18536135

前提

最近在公司代码review过程中, 看到同事的代码中大量使用了goto, 我给出了"不用 goto"的建议. 但其给出的理由是使用goto更简单. 确实, 使用goto可以使得逻辑更简单直接, 但前提是不乱用goto, 而在公司的项目中又很难保证这一点.

问题

使用goto带来的最直观的问题就是逻辑的复杂度直线升高. 举个例子来展现goto是如何一步步导致逻辑破败不堪的. (当然, 这个例子是我臆想出来的场景)

首先, 我们有一个创建订单并验证支付的需求:

package mainimport "fmt"func main() {fmt.Println("处理订单开始")fmt.Println("Step 1: 创建订单")fmt.Println("Step 2: 验证订单")fmt.Println("Step 3: 验证付款信息")fmt.Println("Step 4: 订单完成")fmt.Println("处理订单结束")
}

此时逻辑很清楚吧. 现在, 我们要对验证订单的结果进行处理, 如果验证失败, 则进行错误处理, 很合理吧:

package mainimport "fmt"func main() {fmt.Println("处理订单开始")fmt.Println("Step 1: 创建订单")var validErr errorfmt.Println("Step 2: 验证订单")if validErr != nil {goto Fail}fmt.Println("Step 3: 验证付款信息")fmt.Println("Step 4: 订单完成")goto End
Fail:fmt.Println("验证付款失败")
End:fmt.Println("处理订单结束")
}

现在, 新的需求来了:

  1. 付款信息处理可能因各种原因失败, 需要重试, 最多重试3次
  2. 验证订单也可能失败(异步接口验证, 网络抖动等), 需要重试, 最多重试3次
  3. 若订单验证失败, 需要提示并重新创建订单
package mainimport "fmt"func main() {fmt.Println("处理订单开始")CreatOrder:fmt.Println("Step 1: 创建订单")validRetryNum := 0
ValidOrder:var validErr errorfmt.Println("Step 2: 验证订单")if validErr != nil {validRetryNum++if validRetryNum <= 3 {goto ValidOrder}fmt.Println("订单验证失败")goto CreatOrder}checkRetryNum := 0
CheckOrder:var checkErr errorfmt.Println("Step 3: 验证付款信息")if checkErr != nil {checkRetryNum++if checkRetryNum <= 3 {goto CheckOrder}goto CheckErr}fmt.Println("Step 4: 订单完成")goto EndCheckErr:fmt.Println("付款信息验证失败")
End:fmt.Println("处理订单结束")
}

再来:

  1. 验证付款信息失败, 可能是因为没有付款等, 需要进行付款处理的逻辑
  2. 验证订单付款时, 订单可能认为取消支付, 需要处理资源清理等
package mainimport "fmt"func main() {fmt.Println("处理订单开始")CreatOrder:fmt.Println("Step 1: 创建订单")validRetryNum := 0
ValidOrder:var validErr errorfmt.Println("Step 2: 验证订单")if validErr != nil {validRetryNum++if validRetryNum <= 3 {goto ValidOrder}fmt.Println("订单验证失败")goto CreatOrder}checkRetryNum := 0
CheckOrder:var checkErr errorvar paid, cancel boolfmt.Println("Step 3: 验证付款信息")if checkErr != nil {checkRetryNum++if checkRetryNum <= 3 {goto CheckOrder}goto CheckErr}if cancel {goto CancelOrder}if !paid {goto ProcessOrder}fmt.Println("Step 4: 订单完成")goto EndCheckErr:fmt.Println("付款信息验证失败")goto EndCancelOrder:fmt.Println("取消订单支付")goto EndProcessOrder:fmt.Println("处理付款信息")goto CheckOrderEnd:fmt.Println("处理订单结束")
}

再来

  1. 增加处理失败的日志记录
  2. 增加订单验证失败的日志记录
  3. 不管是取消订单支付, 还是付款处理失败, 都需要进行一些清理工作
package mainimport "fmt"func main() {fmt.Println("处理订单开始")CreatOrder:fmt.Println("Step 1: 创建订单")validRetryNum := 0checkRetryNum := 0var checkErr errorvar validErr errorvar paid, cancel bool
ValidOrder:fmt.Println("Step 2: 验证订单")if validErr != nil {validRetryNum++if validRetryNum <= 3 {goto ValidOrderErrorLog}fmt.Println("订单验证失败")goto CreatOrder}CheckOrder:fmt.Println("Step 3: 验证付款信息")if checkErr != nil {checkRetryNum++if checkRetryNum <= 3 {goto CheckOrderErrorLog}goto CheckErr}if cancel {goto CancelOrder}if !paid {goto ProcessOrder}fmt.Println("Step 4: 订单完成")goto EndValidOrderErrorLog:fmt.Println("记录订单验证失败")goto ValidOrderCheckOrderErrorLog:fmt.Println("记录付款验证失败")goto CheckOrderCheckErr:fmt.Println("付款信息验证失败")goto CleanOrderCancelOrder:fmt.Println("取消订单支付")goto CleanOrderProcessOrder:fmt.Println("处理付款信息")goto CheckOrderCleanOrder:fmt.Println("订单关闭的清理工作")goto EndEnd:fmt.Println("处理订单结束")
}

现在, 如果你还觉得逻辑清晰, 那我只能说一句"牛".

代码演进到现在, 逻辑已经十分混乱了, 逻辑的混乱会导致一系列问题:

  1. 难以理解, 逐步增加后续迭代的成本
  2. 造成额外的心智负担
  3. 追踪困难
  4. 如果是if for 在逻辑上是自上而下的, 但引入 goto会导致逻辑上下横跳
  5. 等等

可能有人会觉得我举的例子有些极端, 实际中没有人会这么做. 那是因为例子总是简单化的, 现实中的场景实际上要更加复杂:

  1. 大段逻辑分散: 例子中的所有单条print语句, 在实际项目中都可能会对应一大段的逻辑
  2. 最小改动原则: 对现有项目进行改动的时候(尤其是需求要的比较急, 要求改动最小实现功能), 对现有代码的改动越小, 则风险越小. 因此逻辑会越堆越难以理解, 直至最后无法使用
  3. 每个人的水平不同: 同一份代码会由团队中的不同人在不同时间维护, 即使你自信自己的水平, 也无法保证代码在未来不会向着这个方向发展

使用场景

当然, 我也不是把goto一棒子打死, 同事在反驳我的时候也给出了强有力的理由"Go 标准库也存在大量使用goto的地方". 比如:

func ParseMAC(s string) (hw HardwareAddr, err error) {if len(s) < 14 {goto error}if s[2] == ':' || s[2] == '-' {if (len(s)+1)%3 != 0 {goto error}n := (len(s) + 1) / 3if n != 6 && n != 8 && n != 20 {goto error}// ...} else if s[4] == '.' {if (len(s)+1)%5 != 0 {goto error}n := 2 * (len(s) + 1) / 5if n != 6 && n != 8 && n != 20 {goto error}// ...} else {goto error}return hw, nilerror:return nil, &AddrError{Err: "invalid MAC address", Addr: s}
}

这种其实是可以接受的, 能够简化流程, 但是, 但是, 不要忘记我不建议使用goto最重要的一点:

  1. 你无法保证自己拥有掌控goto的实力
  2. 即使你自信, 也无法保证同事有掌控goto的实力

一旦在使用过程中产生破窗效应, 使用goto破坏的速度一定是比不用要快的多. 代码很快就会脱离掌控.

因此, 为了避免这种情况, 最好的方式就是在最开始杜绝掉.

最后的最后, goto如果能够好好用的话, 确实能够带来一定的便利性, 前提是项目由你一人开发, 或你拥有掌控权可以拒绝某些腐败代码进入代码库.

goto并不可怕, 可怕的是不加限制的乱用goto.


欢迎来辩...

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

20222312 2024-2025-2 《网络与系统攻防技术》实验四报告

一、恶意代码文件类型标识、脱壳与字符串提取对提供的rada恶意代码样本,进行文件类型识别,脱壳与字符串提取,以获得rada恶意代码的编写作者 (1).通过kali中的file命令查看文件格式和可运行平台,即exe文件,Win32平台 通过PEID查壳文件发现使用UPX壳二、使用IDA Pro静态或…

点阵LED电路分析

以点阵的左上角LED为例,即 A1 LED为例,进行电路分析 9号脚接着LED的阳极,所以9号脚需是高电平,13号脚连着LED的阴极,所以13号脚需是低电平 9号脚连接着Q10的集电极,欲使9号脚为高电平,则需要Q10导通 Q10的发射极连接着+5V电压,欲使Q10导通,则基极需为低电平,即LEDC0为…

苹果手机和电脑数据互传

利用共享的文件夹 这个方法简单来说就是iPhone通过远程连接到Windows服务器,利用共享的文件夹来进行照片中转。 注意:iPhone和Windows必须要连接到同一个局域网内! ● Windows设置 第一步,获取Windows电脑的IP地址。 具体操作是先打开“Windows设置-网络和Internet-网络和共…

由一个业务需求引发的对 ASP.NET 全局变量的调研及结果

在单机模式下,使用哪种技术来存储身份状态信息比较安全可靠呢?前言 前段时间使用 ASP.NET MVC + Form Auth 做了一个单机小项目,当时对于采用什么方式来存储登录状态有些纠结,通常的做法是使用 Cookie 或者 Session,但是我想有没有更好的方式来存储登录状态呢?于是花了点…

苹果手机数据传输

利用共享的文件夹 这个方法简单来说就是iPhone通过远程连接到Windows服务器,利用共享的文件夹来进行照片中转。 注意:iPhone和Windows必须要连接到同一个局域网内! ● Windows设置 第一步,获取Windows电脑的IP地址。 具体操作是先打开“Windows设置-网络和Internet-网络和共…

c语言中返回整数值的长度

001、方法1 while循环[root@PC1 test]# ls test.c [root@PC1 test]# cat test.c ## 测试c程序 #include <stdio.h>int get_length(int a) {int length = 0;while(a > 0){length++;a /= 10;}return length; }int main(void) {int a;printf("a = "…

Blender 常用建模操作

常用简单介绍 挤出 快捷键:E 挤出是2个动作,生成加移动,所以右键撤销只能撤销移动内插 快捷键:I 内插仅是一个动作倒角 快捷键:Ctrl+B 滚动滚轮可以增加倒角的段数环切 快捷键:Ctrl+R 滚动滚轮可以增加倒角的段数挤出详细介绍 沿轴线挤出或者自动挤出挤出流形可以向内挤出…

Stack模块的设置

TEAM: Topological Evolution-aware Framework for Traffic Forecasting–Extended Version Motivation 为了捕捉复杂的时空动态,许多基于深度学习的方法最近被提出,并由于其学习非线性动力学[35,59]的能力,在挑战数据集上显示出了有希望的结果。这些方法通常建立在图神经网…

CCPC辽宁省赛赛后总结

2024CCPC辽宁省赛-赛后总结 ​ 写这篇的时候已经是11/8日了,过了半个多星期才开始写,我实在堕落,啊对对对。 ​ 这算是acm生涯中真正意义上的首场了,本来是奔着首银摄金的目标去的,结果拿了个铜尾,导致我们小队闹得不是很愉快,所以来写下这篇总结来避免下次犯错。ps:滚榜…

C++之endl以及它与换行符的区别

看下C++_primer上的一段话,并给予解释:1. endl 是操纵符 在 C++ 中,endl 是一种特殊的操纵符(manipulator),它的作用不仅是结束一行(相当于换行),还会刷新缓冲区。操纵符是一种可以影响输出行为的特殊值,比如 endl、setw 等。 题外话 想要了解更多关于setw的内容,可…

C++之fixed

在 C++ 中,fixed 是一个操纵符(manipulator),用于指定浮点数的显示格式。 在默认情况下,C++ 会使用科学计数法或定点(小数点)格式输出浮点数,具体取决于数值的大小和有效位数。 然而,当使用 fixed 时,它会强制所有浮点数都以定点格式显示,即以小数点后的固定位数输出…