驚了!goto 語句讓 Go 程式碼變成義大利麵條嗎?

煎魚發表於2022-06-15

大家好,我是煎魚。

Goto 語句在社群的討論中經常被人詬病,認為其破壞了結構化程式設計和程式的抽象,是有害的,可怕的。

最早的觀點來源於 1968 年,Edsger Dijkstra 寫了一封信《Go To Statement Considered Harmful》,來表達其是有害的觀念。

如下圖:

不過,但是,其實...

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(義大利麵條程式碼),指的是對非結構化和難以維護的原始碼的貶義詞。

這樣的程式碼具有複雜而糾結的控制結構,導致程式流程在概念上就像一碗義大利麵,扭曲和糾結。

參考程式碼如下:

  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》 中進行了用法的說明。

規範要求在 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

文章持續更新,可以微信搜【腦子進煎魚了】閱讀,本文 GitHub github.com/eddycjy/blog 已收錄,學習 Go 語言可以看 Go 學習地圖和路線,歡迎 Star 催更。

Go 圖書系列

推薦閱讀

相關文章