前提
最近在公司代码review过程中, 看到同事的代码中大量使用了goto, 我给出了"不用 goto"的建议. 但其给出的理由是使用goto更简单. 确实, 使用goto可以使得逻辑更简单直接, 但前提是不乱用goto, 而在公司的项目中又很难保证这一点. 
问题
使用goto带来的最直观的问题就是逻辑的复杂度直线升高. 举个例子来展现goto是如何一步步导致逻辑破败不堪的. (当然, 这个例子是我臆想出来的场景)
首先, 我们有一个创建订单并验证支付的需求:
package main
import "fmt"
func main() {
    fmt.Println("处理订单开始")
    fmt.Println("Step 1: 创建订单")
    fmt.Println("Step 2: 验证订单")
    fmt.Println("Step 3: 验证付款信息")
    fmt.Println("Step 4: 订单完成")
    fmt.Println("处理订单结束")
}此时逻辑很清楚吧. 现在, 我们要对验证订单的结果进行处理, 如果验证失败, 则进行错误处理, 很合理吧:
package main
import "fmt"
func main() {
    fmt.Println("处理订单开始")
    fmt.Println("Step 1: 创建订单")
    var validErr error
    fmt.Println("Step 2: 验证订单")
    if validErr != nil {
        goto Fail
    }
    fmt.Println("Step 3: 验证付款信息")
    fmt.Println("Step 4: 订单完成")
    goto End
Fail:
    fmt.Println("验证付款失败")
End:
    fmt.Println("处理订单结束")
}
现在, 新的需求来了:
- 付款信息处理可能因各种原因失败, 需要重试, 最多重试3次
- 验证订单也可能失败(异步接口验证, 网络抖动等), 需要重试, 最多重试3次
- 若订单验证失败, 需要提示并重新创建订单
package main
import "fmt"
func main() {
    fmt.Println("处理订单开始")
CreatOrder:
    fmt.Println("Step 1: 创建订单")
    validRetryNum := 0
ValidOrder:
    var validErr error
    fmt.Println("Step 2: 验证订单")
    if validErr != nil {
        validRetryNum++
        if validRetryNum <= 3 {
            goto ValidOrder
        }
        fmt.Println("订单验证失败")
        goto CreatOrder
    }
    checkRetryNum := 0
CheckOrder:
    var checkErr error
    fmt.Println("Step 3: 验证付款信息")
    if checkErr != nil {
        checkRetryNum++
        if checkRetryNum <= 3 {
            goto CheckOrder
        }
        goto CheckErr
    }
    fmt.Println("Step 4: 订单完成")
    goto End
CheckErr:
    fmt.Println("付款信息验证失败")
End:
    fmt.Println("处理订单结束")
}
再来:
- 验证付款信息失败, 可能是因为没有付款等, 需要进行付款处理的逻辑
- 验证订单付款时, 订单可能认为取消支付, 需要处理资源清理等
package main
import "fmt"
func main() {
    fmt.Println("处理订单开始")
CreatOrder:
    fmt.Println("Step 1: 创建订单")
    validRetryNum := 0
ValidOrder:
    var validErr error
    fmt.Println("Step 2: 验证订单")
    if validErr != nil {
        validRetryNum++
        if validRetryNum <= 3 {
            goto ValidOrder
        }
        fmt.Println("订单验证失败")
        goto CreatOrder
    }
    checkRetryNum := 0
CheckOrder:
    var checkErr error
    var paid, cancel bool
    fmt.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 End
CheckErr:
    fmt.Println("付款信息验证失败")
    goto End
CancelOrder:
    fmt.Println("取消订单支付")
    goto End
ProcessOrder:
    fmt.Println("处理付款信息")
    goto CheckOrder
End:
    fmt.Println("处理订单结束")
}再来
- 增加处理失败的日志记录
- 增加订单验证失败的日志记录
- 不管是取消订单支付, 还是付款处理失败, 都需要进行一些清理工作
package main
import "fmt"
func main() {
    fmt.Println("处理订单开始")
CreatOrder:
    fmt.Println("Step 1: 创建订单")
    validRetryNum := 0
    checkRetryNum := 0
    var checkErr error
    var validErr error
    var 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 End
ValidOrderErrorLog:
    fmt.Println("记录订单验证失败")
    goto ValidOrder
CheckOrderErrorLog:
    fmt.Println("记录付款验证失败")
    goto CheckOrder
CheckErr:
    fmt.Println("付款信息验证失败")
    goto CleanOrder
CancelOrder:
    fmt.Println("取消订单支付")
    goto CleanOrder
ProcessOrder:
    fmt.Println("处理付款信息")
    goto CheckOrder
CleanOrder:
    fmt.Println("订单关闭的清理工作")
    goto End
End:
    fmt.Println("处理订单结束")
}现在, 如果你还觉得逻辑清晰, 那我只能说一句"牛".
代码演进到现在, 逻辑已经十分混乱了, 逻辑的混乱会导致一系列问题:
- 难以理解, 逐步增加后续迭代的成本
- 造成额外的心智负担
- 追踪困难
- 如果是iffor在逻辑上是自上而下的, 但引入goto会导致逻辑上下横跳
- 等等
可能有人会觉得我举的例子有些极端, 实际中没有人会这么做. 那是因为例子总是简单化的, 现实中的场景实际上要更加复杂:
- 大段逻辑分散: 例子中的所有单条print语句, 在实际项目中都可能会对应一大段的逻辑
- 最小改动原则: 对现有项目进行改动的时候(尤其是需求要的比较急, 要求改动最小实现功能), 对现有代码的改动越小, 则风险越小. 因此逻辑会越堆越难以理解, 直至最后无法使用
- 每个人的水平不同: 同一份代码会由团队中的不同人在不同时间维护, 即使你自信自己的水平, 也无法保证代码在未来不会向着这个方向发展
使用场景
当然, 我也不是把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) / 3
        if 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) / 5
        if n != 6 && n != 8 && n != 20 {
            goto error
        }
        // ...
    } else {
        goto error
    }
    return hw, nil
error:
    return nil, &AddrError{Err: "invalid MAC address", Addr: s}
}这种其实是可以接受的, 能够简化流程, 但是, 但是, 不要忘记我不建议使用goto最重要的一点:
- 你无法保证自己拥有掌控goto的实力
- 即使你自信, 也无法保证同事有掌控goto的实力
一旦在使用过程中产生破窗效应, 使用goto破坏的速度一定是比不用要快的多. 代码很快就会脱离掌控. 
因此, 为了避免这种情况, 最好的方式就是在最开始杜绝掉.
最后的最后, goto如果能够好好用的话, 确实能够带来一定的便利性, 前提是项目由你一人开发, 或你拥有掌控权可以拒绝某些腐败代码进入代码库. 
goto并不可怕, 可怕的是不加限制的乱用goto. 
欢迎来辩…
