Go 在 Google:服務于軟件工程的語言設計(翻譯)(三)
原文:Go at Google: Language Design in the Service of Software Engineering
地址:https://talks.golang.org/2012/splash.article
作者:Rob Pike
翻譯:Jayce Chant(博客:jaycechant.info,公眾號ID:jayceio)
Rob Pike:Unix 小組成員,參與了 Plan 9 計劃,1992 年和 Ken Thompson 共同開發(fā)了 UTF-8。他和 Ken Thompson 也是 Go 語言最早期的設計者。

譯文較長,分三篇推送,這里是第 14~19 小節(jié)。(完)
14. 垃圾回收
對于一門系統(tǒng)級編程語言來說,垃圾回收可能是一個有爭議的特性,然而我們只花了很少時間就決定 Go 將是一門帶垃圾回收的語言。Go 沒有顯式的內存釋放操作:已分配的內存返回內存池的唯一途徑就是垃圾回收器。
這是一個很容易做出的決定,因為內存管理對一門語言的實際工作方式有著深遠的影響。在 C 和 C++ 中,編程時太多的精力都花在了內存的分配和釋放上。這樣的設計傾向于暴露本可以隱藏得很好的內存管理細節(jié);但反過來說,對內存管理的過多顧慮又限制了內存的使用。相比之下,垃圾回收使得編程接口更清晰明確(garbage collection makes interfaces easier to specify)。
此外,在支持并發(fā)的面向對象語言中,自動內存管理幾乎是必不可少的,因為當一塊內存的所有權在并發(fā)執(zhí)行中來回傳遞時,管理起來是很棘手的。將行為和資源管理分開是很重要的。
一旦有了垃圾回收,語言使用起來就容易多了。
當然,垃圾回收會帶來巨大的成本:資源開銷、執(zhí)行延遲和實現(xiàn)的復雜性。盡管如此,我們相信,主要由程序員感受到的好處,要大于主要由語言實現(xiàn)者承擔的成本。
用 Java 作為服務器開發(fā)語言的經(jīng)驗,讓一些人對面向用戶的系統(tǒng)中的垃圾回收感到緊張。開銷不可控,延遲隨時可能變大,而且為了獲得良好的性能,還需要進行很多參數(shù)調整。然而 Go 卻不同,語言的特性能緩解其中一部分的擔憂,雖然不是全部。
關鍵的一點是,Go 為程序員提供了工具,可以通過控制數(shù)據(jù)結構的布局來限制內存分配 。假設有一個簡單的數(shù)據(jù)結構的類型定義,它包含一個字節(jié)型(數(shù)組)的緩沖區(qū):
type?X?struct?{
????a,?b,?c?int
????buf?[256]byte
}
在 Java 里,buf 字段需要第二次內存分配,對它的訪問也需要第二層的間接訪問。但在 Go 里面,緩沖區(qū)和包含它的結構體一起被分配在一個內存塊中,不需要任何間接分配和訪問。對于系統(tǒng)編程來說,這種設計可以獲得更好的性能,同時減少回收器需要管理的內存塊數(shù)量。在規(guī)模化的情況下,它可以帶來顯著的差異。
舉個更直接的例子,在 Go 里面,提供二階內存分配器(second-order allocators)是很容易和很高效的,例如一個 arena 內存分配器可以一口氣分配一大組的結構體,并用一個空閑鏈表(free list)將它們連接起來。像這樣要反復使用很多小結構體的庫,只要做適當?shù)奶崆鞍才?,就可以不產(chǎn)生垃圾,還能保持高效和快速響應。
譯者注:arena 是 Go 里面用來分配內存的連續(xù)虛擬地址區(qū)域,堆中分配的內存都來自這一區(qū)域,可以近似地看作堆。Go 有自主內存管理策略(基于 Thread-Caching Malloc 改進),會一次性向系統(tǒng)預申請一大塊內存,并將空閑內存用 free list 連在一起。分配內存時會按照一定策略,根據(jù)大小優(yōu)先從 free list 獲取內存;如果對象銷毀,則把內存歸還 free list。只有空閑內存不夠才會向系統(tǒng)申請新的內存,只有空閑內存特別多才會向系統(tǒng)釋放內存,減少內存申請和釋放的系統(tǒng)調用。
這部分內容根據(jù) Go 實現(xiàn)的改進可能會發(fā)生變化,請參考最新的文章,或者直接查看源碼。
https://github.com/golang/go/blob/master/src/runtime/malloc.go
雖然 Go 是一種帶垃圾回收的語言,但是一個資深的程序員可以通過減少施加給回收器的壓力,來提高性能。(另外,Go 安裝時還附帶了很多好用的工具,可以用來分析程序運行時的動態(tài)內存性能。)
為了給程序員提供這種靈活性,Go 必須支持指向堆上分配對象的指針,我們稱之為內部指針(interior pointers)。上面例子中的 X.buf 字段就存在于結構體內部,但獲取這個內部字段的地址是合法的,例如將這個地址傳遞給一個 I/O 子程序。在 Java 以及很多支持垃圾回收的語言里,構造這樣的內部指針是不可能,但在 Go 里面,這是很自然的做法。這個設計點會影響到可以使用哪些回收算法,并且可能會增加算法的實現(xiàn)難度,但是經(jīng)過仔細考慮,我們決定有必要允許使用內部指針,因為這對程序員有好處,并且能夠減少垃圾回收器的壓力(盡管這樣可能會讓垃圾回收器更難實現(xiàn))。到目前為止,我們將類似的 Go 和 Java 程序進行對比的經(jīng)驗表明,使用內部指針可以對總的 arena 大小、執(zhí)行延遲 和 回收時間產(chǎn)生顯著影響。
總而言之,Go 支持垃圾回收,但給程序員提供了一些工具來控制回收開銷。
垃圾回收器仍然是一個活躍的開發(fā)領域。目前的設計是一個并行的標記并清理(mark-and-sweep)回收器,仍然有機會改進它的性能甚至設計。(語言規(guī)范并沒有規(guī)定回收器必須要使用任何特定實現(xiàn)。) 不過,如果程序員注意更巧妙地使用內存,目前的實現(xiàn)已經(jīng)可以在生產(chǎn)環(huán)境工作得很好。
譯者注:Go 1.3 以前使用 mark-and-sweep 回收器,整個過程需要 STW(stop the world),對于內存的申請和釋放量比較大和頻繁的程序而言,回收造成的停頓會比較明顯。
后續(xù)的版本逐漸分離標記和清理過程,引入三色標記法,還有引入混合寫屏障??偟内厔菔菍?GC 分散成多個可以(跟程序執(zhí)行)并發(fā)的過程,將不得不 STW 的階段和時間壓縮到最?。ㄍǔP∮?1ms),跟演講發(fā)表時相比已經(jīng)有了很大的改善。
15. 組合,而不是繼承
Go 采用了一種不同尋常的面向對象編程方法,它允許在任何類型上添加方法,而不僅僅是類;但沒有任何形式的基于類型的繼承,比如子類。這意味著沒有類型層次體系(type hierarchy)。這是一個有意的設計選擇。雖然類型體系已經(jīng)被用來構建了很多成功的軟件,但我們認為這個模型已經(jīng)被過度使用,應該后退一步。
取而代之的是 Go 的接口,這個想法在其他地方已經(jīng)被詳細討論過了(例如參見research.swtch.com/interfaces),但這里還是做一個簡單的總結。
在 Go 里面,一個接口 僅僅 是一組方法的集合。例如,這里是標準庫中 Hash 接口的定義:
type?Hash?interface?{
????Write(p?[]byte)?(n?int,?err?error)
????Sum(b?[]byte)?[]byte
????Reset()
????Size()?int
????BlockSize()?int
}
所有實現(xiàn)這些方法的數(shù)據(jù)類型都隱式地滿足這個接口,沒有 implements 聲明。盡管如此,是否滿足接口是在編譯期靜態(tài)檢查的,所以接口是類型安全的。
一個類型通常會滿足許多接口,每個接口對應于其方法的一個子集。例如,任何滿足 Hash 接口的類型也會滿足 Writer 接口:
type?Writer?interface?{
????Write(p?[]byte)?(n?int,?err?error)
}
這種接口滿足的流動性鼓勵了一種不同的軟件構造方法。但在解釋這個之前,我們應該先解釋一下為什么 Go 沒有子類。
面向對象編程提供了一個強大的洞見:數(shù)據(jù)的行為可以獨立于數(shù)據(jù)的表示進行泛化(generalized)。 當行為(方法集)是固定的時候,這個模型的效果最好,但是一旦你對一個類型進行了子類化,并添加了一個方法,行為就不再相同。相反地如果行為集是固定的,就好像 Go 靜態(tài)定義的接口那樣,行為的統(tǒng)一性使得數(shù)據(jù)和程序可以統(tǒng)一、正交、安全地組合。
一個極端的例子是 Plan 9 內核,所有的系統(tǒng)數(shù)據(jù)項都實現(xiàn)了完全相同的接口,即由 14 個方法定義的文件系統(tǒng) API。這種統(tǒng)一性允許的對象組合水平,即使在今天也極少能在其它系統(tǒng)上看到。這樣的例子比比皆是。這里還有一個:一個系統(tǒng)可以將 TCP 協(xié)議棧導入(import,這是 Plan 9 的術語)到一臺沒有 TCP 甚至沒有以太網(wǎng)(Ethernet)的計算機上,然后通過這個網(wǎng)絡連接到一臺 CPU 架構不同的機器上,導入它的 /proc 樹,并運行一個本地調試器對遠程進程進行斷點調試。這種操作在 Plan 9 上簡直稀松平常,根本沒有任何特別之處。做這種事情的能力完全來自它的設計,不需要特殊的安排(而且都是用普通 C 語言代碼完成的)。
我們認為,這種組合式的系統(tǒng)構造風格已經(jīng)被那些推崇按類型體系設計的語言所忽視。類型體系會造就脆弱易碎的代碼。 體系結構必須在早期設計,通常是作為設計程序的第一步,而一旦程序寫好就很難改動早期的決定。因此,該模型鼓勵在早期做過度設計,程序員試圖預測軟件可能需要的每一種使用方式,增加多個類型和抽象層,僅僅為了以防萬一。這是本末倒置的做法。系統(tǒng)各個部分的交互方式應該隨著系統(tǒng)的發(fā)展去適配,而不是在一開始就固定下來。
因此,Go 鼓勵組合而不是繼承,使用簡單的、通常只有一個方法的接口來定義瑣碎的行為,作為組件之間干凈、可理解的邊界。
上面提到的 Writer 接口,它被定義在 io 包里:任何有 Write 方法的類型,只要有以下這個方法簽名,就可以和補充的 Reader 接口一起工作:
type?Reader?interface?{
????Read(p?[]byte)?(n?int,?err?error)
}
這兩個互補的方法可以類型安全地跟豐富的行為進行連接(chaining),就像通用的 Unix 管道(pipes)一樣。文件、緩沖區(qū)、網(wǎng)絡、加密器、壓縮器、圖像編碼器等都可以連接在一起。格式化 I/O 子程序 Fprintf 采用一個 io.Writer 接口作為參數(shù),而不是像在 C 語言里那樣采用 FILE* 。格式化輸出程序并不了解內容是寫到了哪里,它可能是一個圖像編碼器,背后又輸出給一個壓縮器,壓縮器又輸出給一個加密器,加密器又輸出給一個網(wǎng)絡連接。
接口組合是一種不同的編程風格,習慣了類型層次體系的人需要調整思路才能適應,但這樣可以獲得設計的適應性,這是通過類型體系很難實現(xiàn)的。
還要注意的是,消除類型層次結構也消除了一種形式的依賴層次結構。接口的滿足允許程序有機地生長,而不需要預先確定的合約。而且它是一種線性的增長形式,對一個接口的改變只影響該接口的直接用戶,不需要再更新子樹。缺乏 implements 聲明會讓一些人感到不安,但它能讓程序自然、優(yōu)雅、安全地生長。
Go 的接口對程序設計有重大影響。其中一個地方是用接口作為參數(shù)的函數(shù)的使用。這些不是方法,而是函數(shù)。一些例子應該可以說明它們的力量。ReadAll 返回一個字節(jié)切片(數(shù)組),包含了所有可以從 io.Reader 接口讀取的數(shù)據(jù):
func?ReadAll(r?io.Reader)?([]byte,?error)
封裝器(指接受一個接口參數(shù)并返回一個接口的函數(shù))的使用也很普遍。下面是一些原型。LoggingReader 記錄傳入的 Reader 的每個 Read 調用。LimitingReader 在讀取 n 個字節(jié)后停止。ErrorInjector 通過模擬 I/O errors 來輔助測試。我們還能找到更多例子。
func?LoggingReader(r?io.Reader)?io.Reader
func?LimitingReader(r?io.Reader,?n?int64)?io.Reader
func?ErrorInjector(r?io.Reader)?io.Reader
這些設計與分層的、子類型繼承的方法完全不同,它們是更松散的(甚至是臨時的)、有機的、解耦的、獨立的,因此是可彈性伸縮的。
16. 錯誤處理
Go 沒有傳統(tǒng)意義上的異常機制,也就是說,沒有與錯誤處理相關的控制結構。(Go 確實提供了處理異常情況(例如除零異常)的機制。一對名為 panic 和 recover 的內置函數(shù)允許程序員處理類似的情況。然而,這些函數(shù)是故意設計得不好用,也很少使用,而且沒有像 Java 庫使用異常那樣集成到代碼庫中。)
錯誤處理的關鍵語言特性是一個預先定義的接口類型 error ,它代表了一個有 Error 方法可以返回字符串的值:
type?error?interface?{
????Error()?string
}
代碼庫使用 error 類型來返回錯誤的描述。結合函數(shù)多值返回的能力,很容易將計算結果與錯誤值(如果有)一起返回。例如,Go 里等價于 C 的 getchar 的函數(shù)不會在遇到 EOF 時返回一個超出范圍的值,也不會拋出一個異常;它只是在字符旁返回一個 error 值, nil error 值表示成功。下面是緩沖 I/O 包的 bufio.Reader 接口類型的 ReadByte 方法的簽名:
func?(b?*Reader)?ReadByte()?(c?byte,?err?error)
這是一個簡單清晰的設計,很容易理解。錯誤只是值,程序用它們來計算,就像用任意其他類型的值來計算一樣。
在 Go 中不加入異常是一個刻意的選擇。雖然有很多批評者不同意這個決定,但有幾個原因讓我們相信它可以讓軟件變得更好。
首先,計算機程序中的錯誤并不是真的『異常』(nothing truly exceptional,譯者注:也可以翻譯成:沒有什么特別,平平無奇,這里翻譯成異常,是為了跟 exception 的中文術語對應)。例如,無法打開文件是一個常見的問題,不值得使用特殊的語言結構;if 和 return 就可以了:
f,?err?:=?os.Open(fileName)
if?err?!=?nil?{
????return?err
}
另外,如果使用特殊的控制結構,錯誤處理會扭曲(distorts)程序處理錯誤的控制流(control flow)。Java 風格的 try-catch-finally 塊跟多個重疊的控制流互相交錯,而這些控制流本身的交互就很復雜。相比之下,雖然 Go 使代碼在檢查錯誤時更加啰嗦,但顯式的設計使控制流保持了真正的(literally)簡單直接。
毫無疑問,由此產(chǎn)生的代碼可能會更長,但這種代碼的清晰和簡單可以彌補它的啰嗦。明確的錯誤檢查迫使程序員在錯誤出現(xiàn)時就考慮并處理它們。 異常太容易讓人們忽略而不是處理它們,將責任推給調用棧,直到為時已晚,無法很好地修復乃至診斷問題。
17. 工具
軟件工程需要工具。每一種語言都是在一個有其他語言和大量工具的環(huán)境中運行,這些工具用來編譯、編輯、調試、性能分析、測試和運行程序。
Go 的語法、包系統(tǒng)、命名慣例和其他特性在設計時就已經(jīng)將工具易于編寫考慮在內,庫里面包括了 Go 的詞法分析器、解析器和類型檢查器。
控制 Go 程序的工具非常容易編寫,以至于這樣的工具現(xiàn)在已經(jīng)有很多,有些工具對軟件工程產(chǎn)生了很有趣的影響。
其中最著名的是 gofmt ,Go 的源代碼格式化工具。從項目一開始,我們就打算用機器來格式化 Go 程序代碼,從而消除程序員之間爭論的一整個問題分類:該如何排版代碼?gofmt 運行在我們編寫的所有 Go 程序上,大多數(shù)開源社區(qū)也在用它。它是作為代碼倉庫的『提交前(presubmit)』檢查來運行的,以確保所有檢入(check-in)的 Go 程序格式都是一樣的。
gofmt 經(jīng)常被用戶推崇為 Go 最好的特性之一,盡管它根本不是 Go 語言的一部分。gofmt 的存在和使用意味著,從一開始,社區(qū)里看到的代碼總是按照 gofmt 的格式,所以 Go 程序有一個現(xiàn)在大家都很熟悉的統(tǒng)一風格。統(tǒng)一的表現(xiàn)形式使得代碼更容易閱讀,因此工作起來也更快。不用花時間格式化代碼,時間就可以節(jié)省下來干別的。gofmt 還影響了可伸縮性:既然所有的代碼看起來都是一樣的,團隊就更容易一起合作,也更容易使用其他人的代碼。
譯者注:
這個功能雖然看起來不起眼,但在實際的團隊開發(fā)中其實是很實用的。在使用別的沒有統(tǒng)一風格的語言時,總是要為統(tǒng)一團隊的代碼風格付出額外的精力(尤其是有新成員加入時)。
我們要么給團隊制定統(tǒng)一的風格規(guī)范,并落實到每個人(最好使用格式化插件并應用相同的配置);要么忍受代碼里同時存在好幾種風格穿插,影響閱讀。
更糟糕的情況是,幾個人都啟用了格式化插件,卻應用了不同的配置,先后修改同一份代碼,提交時就很容易出現(xiàn)大量差異乃至沖突,實際上僅僅是代碼風格的差異。這些無關緊要的差異如果不小心提交到倉庫,真正重要的修改就將被淹沒其中,干擾我們日后查看歷史分析問題。
gofmt 還使另一類我們之前沒有清晰預見到的工具得以實現(xiàn)。gofmt 的工作原理是解析源代碼,并從解析樹本身重新格式化。這使得在格式化之前可以編輯解析樹,于是一套自動重構工具應運而生。這些工具很容易編寫,由于它們直接在解析樹上工作,所以語義豐富,可以自動生成規(guī)范格式化的代碼。
第一個例子是 gofmt 本身的 -r (rewrite)標志參數(shù),它使用簡單的模式匹配語言來實現(xiàn)表達式級別的重寫。比如有一天,我們?yōu)榍衅磉_式的右側引入了一個默認值:切片本身的長度。只需一條命令,整個 Go 源代碼樹就被更新為使用這個默認值:
gofmt?-r?'a[b:len(a)]?->?a[b:]'
這個轉換的一個關鍵點是,因為輸入和輸出都是規(guī)范格式,所以對源代碼所做的唯一改變是語義上的改變。
一個類似但更復雜的處理是,當 Go 語言里以換行結束的語句,不再需要分號作為終止符時, gofmt 可以用來更新源碼樹。
另一個重要的工具是 gofix,它可以運行用 Go 本身編寫的『源碼樹重寫模塊(tree-rewriting modules)』,因此能夠進行更高級的重構。gofix 工具讓我們在 Go 1 發(fā)布之前對 API 和 語言特性 進行了全面的修改,包括修改 map 刪除條目的語法,為操作時間值引入全新的 API ,等等。隨著這些變化的推出,用戶只需要運行簡單的命令就能更新他們的所有代碼:
gofix
請注意,這些工具允許我們,在舊代碼仍然可以正常工作的前提下,更新代碼。因此,Go 的代碼倉庫很容易隨著庫的演化保持更新。舊的 API 可以快速自動地被廢棄,因此只需要維護一個版本的 API。例如,我們最近改變了 Go 的協(xié)議緩沖區(qū)實現(xiàn),改為使用 "getter" 函數(shù),而之前的接口并沒有這些函數(shù)。我們在 Google 所有 Go 代碼上運行 gofix 來更新所有使用協(xié)議緩沖區(qū)的程序,現(xiàn)在只有一個版本的 API 在使用。在 Google 的代碼庫規(guī)模下,對 C++ 或 Java 庫進行類似的全面修改幾乎是不可能實現(xiàn)的。
Go 標準庫里的解析包,讓其他一些工具也得以實現(xiàn)。例如 go 工具,它可以管理程序的構建,包括從遠程代碼倉庫獲取包;godoc 文檔提取器,是一個驗證 API 兼容性合約是否隨著庫的更新而得到維護的程序;等等。
雖然像這樣的工具在語言設計中很少被提及,但它們是語言生態(tài)系統(tǒng)中不可缺少的一部分,事實上,Go 在設計時就考慮到了工具的問題,這對語言、庫和社區(qū)的發(fā)展都有巨大的影響。
18. 結論
Go 在 Google 內部用得越來越多。
一些面向用戶的大型服務都在使用它,包括 youtube.com 和 dl.google.com (提供 Chrome、Android 和其他下載的下載服務器),以及我們自己的 golang.org 。當然也有很多小的服務,大多是使用 Google App Engine 對 Go 的原生支持構建的。
很多其他公司也在使用 Go;這個名單很長,但其中比較著名的幾個是:
BBC Worldwide Canonical Heroku Nokia SoundCloud
看來,Go 正在實現(xiàn)它的目標。不過,現(xiàn)在宣布成功還為時過早。我們還沒有足夠的經(jīng)驗,尤其是在大型程序(數(shù)百萬行代碼那種)方面的經(jīng)驗,去斷言我們已經(jīng)成功創(chuàng)造了一門彈性可伸縮的語言。盡管所有的指標都是正面的。
在較小的范圍內,一些小事情還不夠好,可能會在 Go 以后的版本里微調(Go 2?)。例如,變量聲明語法的形式太多,程序員很容易被非 nil 接口里面的 nil 值的行為搞糊涂,還有很多庫和接口的細節(jié)可以再進行一輪設計。
不過值得注意的是, gofix 和 gofmt 在 Go 1 的前期給了我們修復許多其他問題的機會。正因為有這些工具,Go 在今天得以更接近它的設計者的期待,而這些工具本身也是由于語言的設計才得以實現(xiàn)。
不過,不是所有事情都已經(jīng)確定。我們還在學習中(但語言暫時是凍結的)。
譯者注:根據(jù)譯者的理解,這里的語言凍結,應該是指為了兌現(xiàn) Go 1 backwards compatibility 的承諾,Go 1.x 的 API 已經(jīng)基本固定,后續(xù)只會新增特性和對現(xiàn)有特性做兼容的微調,更多是在底層實現(xiàn)上做改進。破壞兼容性的修改,只能等到 Go 2。
Go 語言的一個主要的弱點,是它的實現(xiàn)仍需努力改進。編譯器生成的代碼,尤其是運行時的性能應該更好,這方面的工作還在繼續(xù)。目前已經(jīng)取得了一些進展;事實上,一些基準測試顯示,與 2012 年初發(fā)布的第一版 Go 相比,當前開發(fā)版的性能已經(jīng)翻了一番。
19. 小結
軟件工程指導了 Go 的設計。與大多數(shù)通用編程語言相比,Go 的設計是為了解決我們在構建大型服務器軟件時接觸到的一系列軟件工程問題。這么一說,這可能會讓 Go 聽起來相當沉悶和工業(yè)化,但事實上,在整個設計過程中,對清晰、簡單和可組合性的關注反而導致了一門工作效率高且有趣的語言,很多程序員都覺得它表達力強而且功能強大。
造就這個結果的特性包括:
清晰的依賴關系 清晰的語法 清晰的語義 組合而非繼承 編程模型提供的簡單性(垃圾回收、并發(fā)) 易用的工具( go工具、gofmt、godoc、gofix)
如果你還沒有嘗試過 Go,我們建議你去嘗試:
https://golang.org
譯者小結:
一萬八千多字,終于翻譯完了。
一開始我沒有留意原文的字數(shù),以為最多花兩個晚上,就能翻譯完。實際上,如果不涉及那么多專業(yè)概念,沒有那么多上下文省略和帶歧義的表述,這個長度兩晚也是可以勉強完成的。
可本文就是有很多地方,需要有相關的背景知識,無法單純從原文確定作者的準確意思。沒辦法,有歧義又了解不夠的地方,只好查資料、翻源碼,猜測原作者最大可能想表達什么。所以導致翻譯進度又慢又累。而即使這樣,如開頭所說,仍然無法避免會有理解偏差和錯誤。
這樣一來,對之前的譯文就變得更加寬容了。筆誤和排版混亂仍然不應該。但那些在我看來像是顯而易見的錯誤,也許只是剛好落入了我的知識范圍;而我的譯文里,可能也有落在我知識盲區(qū)最后只好瞎蒙的地方,成為別人眼里的低級錯誤。
歡迎留言指出錯誤,或者提出你不同的見解。
推薦閱讀

