1. 程式人生 > >修改Go語言(golang)編譯器原始碼讓它支援UTF-8 BOM

修改Go語言(golang)編譯器原始碼讓它支援UTF-8 BOM

  Go語言(golang)第一個正式版Go1釋出了,但是這個新興的程式語言還是非常不完善。這不,我(Liigo)又發現它的編譯器竟然不支援編譯帶BOM的UTF-8編碼的.go原始檔。這就很奇怪,該語言明明要求原始碼檔案.go必須是UTF-8編碼,但又不允許帶UTF-8 BOM。要知道,這個世界上帶BOM的檔案太多了,很多文字編輯器/程式碼編輯器/IDE支援生成、甚至預設生成帶有BOM的UTF-8檔案。如果僅僅因為原始碼檔案多了BOM,編譯器就不能編譯這個檔案,那也太低能了。

Go語言編譯器(gc)不支援帶有BOM的UTF-8原始檔
Golang's compiler (gc) don't accept the .go files with UTF-8 BOM: 

E:\liigo\golang\src>go run hello.go
package :
hello.go:1:1: illegal character U+FEFF

E:\liigo\golang\src>go run hello.go
# command-line-arguments
.\hello.go:9: illegal UTF-8 sequence
ce d2

  好在Go語言是開源專案,我(Liigo)來貢獻程式碼,讓它支援編譯帶UTF-8 BOM的.go原始碼檔案。經過分析後發現,Go語言編譯器(gc)原始碼中有兩處地方涉及從磁碟檔案中讀取.go檔案:一個是C語言寫的詞法分析器(src/cmd/gc/lex.c),一個是Go語言寫的.go檔案解析器(src/pkg/go/parser/interface.go)。解決思路也很簡單,就是從磁碟讀取檔案內容後,判斷前三個位元組,與UTF-8 BOM的三個位元組(0xef, 0xbb, 0xbf)核對,如果一致則忽略這三個位元組,從第四個位元組算作檔案的真正內容,然後再交給詞法分析器和解析器處理,後面就一切正常了。

  Go語言的詞法分析器是用C語言手工編寫的,其中用到了lib9/libbio庫,是一個磁碟檔案讀寫緩衝區,逐位元組從該緩衝區讀取資料時,它可以允許“反讀取”最近的最多4個位元組。就是說,它可以把吐出來的最後那個位元組再吸回去。假設我剛剛讀取了abcd是個位元組,現在我“反讀取”後兩個位元組,實際上就相當於我剛剛讀取了ab還沒有讀取cd。利用它的這個“反讀取”機制,恰恰可以很容易的忽略掉UTF-8檔案最前面的BOM:首先讀出前三個位元組,如果這三個位元組正好是UTF-8 BOM的三個位元組(0xef, 0xbb, 0xbf),那麼直接把剛剛讀出的三個位元組扔掉就完事了,後面詞法分析器處理時正好從BOM後面的位元組開始讀取;如果已經讀出的三個位元組不是UTF-8 BOM呢,需要“反讀取”,即把他們再放回去,就當作我沒有讀取過它們。修改後的程式碼如下:

// src/cmd/gc/lex.c : 
  319                 // Try to read and ignore UTF-8 BOM
  320                 c1 = Bgetc(curio.bin);
  321                 c2 = Bgetc(curio.bin);
  322                 c3 = Bgetc(curio.bin);
  323                 if(c1 != 0xef || c2 != 0xbb || c3 != 0xbf) {
  324                         // If not UTF-8 BOM, restore the bytes.
  325                         // Bungetsize > 3, so we can safely call Bungetc() 3 times.
  326                         Bungetc(curio.bin);
  327                         Bungetc(curio.bin);
  328                         Bungetc(curio.bin);
  329                 }

  Go語言的原始碼解析器(pkg/go/parser)是用Go語言自己編寫的,其功能是解析.go原始碼檔案為語法樹。Go語言官方提供的go build命令用pkg/go/parser分析處理編譯前的庫依賴項。go build命令(pkg/go/parser)是把.go檔案整個讀入記憶體後再解析的。我要做的工作就是,在pkg/go/parser開始正式解析前,把前面可能存在的UTF-8 BOM刪除掉即可,這個工作僅僅涉及Go語言中byte slice的基本操作,是很輕量級的廉價操作。

// src/pkg/go/parser/interface.go : 
+// The data read from .go files maybe start with the UTF-8 BOM(byte order mark),
+// we ignore the bytes here to make sure that the parser parses properly.
+//
+func ignoreUTF8BOM(data []byte) []byte {
+	if data == nil {
+		return nil
+	}
+	if len(data) >= 3 && data[0] == 0xef && data[1] == 0xbb && data[2] == 0xbf {
+		return data[3:]
+	}
+	return data
+}
+
 // If src != nil, readSource converts src to a []byte if possible;
 // otherwise it returns an error. If src == nil, readSource returns
 // the result of reading the file specified by filename.
