goto 语句到底坑不坑?

365次阅读  |  发布于2年以前

Goto 语句在社区的讨论中经常被人诟病,认为其破坏了结构化编程和程序的抽象,是有害的,可怕的,是一种糟粕。

最早的观点来源于 1968 年,Edsger Dijkstra 写了一封信《Go To Statement Considered Harmful[1]》,来表达其是有害的观念。

如下图:

不过,但是,其实...

Go 支持了 goto 语句,很多人不理解,大喊 less is more 的 Go Team 居然加了...

今天就由煎鱼带大家看看。

Goto 语法

Goto 的语法格式,如下:

goto label
...
...
label: statement

代码案例,如下:

package main

import "fmt"

func main() {
    learnGoTo()
}

func learnGoTo() {
    fmt.Println("a")
    goto FINISH
    fmt.Println("b")
FINISH:
    fmt.Println("c")
}

上述代码在函数 learnGoTo 中先输出了 a,然后到了 goto FINISH 代码段,因此直接跳到了 c 的输出,所以 b 的输出代码被直接跳过。

输出结果:

a
c

Goto 的危害

Goto 的危害所带来的一个经典名称是:Spaghetti code[2](意大利面条代码),指的是对非结构化和难以维护的源代码的贬义词。

这样的代码具有复杂而纠结的控制结构,导致程序流程在概念上就像一碗意大利面,扭曲和纠结。

参考代码如下:

  INPUT "How many numbers should be sorted? "; T
  DIM n(T)
  FOR i = 1 TO T
    PRINT "NUMBER:"; i
    INPUT n(i)
  NEXT i
  'Calculations:
  C = T
 E180:
  C = INT(C / 2)
  IF C = 0 THEN GOTO C330
  D = T - C
  E = 1
 I220:
  f = E
 F230:
  g = f + C
  IF n(f) > n(g) THEN SWAP n(f), n(g)
  f = f - C
  IF f > 0 THEN GOTO F230
  E = E + 1
  IF E > D THEN GOTO E180
 GOTO I220
 C330:
  PRINT "The sorted list is"
  FOR i = 1 TO T
    PRINT n(i)
  NEXT i

上面这个例子,你能看到 goto 语句能够在任意控制流中到处流转,你可能还得记住它的标签是什么,跳到哪里。

程序员还要起出各种名字,例如:煎鱼哥哥、煎鱼弟弟、煎鱼朋友。起名的灵感是贫乏的,很容易混乱。

真实世界中长期发展的业务代码,滥用 goto 语句可能会更严重。

Goto 存在的意义

Go Spec

实际上在 Go 中,Goto 语句与其他语言相比有着更加严格的限制,在 Go Spec 《Goto statements[3]》 中进行了用法的说明。

规范要求在 goto 语句的作用域范围内不能有任何变量声明等动作,是坏味道。

如下代码:

 goto L  // BAD
 v := 3
L:

因为这会导致变量 v 的声明被跳过。

同时要求代码块外的 goto 语句不能跳转到另外一块代码块内的标签。

如下代码:

if n%2 == 1 {
 goto L1
}
for n > 0 {
 f()
 n--
L1:
 f()
 n--
}

不能从 if 代码块横跨作用域到 for 代码块。

Go 标准库源码例子

可以看看 Go 标准库中的 math/gamma.go 源代码,是一个很不错的案例。

如下代码:

for x < 0 {
    if x > -1e-09 {
      goto small
    }
    z = z / x
    x = x + 1
  }
  for x < 2 {
    if x < 1e-09 {
      goto small
    }
    z = z / x
    x = x + 1
  }

  if x == 2 {
    return z
  }

  x = x - 2
  p = (((((x*_gamP[0]+_gamP[1])*x+_gamP[2])*x+_gamP[3])*x+_gamP[4])*x+_gamP[5])*x + _gamP[6]
  q = ((((((x*_gamQ[0]+_gamQ[1])*x+_gamQ[2])*x+_gamQ[3])*x+_gamQ[4])*x+_gamQ[5])*x+_gamQ[6])*x + _gamQ[7]
  return z * p / q

small:
  if x == 0 {
    return Inf(1)
  }
  return z / ((1 + Euler*x) * x)
}

自上而下观察观察代码时,能够更快的识别到 goto 语句,并看到下方的标签跳转处,在实现和可读性上都是可以接受的。

意义

说到这里,有的同学可能会发现。出问题,更多是在没有限制的情况下,那 goto 到处乱飞,当然是不合理的。

图来自网络但这其实又两派观点,就如我们之前文章的读者所提到:

可以怪程序员写出意大利面条,也可以寄望语言层面规避,这样可以做的更好,不需要每一个新来的程序员都要重新培养意识。

Go 也会在 break 中支持标签跳转,与 goto 的用法是相似的:

Loop:
    for {
        select {
            ...
            break Loop
        }
    }

Go Team 显然选择了语言层面去规避 goto 的部分复杂场景,约束了只能在一个代码块进行 goto 跳转,这样能够拥有更好的可读性,也能得到相应的价值。

总结

一个新的关键字的产生,必然包含其背景的原因和行为。如果只是一味地一刀切,最后肯定会解决了个寂寞。

经过这近 60 年的计算机行业的 goto 知识熏陶和思考,大家已经认识到 goto 在任意控制流中乱跳是非常恶心的。包括世界上最好的语言 PHP,其实在 5.3.0 起,也慎重的加入了 goto,也是带限制的,范围是同一个文件和作用域。

新的 goto 形态,是这种带限制的 goto 模式的探索。你觉得怎么样?

If you need to go to somewhere, goto is the way to go. —— Ken Thompson

参考资料

[1]Go To Statement Considered Harmful: https://dl.acm.org/doi/10.1145/362929.362947

[2]Spaghetti code: https://en.wikipedia.org/wiki/Spaghetti_code

[3]Goto statements: https://go.dev/ref/spec#Goto_statements

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8