goto 語句讓 Go 代碼變成意大利麪條?
大家好,我是煎魚。
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
推薦閲讀
參考資料
Go To Statement Considered Harmful: https://dl.acm.org/doi/10.1145/362929.362947
Spaghetti code: https://en.wikipedia.org/wiki/Spaghetti_code
Goto statements: https://go.dev/ref/spec#Goto_statements
關注煎魚,獲取業內第一手消息和知識 :point_down:
你好,我是煎魚, 出版過 Go 暢銷書《Go 語言編程之旅》,再到獲得 GOP(Go 領域最有觀點專家)榮譽, 點擊藍字查看我的出書之路 。
日常分享高質量文章,輸出 Go 面試、工作經驗、架構設計, 加微信拉讀者交流羣,和大家交流!
- Go 代碼風格沒人喜歡?不對,Gofmt 是所有人的最愛...
- 10 張圖解|高併發分佈式架構演進
- Go 只會 if err != nil?這是不對的,分享這些優雅的處理姿勢給你!
- 中美程序員不完全對比
- Go 常量只支持基本數據類型?為什麼?社區撕了 9 年了...
- Go1.19 那些事:國產芯片、內存模型等新特性,你知道多少?
- 有人問你後端面試考哪些?把這篇扔給他!
- Go 內聯優化:如何讓我們的程序更快?
- goto 語句讓 Go 代碼變成意大利麪條?
- Go 錯誤處理中再套個娃,能解決煩惱不?
- 10 條 Go 官方諺語,你知道幾條?
- 新提案:創建 Go 簡單類型的指針表達式
- 為什麼 Go 語言能在中國這麼火?
- 好物分享:快速找到 Goroutine 泄露的地方
- 新提案:增加標準庫 Context 的取消 API
- Go 之父:聊聊我眼中的 Go 語言和環境
- 太瘋狂了,Go 程序説 nil 不是 nil...
- Go 返回值命名還有存在的必要嗎?
- Go 要違背初心嗎?新提案:手動管理內存
- 開啟 Go 泛型時代:第三方泛型庫分享