@@ -27,22 +40,23 @@
 		case string:
 			return []byte(s), nil
 		case []byte:
-			return s, nil
+			return ignoreUTF8BOM(s), nil
 		case *bytes.Buffer:
 			// is io.Reader, but src is already available in []byte form
 			if s != nil {
-				return s.Bytes(), nil
+				return ignoreUTF8BOM(s.Bytes()), nil
 			}
 		case io.Reader:
 			var buf bytes.Buffer
 			if _, err := io.Copy(&buf, s); err != nil {
 				return nil, err
 			}
-			return buf.Bytes(), nil
+			return ignoreUTF8BOM(buf.Bytes()), nil
 		}
 		return nil, errors.New("invalid source")
 	}
-	return ioutil.ReadFile(filename)
+	fileData, err := ioutil.ReadFile(filename)
+	return ignoreUTF8BOM(fileData), err
 }

  但是Go語言的作者/官方開發者拒絕這一改進。Go開發組老大Rob Pike親自回覆給出拒絕的理由:

Strictly speaking, a BOM is legal in UTF-8 but only as a marker for
the type of the data stream, a magic number if you will. Since Go
source code is required to be UTF-8, a BOM is never necessary and
arguably erroneous. We've come this far without accepting BOMS and I'd
like to keep it that way.

  在我看來,這理由非常勉強,在邏輯上甚至都不成立。哦,既然規定了.go必須使用UTF-8編碼,所以就一定不能加UTF-8 BOM了?加了BOM就拒不認可了?前面已經說過了,幾乎所有文字編輯器都支援UTF-8 BOM,怎麼到你這裡就不合法了。一個很現實的例子是,Windows XP / Windows 7系統內建的“記事本”程式(notepad.exe)在儲存UTF-8檔案時是一定會自動新增UTF-8 BOM的。也就是說,你想用記事本儲存的.go原始碼是一定不能編譯通過的。但就是這麼嚴重的問題,Go作者們就是不當一回事;這麼容易就可以改進的問題,他們就是拒絕改進。生生的為使用者使用go語言又憑空製造一道阻力。要說是面臨技術方案的妥協選擇折中還可以理解,偏偏要在無關緊要的地方堅持己見、寧死不去考慮方便使用者。我只能說他們是死腦筋。類似的情況我遇到也不止一次了,總結下來就是:技術大牛掛帥做產品害人害己;閉門造車的代價是死都不知道咋死的。(這樣的總結也為我們做易語言產品敲響了警鐘:絕對不能單純以技術人員的心態做產品。)

---------------------------------------------------------------------------------------------------

2013-7-15 Liigo 補記

2013年5月13日釋出的 golang 1.1 終於支援帶UTF-8 BOM的原始碼檔案了

"The Unicode byte order mark U+FEFF, encoded in UTF-8, is now permitted as the first character of a Go source file."
http://golang.org/doc/go1.1#unicode

這一日,距離Go語言開發組老大Rob Pike當年拒絕UTF-8 BOM那番話發表(2012-4-17,內容見上文),已過去一年多,可能他已經忘記自己曾經說過什麼。

而且他們對原始碼的修改思路也是跟我一致的(都修改了lex.c同一處;我雖然沒有直接修改scanner,但通過修改parser也間接達到了目的):

cc2bca9c03ef by Rob Pike, 2012-9-10: gc: initial BOM is legal

3d58333e8e2a by Russ Cox, 2012-10-7: cmd/gc: skip over reported BOMs

4245c8cdc599 by Robert Griesemer, 2012-9-7: go/scanner: skip first character if it's a BOM

30444b809a9e by Robert Griesemer, 2013-4-12: go/scanner: reject BOMs that are not at the beginning

看到沒!三個作者(包括兩位老大)先後修改提交了至少4次,前後跨度7個月,才總算達到了我一次性修改提交原始碼的效果。(本人當時初學Go語言不久,自我感覺技術很爛!我在易語言公司就技術水平而言也是墊底的。)

---------------------------------------------------------------------------------------------------

2014-3-18 Liigo 補記

歷史總是驚人的相似,時隔兩年之後,我(Liigo)又修改了Rust程式語言編譯器的原始碼,為其增加了支援UTF-8 BOM的功能。與Go語言官方開發組形成鮮明對比的是,Rust官方開發人員很爽快的採納了我這個Pull Request#12976:libsyntax: librustdoc: ignore utf-8 BOM in .rs files。


---------------------------------------------------------------------------------------------------

2015-2-5 Liigo 更新:稍作文字修正和潤色。