goto 語句讓 Go 代碼變成意大利麪條?

語言: CN / TW / HK

大家好,我是煎魚。

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: http://dl.acm.org/doi/10.1145/362929.362947

[2]

Spaghetti code: http://en.wikipedia.org/wiki/Spaghetti_code

[3]

Goto statements: http://go.dev/ref/spec#Goto_statements

關注煎魚,獲取業內第一手消息和知識 :point_down:

你好,我是煎魚, 出版過 Go 暢銷書《Go 語言編程之旅》,再到獲得 GOP(Go 領域最有觀點專家)榮譽, 點擊藍字查看我的出書之路

日常分享高質量文章,輸出 Go 面試、工作經驗、架構設計, 加微信拉讀者交流羣,和大家交流!