12.9. 幾點忠告

雖然反射提供的API遠多於我們講到的,我們前面的例子主要是給出了一個方向,通過反射可以實現哪些功能。反射是一個強大並富有表達力的工具,但是它應該被小心地使用,原因有三。

第一個原因是,基於反射的代碼是比較脆弱的。對於每一個會導致編譯器報告類型錯誤的問題,在反射中都有與之相對應的誤用問題,不同的是編譯器會在構建時馬上報告錯誤,而反射則是在真正運行到的時候才會拋出panic異常,可能是寫完代碼很久之後了,而且程序也可能運行了很長的時間。

以前面的readList函數(§12.6)為例,為了從輸入讀取字符串並填充int類型的變量而調用的reflect.Value.SetString方法可能導致panic異常。絕大多數使用反射的程序都有類似的風險,需要非常小心地檢查每個reflect.Value的對應值的類型、是否可取地址,還有是否可以被修改等。

避免這種因反射而導致的脆弱性的問題的最好方法,是將所有的反射相關的使用控制在包的內部,如果可能的話避免在包的API中直接暴露reflect.Value類型,這樣可以限制一些非法輸入。如果無法做到這一點,在每個有風險的操作前指向額外的類型檢查。以標準庫中的代碼為例,當fmt.Printf收到一個非法的操作數時,它並不會拋出panic異常,而是打印相關的錯誤信息。程序雖然還有BUG,但是會更加容易診斷。

fmt.Printf("%d %s\n", "hello", 42) // "%!d(string=hello) %!s(int=42)"

反射同樣降低了程序的安全性,還影響了自動化重構和分析工具的準確性,因為它們無法識別運行時才能確認的類型信息。

避免使用反射的第二個原因是,即使對應類型提供了相同文檔,但是反射的操作不能做靜態類型檢查,而且大量反射的代碼通常難以理解。總是需要小心翼翼地為每個導出的類型和其它接受interface{}或reflect.Value類型參數的函數維護說明文檔。

第三個原因,基於反射的代碼通常比正常的代碼運行速度慢一到兩個數量級。對於一個典型的項目,大部分函數的性能和程序的整體性能關係不大,所以當反射能使程序更加清晰的時候可以考慮使用。測試是一個特別適合使用反射的場景,因為每個測試的數據集都很小。但是對於性能關鍵路徑的函數,最好避免使用反射。