幾年前,我負責重寫一個圖像處理服務。為了弄清楚對於給定的圖像和一個或多個轉換(調整大小、圓形裁剪、修改格式等),我的新服務創建的輸出是否和舊服務一致,我必須自己檢查圖像。顯然,我需要自動化,但我找不到一個現有的 Python 庫可以告訴我,這兩張圖片在像素級上有什麼不同,因此有了diffimg ,它可以給你一個差異比 / 百分比,或生成差異圖像(檢出 readme,裡面有一個例子)。
我最初是用 Python 實現的(我最熟悉的語言),主要部分使用了 Pillow 。它可以用作庫或命令行工具。程序的基本部分非常小,只有幾十行,這要感謝Pillow。構建這個工具並不費力( xkcd 是對的,幾乎所有東西都有一個 Python 模塊),但它至少對於除我自己之外的幾十人是有用的。
幾個月前,我加入了一家公司,他們有幾個服務是用 Go 編寫的,我需要快速上手這門語言。編寫diffimg-go 看起來很有趣,甚至可能是一種有用的方法。這裡有一些來自經驗的興趣點,以及一些在工作中使用它的興趣點。
一、對比 Python 和 Go
幾年前,我負責重寫一個圖像處理服務。為了弄清楚對於給定的圖像和一個或多個轉換(調整大小、圓形裁剪、修改格式等),我的新服務創建的輸出是否和舊服務一致,我必須自己檢查圖像。顯然,我需要自動化,但我找不到一個現有的 Python 庫可以告訴我,這兩張圖片在像素級上有什麼不同,因此有了diffimg ,它可以給你一個差異比 / 百分比,或生成差異圖像(檢出 readme,裡面有一個例子)。
我最初是用 Python 實現的(我最熟悉的語言),主要部分使用了 Pillow 。它可以用作庫或命令行工具。程序的基本部分非常小,只有幾十行,這要感謝Pillow。構建這個工具並不費力( xkcd 是對的,幾乎所有東西都有一個 Python 模塊),但它至少對於除我自己之外的幾十人是有用的。
幾個月前,我加入了一家公司,他們有幾個服務是用 Go 編寫的,我需要快速上手這門語言。編寫diffimg-go 看起來很有趣,甚至可能是一種有用的方法。這裡有一些來自經驗的興趣點,以及一些在工作中使用它的興趣點。
一、對比 Python 和 Go
(代碼: diffimg (Python)和 diffimg-go )
1、標準庫:Go 有一個相當不錯的 image 標準庫模塊,以及命令行 flag 解析庫。我不需要尋找任何外部依賴;diffimg-go 實現沒有依賴,而 Python 實現使用了相當重量級的第三方模塊(諷刺的是)Pillow。Go 的標準庫更有條理,而且經過深思熟慮,而 Python 的會逐步發展,它們是多年來由許多作者創建的,有許多不同的約定。Go 標準庫的一致性使開發者更易於預測任何給定的模塊將如何發揮作用,而且源代碼有非常好的文檔記錄。
(1)使用標準 image 庫的一個缺點是它不自動檢測圖像是否有一個 alpha 通道;所有圖像類型的像素值都有四個通道(RGBA)。因此,diffimg-go 實現要求用戶指明是否要使用 alpha 通道。這個小小的不便不值得找第三方庫來修復。
(2)一個很大的好處是,標準庫中有足夠的內容,你不需要像 Django 這樣的 Web 框架。用 Go 有可能在沒有任何依賴關係的情況下建立一個真正可用的 Web 服務。Python 號稱“自帶電池(batteries-included)”,但在我看來,Go 做得更好。
幾年前,我負責重寫一個圖像處理服務。為了弄清楚對於給定的圖像和一個或多個轉換(調整大小、圓形裁剪、修改格式等),我的新服務創建的輸出是否和舊服務一致,我必須自己檢查圖像。顯然,我需要自動化,但我找不到一個現有的 Python 庫可以告訴我,這兩張圖片在像素級上有什麼不同,因此有了diffimg ,它可以給你一個差異比 / 百分比,或生成差異圖像(檢出 readme,裡面有一個例子)。
我最初是用 Python 實現的(我最熟悉的語言),主要部分使用了 Pillow 。它可以用作庫或命令行工具。程序的基本部分非常小,只有幾十行,這要感謝Pillow。構建這個工具並不費力( xkcd 是對的,幾乎所有東西都有一個 Python 模塊),但它至少對於除我自己之外的幾十人是有用的。
幾個月前,我加入了一家公司,他們有幾個服務是用 Go 編寫的,我需要快速上手這門語言。編寫diffimg-go 看起來很有趣,甚至可能是一種有用的方法。這裡有一些來自經驗的興趣點,以及一些在工作中使用它的興趣點。
一、對比 Python 和 Go
(代碼: diffimg (Python)和 diffimg-go )
1、標準庫:Go 有一個相當不錯的 image 標準庫模塊,以及命令行 flag 解析庫。我不需要尋找任何外部依賴;diffimg-go 實現沒有依賴,而 Python 實現使用了相當重量級的第三方模塊(諷刺的是)Pillow。Go 的標準庫更有條理,而且經過深思熟慮,而 Python 的會逐步發展,它們是多年來由許多作者創建的,有許多不同的約定。Go 標準庫的一致性使開發者更易於預測任何給定的模塊將如何發揮作用,而且源代碼有非常好的文檔記錄。
(1)使用標準 image 庫的一個缺點是它不自動檢測圖像是否有一個 alpha 通道;所有圖像類型的像素值都有四個通道(RGBA)。因此,diffimg-go 實現要求用戶指明是否要使用 alpha 通道。這個小小的不便不值得找第三方庫來修復。
(2)一個很大的好處是,標準庫中有足夠的內容,你不需要像 Django 這樣的 Web 框架。用 Go 有可能在沒有任何依賴關係的情況下建立一個真正可用的 Web 服務。Python 號稱“自帶電池(batteries-included)”,但在我看來,Go 做得更好。
2、靜態類型系統:我過去使用靜態類型語言,但我過去幾年一直在使用 Python 編程。體驗起初有點煩人,感覺它好像只是減慢了我的速度,它迫使我過度明確,即使我偶爾錯了,Python 也會讓我做我想做的。有點像你給人發指令而對方總是打斷你讓你闡明你是什麼意思,而有的人則總是點頭,看上去已經理解你,但是你並不確定他們是否已經全部瞭解。它將大大減少與類型相關的 Bug,但是我發現,我仍然需要花幾乎相同的時間編寫測試。
(1)Go 的一個常見缺點是它沒有用戶可實現的泛型類型。雖然這不是一個構建大型可擴展應用程序的必備特性,但它肯定會減緩開發速度。雖然已經有替代模式建議,但是它們中沒有一個和真正的泛型類型一樣有效。
(2)靜態類型系統的一個優點是,可以更簡單快速地閱讀不熟悉的代碼庫。用好類型可以帶來許多動態類型系統中會丟失的額外信息。
幾年前,我負責重寫一個圖像處理服務。為了弄清楚對於給定的圖像和一個或多個轉換(調整大小、圓形裁剪、修改格式等),我的新服務創建的輸出是否和舊服務一致,我必須自己檢查圖像。顯然,我需要自動化,但我找不到一個現有的 Python 庫可以告訴我,這兩張圖片在像素級上有什麼不同,因此有了diffimg ,它可以給你一個差異比 / 百分比,或生成差異圖像(檢出 readme,裡面有一個例子)。
我最初是用 Python 實現的(我最熟悉的語言),主要部分使用了 Pillow 。它可以用作庫或命令行工具。程序的基本部分非常小,只有幾十行,這要感謝Pillow。構建這個工具並不費力( xkcd 是對的,幾乎所有東西都有一個 Python 模塊),但它至少對於除我自己之外的幾十人是有用的。
幾個月前,我加入了一家公司,他們有幾個服務是用 Go 編寫的,我需要快速上手這門語言。編寫diffimg-go 看起來很有趣,甚至可能是一種有用的方法。這裡有一些來自經驗的興趣點,以及一些在工作中使用它的興趣點。
一、對比 Python 和 Go
(代碼: diffimg (Python)和 diffimg-go )
1、標準庫:Go 有一個相當不錯的 image 標準庫模塊,以及命令行 flag 解析庫。我不需要尋找任何外部依賴;diffimg-go 實現沒有依賴,而 Python 實現使用了相當重量級的第三方模塊(諷刺的是)Pillow。Go 的標準庫更有條理,而且經過深思熟慮,而 Python 的會逐步發展,它們是多年來由許多作者創建的,有許多不同的約定。Go 標準庫的一致性使開發者更易於預測任何給定的模塊將如何發揮作用,而且源代碼有非常好的文檔記錄。
(1)使用標準 image 庫的一個缺點是它不自動檢測圖像是否有一個 alpha 通道;所有圖像類型的像素值都有四個通道(RGBA)。因此,diffimg-go 實現要求用戶指明是否要使用 alpha 通道。這個小小的不便不值得找第三方庫來修復。
(2)一個很大的好處是,標準庫中有足夠的內容,你不需要像 Django 這樣的 Web 框架。用 Go 有可能在沒有任何依賴關係的情況下建立一個真正可用的 Web 服務。Python 號稱“自帶電池(batteries-included)”,但在我看來,Go 做得更好。
2、靜態類型系統:我過去使用靜態類型語言,但我過去幾年一直在使用 Python 編程。體驗起初有點煩人,感覺它好像只是減慢了我的速度,它迫使我過度明確,即使我偶爾錯了,Python 也會讓我做我想做的。有點像你給人發指令而對方總是打斷你讓你闡明你是什麼意思,而有的人則總是點頭,看上去已經理解你,但是你並不確定他們是否已經全部瞭解。它將大大減少與類型相關的 Bug,但是我發現,我仍然需要花幾乎相同的時間編寫測試。
(1)Go 的一個常見缺點是它沒有用戶可實現的泛型類型。雖然這不是一個構建大型可擴展應用程序的必備特性,但它肯定會減緩開發速度。雖然已經有替代模式建議,但是它們中沒有一個和真正的泛型類型一樣有效。
(2)靜態類型系統的一個優點是,可以更簡單快速地閱讀不熟悉的代碼庫。用好類型可以帶來許多動態類型系統中會丟失的額外信息。
3、接口和結構:Go 使用接口和結構,而 Python 使用類。在我看來,這可能是最有趣的區別,因為它迫使我區分定義行為的類型和保存信息的類型這兩個概念。Python 和其他傳統的面向對象的語言都鼓勵你將它們混在一起,但這兩種範式各有利弊:
(1)Go 強烈建議組合而不是繼承。雖然它通過嵌入繼承,不用類,但其數據和方法不是那麼容易傳遞。通常,我認為組合是更好的默認模式,但我不是一個絕對主義者,在某些情況下繼承更合適,所以我不喜歡語言幫我作出這個決定。
(2)接口實現的分離意味著,如果有許多類型彼此相似,你就需要多次編寫類似的代碼。由於缺少泛型類型,在 Go 中,有些情況下我無法重用代碼,不過在 Python 中可以。
(3)然而,由於 Go 是靜態類型的,當你編寫的代碼會導致運行時錯誤時,編譯器 / 源碼分析器(linter)會告訴你。Python 源碼分析器也可以有一點這樣的功能。但在 Python 中,當你試圖訪問一個可能不存在的方法或屬性時,由於語言的動態性,Python 源碼分析器無法確切地知道什麼方法 / 屬性存在,直到運行時。靜態定義的接口和結構是唯一在編譯時和開發過程中知道什麼可用的方法,這使得編譯時報錯的 Go 比運行時報錯的 Python 更可靠。
幾年前,我負責重寫一個圖像處理服務。為了弄清楚對於給定的圖像和一個或多個轉換(調整大小、圓形裁剪、修改格式等),我的新服務創建的輸出是否和舊服務一致,我必須自己檢查圖像。顯然,我需要自動化,但我找不到一個現有的 Python 庫可以告訴我,這兩張圖片在像素級上有什麼不同,因此有了diffimg ,它可以給你一個差異比 / 百分比,或生成差異圖像(檢出 readme,裡面有一個例子)。
我最初是用 Python 實現的(我最熟悉的語言),主要部分使用了 Pillow 。它可以用作庫或命令行工具。程序的基本部分非常小,只有幾十行,這要感謝Pillow。構建這個工具並不費力( xkcd 是對的,幾乎所有東西都有一個 Python 模塊),但它至少對於除我自己之外的幾十人是有用的。
幾個月前,我加入了一家公司,他們有幾個服務是用 Go 編寫的,我需要快速上手這門語言。編寫diffimg-go 看起來很有趣,甚至可能是一種有用的方法。這裡有一些來自經驗的興趣點,以及一些在工作中使用它的興趣點。
一、對比 Python 和 Go
(代碼: diffimg (Python)和 diffimg-go )
1、標準庫:Go 有一個相當不錯的 image 標準庫模塊,以及命令行 flag 解析庫。我不需要尋找任何外部依賴;diffimg-go 實現沒有依賴,而 Python 實現使用了相當重量級的第三方模塊(諷刺的是)Pillow。Go 的標準庫更有條理,而且經過深思熟慮,而 Python 的會逐步發展,它們是多年來由許多作者創建的,有許多不同的約定。Go 標準庫的一致性使開發者更易於預測任何給定的模塊將如何發揮作用,而且源代碼有非常好的文檔記錄。
(1)使用標準 image 庫的一個缺點是它不自動檢測圖像是否有一個 alpha 通道;所有圖像類型的像素值都有四個通道(RGBA)。因此,diffimg-go 實現要求用戶指明是否要使用 alpha 通道。這個小小的不便不值得找第三方庫來修復。
(2)一個很大的好處是,標準庫中有足夠的內容,你不需要像 Django 這樣的 Web 框架。用 Go 有可能在沒有任何依賴關係的情況下建立一個真正可用的 Web 服務。Python 號稱“自帶電池(batteries-included)”,但在我看來,Go 做得更好。
2、靜態類型系統:我過去使用靜態類型語言,但我過去幾年一直在使用 Python 編程。體驗起初有點煩人,感覺它好像只是減慢了我的速度,它迫使我過度明確,即使我偶爾錯了,Python 也會讓我做我想做的。有點像你給人發指令而對方總是打斷你讓你闡明你是什麼意思,而有的人則總是點頭,看上去已經理解你,但是你並不確定他們是否已經全部瞭解。它將大大減少與類型相關的 Bug,但是我發現,我仍然需要花幾乎相同的時間編寫測試。
(1)Go 的一個常見缺點是它沒有用戶可實現的泛型類型。雖然這不是一個構建大型可擴展應用程序的必備特性,但它肯定會減緩開發速度。雖然已經有替代模式建議,但是它們中沒有一個和真正的泛型類型一樣有效。
(2)靜態類型系統的一個優點是,可以更簡單快速地閱讀不熟悉的代碼庫。用好類型可以帶來許多動態類型系統中會丟失的額外信息。
3、接口和結構:Go 使用接口和結構,而 Python 使用類。在我看來,這可能是最有趣的區別,因為它迫使我區分定義行為的類型和保存信息的類型這兩個概念。Python 和其他傳統的面向對象的語言都鼓勵你將它們混在一起,但這兩種範式各有利弊:
(1)Go 強烈建議組合而不是繼承。雖然它通過嵌入繼承,不用類,但其數據和方法不是那麼容易傳遞。通常,我認為組合是更好的默認模式,但我不是一個絕對主義者,在某些情況下繼承更合適,所以我不喜歡語言幫我作出這個決定。
(2)接口實現的分離意味著,如果有許多類型彼此相似,你就需要多次編寫類似的代碼。由於缺少泛型類型,在 Go 中,有些情況下我無法重用代碼,不過在 Python 中可以。
(3)然而,由於 Go 是靜態類型的,當你編寫的代碼會導致運行時錯誤時,編譯器 / 源碼分析器(linter)會告訴你。Python 源碼分析器也可以有一點這樣的功能。但在 Python 中,當你試圖訪問一個可能不存在的方法或屬性時,由於語言的動態性,Python 源碼分析器無法確切地知道什麼方法 / 屬性存在,直到運行時。靜態定義的接口和結構是唯一在編譯時和開發過程中知道什麼可用的方法,這使得編譯時報錯的 Go 比運行時報錯的 Python 更可靠。
4、沒有可選參數:Go 只有可變函數,類似於 Python 的關鍵字參數,但不那麼有用,因為參數需要是相同的類型。我發現關鍵字參數是我真正懷念的特性,這主要是你可以把任何類型的一個 kwarg 扔給任何需要它的函數,而無需重寫它的每一個調用,這讓重構簡單了許多。我在工作中經常使用這個特性,它為我節省了很多時間。由於沒有該特性,這使得我在處理是否應該基於命令行標誌創建差異圖像時顯得有些笨拙。
5、冗長:Go 有點冗長(儘管不是像 Java 那麼冗長)。這部分是因為其類型系統沒有泛型,但主要是因為語言本身很小,沒有提供很多特性(你只有一種循環結構可以使用!)。我懷念Python 的列表推導式(list comprehensions)和其他函數式編程特性。如果你熟悉Python,你一兩天就可以學完 Tour of Go ,然後你就瞭解了整個語言。
6、錯誤處理:Python 有異常,而 Go 在可能出錯的地方通過從函數返回元組 value, error 來傳播錯誤。Python 允許你在調用棧中的任何位置捕獲錯誤,而不需要你一次又一次地手動將錯誤傳遞回去。這又使得代碼簡潔,而不像 Go 那樣被其臭名昭著的 if err != nil 模式搞得雜亂無章,不過你需要弄清楚函數及其內部的所有調用可能拋出的異常(使用 except Exception: 通常是一種糟糕的變通方案)。良好的文檔註釋和測試可以提供幫助,你在使用其中任何一種語言編程時都應該添加。Go 的系統絕對更安全。如果你忽視了 err 值,你還是可以搬起石頭砸自己的腳,但該系統使糟糕的主意變得更明顯。
幾年前,我負責重寫一個圖像處理服務。為了弄清楚對於給定的圖像和一個或多個轉換(調整大小、圓形裁剪、修改格式等),我的新服務創建的輸出是否和舊服務一致,我必須自己檢查圖像。顯然,我需要自動化,但我找不到一個現有的 Python 庫可以告訴我,這兩張圖片在像素級上有什麼不同,因此有了diffimg ,它可以給你一個差異比 / 百分比,或生成差異圖像(檢出 readme,裡面有一個例子)。
我最初是用 Python 實現的(我最熟悉的語言),主要部分使用了 Pillow 。它可以用作庫或命令行工具。程序的基本部分非常小,只有幾十行,這要感謝Pillow。構建這個工具並不費力( xkcd 是對的,幾乎所有東西都有一個 Python 模塊),但它至少對於除我自己之外的幾十人是有用的。
幾個月前,我加入了一家公司,他們有幾個服務是用 Go 編寫的,我需要快速上手這門語言。編寫diffimg-go 看起來很有趣,甚至可能是一種有用的方法。這裡有一些來自經驗的興趣點,以及一些在工作中使用它的興趣點。
一、對比 Python 和 Go
(代碼: diffimg (Python)和 diffimg-go )
1、標準庫:Go 有一個相當不錯的 image 標準庫模塊,以及命令行 flag 解析庫。我不需要尋找任何外部依賴;diffimg-go 實現沒有依賴,而 Python 實現使用了相當重量級的第三方模塊(諷刺的是)Pillow。Go 的標準庫更有條理,而且經過深思熟慮,而 Python 的會逐步發展,它們是多年來由許多作者創建的,有許多不同的約定。Go 標準庫的一致性使開發者更易於預測任何給定的模塊將如何發揮作用,而且源代碼有非常好的文檔記錄。
(1)使用標準 image 庫的一個缺點是它不自動檢測圖像是否有一個 alpha 通道;所有圖像類型的像素值都有四個通道(RGBA)。因此,diffimg-go 實現要求用戶指明是否要使用 alpha 通道。這個小小的不便不值得找第三方庫來修復。
(2)一個很大的好處是,標準庫中有足夠的內容,你不需要像 Django 這樣的 Web 框架。用 Go 有可能在沒有任何依賴關係的情況下建立一個真正可用的 Web 服務。Python 號稱“自帶電池(batteries-included)”,但在我看來,Go 做得更好。
2、靜態類型系統:我過去使用靜態類型語言,但我過去幾年一直在使用 Python 編程。體驗起初有點煩人,感覺它好像只是減慢了我的速度,它迫使我過度明確,即使我偶爾錯了,Python 也會讓我做我想做的。有點像你給人發指令而對方總是打斷你讓你闡明你是什麼意思,而有的人則總是點頭,看上去已經理解你,但是你並不確定他們是否已經全部瞭解。它將大大減少與類型相關的 Bug,但是我發現,我仍然需要花幾乎相同的時間編寫測試。
(1)Go 的一個常見缺點是它沒有用戶可實現的泛型類型。雖然這不是一個構建大型可擴展應用程序的必備特性,但它肯定會減緩開發速度。雖然已經有替代模式建議,但是它們中沒有一個和真正的泛型類型一樣有效。
(2)靜態類型系統的一個優點是,可以更簡單快速地閱讀不熟悉的代碼庫。用好類型可以帶來許多動態類型系統中會丟失的額外信息。
3、接口和結構:Go 使用接口和結構,而 Python 使用類。在我看來,這可能是最有趣的區別,因為它迫使我區分定義行為的類型和保存信息的類型這兩個概念。Python 和其他傳統的面向對象的語言都鼓勵你將它們混在一起,但這兩種範式各有利弊:
(1)Go 強烈建議組合而不是繼承。雖然它通過嵌入繼承,不用類,但其數據和方法不是那麼容易傳遞。通常,我認為組合是更好的默認模式,但我不是一個絕對主義者,在某些情況下繼承更合適,所以我不喜歡語言幫我作出這個決定。
(2)接口實現的分離意味著,如果有許多類型彼此相似,你就需要多次編寫類似的代碼。由於缺少泛型類型,在 Go 中,有些情況下我無法重用代碼,不過在 Python 中可以。
(3)然而,由於 Go 是靜態類型的,當你編寫的代碼會導致運行時錯誤時,編譯器 / 源碼分析器(linter)會告訴你。Python 源碼分析器也可以有一點這樣的功能。但在 Python 中,當你試圖訪問一個可能不存在的方法或屬性時,由於語言的動態性,Python 源碼分析器無法確切地知道什麼方法 / 屬性存在,直到運行時。靜態定義的接口和結構是唯一在編譯時和開發過程中知道什麼可用的方法,這使得編譯時報錯的 Go 比運行時報錯的 Python 更可靠。
4、沒有可選參數:Go 只有可變函數,類似於 Python 的關鍵字參數,但不那麼有用,因為參數需要是相同的類型。我發現關鍵字參數是我真正懷念的特性,這主要是你可以把任何類型的一個 kwarg 扔給任何需要它的函數,而無需重寫它的每一個調用,這讓重構簡單了許多。我在工作中經常使用這個特性,它為我節省了很多時間。由於沒有該特性,這使得我在處理是否應該基於命令行標誌創建差異圖像時顯得有些笨拙。
5、冗長:Go 有點冗長(儘管不是像 Java 那麼冗長)。這部分是因為其類型系統沒有泛型,但主要是因為語言本身很小,沒有提供很多特性(你只有一種循環結構可以使用!)。我懷念Python 的列表推導式(list comprehensions)和其他函數式編程特性。如果你熟悉Python,你一兩天就可以學完 Tour of Go ,然後你就瞭解了整個語言。
6、錯誤處理:Python 有異常,而 Go 在可能出錯的地方通過從函數返回元組 value, error 來傳播錯誤。Python 允許你在調用棧中的任何位置捕獲錯誤,而不需要你一次又一次地手動將錯誤傳遞回去。這又使得代碼簡潔,而不像 Go 那樣被其臭名昭著的 if err != nil 模式搞得雜亂無章,不過你需要弄清楚函數及其內部的所有調用可能拋出的異常(使用 except Exception: 通常是一種糟糕的變通方案)。良好的文檔註釋和測試可以提供幫助,你在使用其中任何一種語言編程時都應該添加。Go 的系統絕對更安全。如果你忽視了 err 值,你還是可以搬起石頭砸自己的腳,但該系統使糟糕的主意變得更明顯。
7、第三方模塊:在 Go 模塊出現之前,Go 的包管理器會把所有下載的包扔到 GOPATH/src目錄下,而不是項目的目錄(像大多數其他語言)。
GOPATH/src目錄下,而不是項目的目錄(像大多數其他語言)。GOPATH 下這些模塊的路徑也會從託管包的 URL 構建,所以你的 import 語句將是類似 import " github.com/someuser/somepackage " 這個樣子。在幾乎所有 Go 代碼庫的源代碼中嵌入 github.com 似乎是一個奇怪的選擇。在任何情況下,Go 允許以傳統的方式做事,但 Go 模塊仍然是個很新的特性,所以在一段時間內,在缺少管理的 Go 代碼庫中,這種奇怪的行為仍將很常見。
8、異步性:Goroutines 是啟動異步任務的一種非常方便的方法。在 async/await 之前,Python 的異步解決方案有點令人沮喪。遺憾的是,我沒有用 Python 或 Go 編寫很多真實的異步代碼,而 diffimg 的簡單性似乎並不適合說明異步性的額外開銷,所以我沒有太多要說的,雖然我確實喜歡使用 Go 的channels 來處理多個異步任務。我的理解是,對於性能,Go 仍然佔了上風了,因為 goroutine 可以充分利用多處理器併發,而 Python 的基本 async/await 仍侷限於一個處理器,所以主要用於 I / O 密集型任務。
9、調試:Python 勝出。pdb(以及像 ipdb 這樣更復雜的選項)非常靈活,一旦你進入 REPL,你可以編寫任何你想要的代碼。 Delve 是一個很好的調試器,但和直接放入解釋器不一樣,語言的全部功能都唾手可得。
Go 總結
我對 Go 最初的印象是,由於它的抽象能力(有意為之)有限,所以它不像 Python 那樣有趣。Python 有更多的特性,因此有更多的方法來做一些事情,找到最快、最易讀或“最聰明”的解決方案可能會很有趣。Go 會積極地阻止你變得“聰明”。我認為,Go 的優勢在於它並不聰明。
它的極簡主義和缺乏自由限制了單個開發人員實現一個想法。然而,當項目擴展到幾十個或幾百個開發人員時,這個弱點變成了它的力量——因為每個人都使用同樣的語言特性小工具集,更容易統一,因此可以被他人理解。使用 Go 仍有可能編寫糟糕的代碼,但與那些更“強大”的語言相比,它讓你更難創造怪物。
使用一段時間後,我理解了為什麼像谷歌這樣的公司想要一門這樣的語言。新工程師不斷地加入到大量代碼庫的開發,在更復雜 / 更強大的語言中,在最後期限的壓力下,引入複雜性的速度比它被消除的速度更快。防止這種情況發生的最好方法是使用一種更不容易產生複雜性的語言。
所以說,我很高興工作在一個大型應用程序上下文中的 Go 代碼庫上,有一個多樣化和不斷增長的團隊。事實上,我認為我更喜歡它。我只是不想把它用於我自己的個人項目。
幾年前,我負責重寫一個圖像處理服務。為了弄清楚對於給定的圖像和一個或多個轉換(調整大小、圓形裁剪、修改格式等),我的新服務創建的輸出是否和舊服務一致,我必須自己檢查圖像。顯然,我需要自動化,但我找不到一個現有的 Python 庫可以告訴我,這兩張圖片在像素級上有什麼不同,因此有了diffimg ,它可以給你一個差異比 / 百分比,或生成差異圖像(檢出 readme,裡面有一個例子)。
我最初是用 Python 實現的(我最熟悉的語言),主要部分使用了 Pillow 。它可以用作庫或命令行工具。程序的基本部分非常小,只有幾十行,這要感謝Pillow。構建這個工具並不費力( xkcd 是對的,幾乎所有東西都有一個 Python 模塊),但它至少對於除我自己之外的幾十人是有用的。
幾個月前,我加入了一家公司,他們有幾個服務是用 Go 編寫的,我需要快速上手這門語言。編寫diffimg-go 看起來很有趣,甚至可能是一種有用的方法。這裡有一些來自經驗的興趣點,以及一些在工作中使用它的興趣點。
一、對比 Python 和 Go
(代碼: diffimg (Python)和 diffimg-go )
1、標準庫:Go 有一個相當不錯的 image 標準庫模塊,以及命令行 flag 解析庫。我不需要尋找任何外部依賴;diffimg-go 實現沒有依賴,而 Python 實現使用了相當重量級的第三方模塊(諷刺的是)Pillow。Go 的標準庫更有條理,而且經過深思熟慮,而 Python 的會逐步發展,它們是多年來由許多作者創建的,有許多不同的約定。Go 標準庫的一致性使開發者更易於預測任何給定的模塊將如何發揮作用,而且源代碼有非常好的文檔記錄。
(1)使用標準 image 庫的一個缺點是它不自動檢測圖像是否有一個 alpha 通道;所有圖像類型的像素值都有四個通道(RGBA)。因此,diffimg-go 實現要求用戶指明是否要使用 alpha 通道。這個小小的不便不值得找第三方庫來修復。
(2)一個很大的好處是,標準庫中有足夠的內容,你不需要像 Django 這樣的 Web 框架。用 Go 有可能在沒有任何依賴關係的情況下建立一個真正可用的 Web 服務。Python 號稱“自帶電池(batteries-included)”,但在我看來,Go 做得更好。
2、靜態類型系統:我過去使用靜態類型語言,但我過去幾年一直在使用 Python 編程。體驗起初有點煩人,感覺它好像只是減慢了我的速度,它迫使我過度明確,即使我偶爾錯了,Python 也會讓我做我想做的。有點像你給人發指令而對方總是打斷你讓你闡明你是什麼意思,而有的人則總是點頭,看上去已經理解你,但是你並不確定他們是否已經全部瞭解。它將大大減少與類型相關的 Bug,但是我發現,我仍然需要花幾乎相同的時間編寫測試。
(1)Go 的一個常見缺點是它沒有用戶可實現的泛型類型。雖然這不是一個構建大型可擴展應用程序的必備特性,但它肯定會減緩開發速度。雖然已經有替代模式建議,但是它們中沒有一個和真正的泛型類型一樣有效。
(2)靜態類型系統的一個優點是,可以更簡單快速地閱讀不熟悉的代碼庫。用好類型可以帶來許多動態類型系統中會丟失的額外信息。
3、接口和結構:Go 使用接口和結構,而 Python 使用類。在我看來,這可能是最有趣的區別,因為它迫使我區分定義行為的類型和保存信息的類型這兩個概念。Python 和其他傳統的面向對象的語言都鼓勵你將它們混在一起,但這兩種範式各有利弊:
(1)Go 強烈建議組合而不是繼承。雖然它通過嵌入繼承,不用類,但其數據和方法不是那麼容易傳遞。通常,我認為組合是更好的默認模式,但我不是一個絕對主義者,在某些情況下繼承更合適,所以我不喜歡語言幫我作出這個決定。
(2)接口實現的分離意味著,如果有許多類型彼此相似,你就需要多次編寫類似的代碼。由於缺少泛型類型,在 Go 中,有些情況下我無法重用代碼,不過在 Python 中可以。
(3)然而,由於 Go 是靜態類型的,當你編寫的代碼會導致運行時錯誤時,編譯器 / 源碼分析器(linter)會告訴你。Python 源碼分析器也可以有一點這樣的功能。但在 Python 中,當你試圖訪問一個可能不存在的方法或屬性時,由於語言的動態性,Python 源碼分析器無法確切地知道什麼方法 / 屬性存在,直到運行時。靜態定義的接口和結構是唯一在編譯時和開發過程中知道什麼可用的方法,這使得編譯時報錯的 Go 比運行時報錯的 Python 更可靠。
4、沒有可選參數:Go 只有可變函數,類似於 Python 的關鍵字參數,但不那麼有用,因為參數需要是相同的類型。我發現關鍵字參數是我真正懷念的特性,這主要是你可以把任何類型的一個 kwarg 扔給任何需要它的函數,而無需重寫它的每一個調用,這讓重構簡單了許多。我在工作中經常使用這個特性,它為我節省了很多時間。由於沒有該特性,這使得我在處理是否應該基於命令行標誌創建差異圖像時顯得有些笨拙。
5、冗長:Go 有點冗長(儘管不是像 Java 那麼冗長)。這部分是因為其類型系統沒有泛型,但主要是因為語言本身很小,沒有提供很多特性(你只有一種循環結構可以使用!)。我懷念Python 的列表推導式(list comprehensions)和其他函數式編程特性。如果你熟悉Python,你一兩天就可以學完 Tour of Go ,然後你就瞭解了整個語言。
6、錯誤處理:Python 有異常,而 Go 在可能出錯的地方通過從函數返回元組 value, error 來傳播錯誤。Python 允許你在調用棧中的任何位置捕獲錯誤,而不需要你一次又一次地手動將錯誤傳遞回去。這又使得代碼簡潔,而不像 Go 那樣被其臭名昭著的 if err != nil 模式搞得雜亂無章,不過你需要弄清楚函數及其內部的所有調用可能拋出的異常(使用 except Exception: 通常是一種糟糕的變通方案)。良好的文檔註釋和測試可以提供幫助,你在使用其中任何一種語言編程時都應該添加。Go 的系統絕對更安全。如果你忽視了 err 值,你還是可以搬起石頭砸自己的腳,但該系統使糟糕的主意變得更明顯。
7、第三方模塊:在 Go 模塊出現之前,Go 的包管理器會把所有下載的包扔到 GOPATH/src目錄下,而不是項目的目錄(像大多數其他語言)。
GOPATH/src目錄下,而不是項目的目錄(像大多數其他語言)。GOPATH 下這些模塊的路徑也會從託管包的 URL 構建,所以你的 import 語句將是類似 import " github.com/someuser/somepackage " 這個樣子。在幾乎所有 Go 代碼庫的源代碼中嵌入 github.com 似乎是一個奇怪的選擇。在任何情況下,Go 允許以傳統的方式做事,但 Go 模塊仍然是個很新的特性,所以在一段時間內,在缺少管理的 Go 代碼庫中,這種奇怪的行為仍將很常見。
8、異步性:Goroutines 是啟動異步任務的一種非常方便的方法。在 async/await 之前,Python 的異步解決方案有點令人沮喪。遺憾的是,我沒有用 Python 或 Go 編寫很多真實的異步代碼,而 diffimg 的簡單性似乎並不適合說明異步性的額外開銷,所以我沒有太多要說的,雖然我確實喜歡使用 Go 的channels 來處理多個異步任務。我的理解是,對於性能,Go 仍然佔了上風了,因為 goroutine 可以充分利用多處理器併發,而 Python 的基本 async/await 仍侷限於一個處理器,所以主要用於 I / O 密集型任務。
9、調試:Python 勝出。pdb(以及像 ipdb 這樣更復雜的選項)非常靈活,一旦你進入 REPL,你可以編寫任何你想要的代碼。 Delve 是一個很好的調試器,但和直接放入解釋器不一樣,語言的全部功能都唾手可得。
Go 總結
我對 Go 最初的印象是,由於它的抽象能力(有意為之)有限,所以它不像 Python 那樣有趣。Python 有更多的特性,因此有更多的方法來做一些事情,找到最快、最易讀或“最聰明”的解決方案可能會很有趣。Go 會積極地阻止你變得“聰明”。我認為,Go 的優勢在於它並不聰明。
它的極簡主義和缺乏自由限制了單個開發人員實現一個想法。然而,當項目擴展到幾十個或幾百個開發人員時,這個弱點變成了它的力量——因為每個人都使用同樣的語言特性小工具集,更容易統一,因此可以被他人理解。使用 Go 仍有可能編寫糟糕的代碼,但與那些更“強大”的語言相比,它讓你更難創造怪物。
使用一段時間後,我理解了為什麼像谷歌這樣的公司想要一門這樣的語言。新工程師不斷地加入到大量代碼庫的開發,在更復雜 / 更強大的語言中,在最後期限的壓力下,引入複雜性的速度比它被消除的速度更快。防止這種情況發生的最好方法是使用一種更不容易產生複雜性的語言。
所以說,我很高興工作在一個大型應用程序上下文中的 Go 代碼庫上,有一個多樣化和不斷增長的團隊。事實上,我認為我更喜歡它。我只是不想把它用於我自己的個人項目。
二、進入 Rust
幾周前,我決定嘗試學習 Rust。我之前曾試圖這樣做,但發現類型系統和借用檢查(borrow checker)讓人困惑,沒有足夠的背景信息,我不知道為什麼要把所有這些限制強加給我,對於我想做的任務來說大而笨重。然而,自那時以來,我對程序執行時內存中發生了什麼有了更多的瞭解。我從這本書開始,而不是試圖一頭扎進去。這有很大的幫助,這份介紹比我見過的任何編程語言的介紹都要好。
在我讀過這本書的前十幾章之後,我覺得自己有足夠的信心嘗試 diffimg 的另一個實現(這時,我覺得我的 Rust 經驗與我寫 diffimg-go 時的 Go 經驗一樣多)。這比我用 Go 實現的時間要長,而後者比用 Python 需要的時間更長。我認為,即使考慮到我對 Python 更加熟悉,這也是對的——兩種語言都有更多的東西要寫。
在編寫 diffimg-rs 時,我注意到一些事情。
1、類型系統:現在,我已經習慣 Go 中更基本的靜態類型系統了,但 Rust 更強大(也更復雜)。有了 Go 中接口和結構的基礎(它們都簡單得多),泛型類型、枚舉類型、traits、引用類型、生命週期就是全部我要額外學習的概念了。此外,Rust 使用其類型系統實現其它語言不使用類型系統實現的特性(如 Result ,我很快會介紹)。幸運的是,編譯器 / 源碼解析器非常有幫助,它可以告訴你你做錯了什麼,甚至經常告訴你如何解決這個問題。儘管如此,這比我學習 Go 的類型系統所用的時間要多得多,我還沒有適應它的所有特性。
(1)有一個地方,因為類型系統,我正在使用的圖像庫的實現會導致令人不快的代碼重複量。我最終只要匹配兩個最重要的枚舉類型,但匹配其他類型會導致另外半打左右幾乎相同的代碼行。在這個規模上,這不是一個問題,但它使我生氣。也許這裡使用宏是個不錯的主意,我仍然需要試驗。
幾年前,我負責重寫一個圖像處理服務。為了弄清楚對於給定的圖像和一個或多個轉換(調整大小、圓形裁剪、修改格式等),我的新服務創建的輸出是否和舊服務一致,我必須自己檢查圖像。顯然,我需要自動化,但我找不到一個現有的 Python 庫可以告訴我,這兩張圖片在像素級上有什麼不同,因此有了diffimg ,它可以給你一個差異比 / 百分比,或生成差異圖像(檢出 readme,裡面有一個例子)。
我最初是用 Python 實現的(我最熟悉的語言),主要部分使用了 Pillow 。它可以用作庫或命令行工具。程序的基本部分非常小,只有幾十行,這要感謝Pillow。構建這個工具並不費力( xkcd 是對的,幾乎所有東西都有一個 Python 模塊),但它至少對於除我自己之外的幾十人是有用的。
幾個月前,我加入了一家公司,他們有幾個服務是用 Go 編寫的,我需要快速上手這門語言。編寫diffimg-go 看起來很有趣,甚至可能是一種有用的方法。這裡有一些來自經驗的興趣點,以及一些在工作中使用它的興趣點。
一、對比 Python 和 Go
(代碼: diffimg (Python)和 diffimg-go )
1、標準庫:Go 有一個相當不錯的 image 標準庫模塊,以及命令行 flag 解析庫。我不需要尋找任何外部依賴;diffimg-go 實現沒有依賴,而 Python 實現使用了相當重量級的第三方模塊(諷刺的是)Pillow。Go 的標準庫更有條理,而且經過深思熟慮,而 Python 的會逐步發展,它們是多年來由許多作者創建的,有許多不同的約定。Go 標準庫的一致性使開發者更易於預測任何給定的模塊將如何發揮作用,而且源代碼有非常好的文檔記錄。
(1)使用標準 image 庫的一個缺點是它不自動檢測圖像是否有一個 alpha 通道;所有圖像類型的像素值都有四個通道(RGBA)。因此,diffimg-go 實現要求用戶指明是否要使用 alpha 通道。這個小小的不便不值得找第三方庫來修復。
(2)一個很大的好處是,標準庫中有足夠的內容,你不需要像 Django 這樣的 Web 框架。用 Go 有可能在沒有任何依賴關係的情況下建立一個真正可用的 Web 服務。Python 號稱“自帶電池(batteries-included)”,但在我看來,Go 做得更好。
2、靜態類型系統:我過去使用靜態類型語言,但我過去幾年一直在使用 Python 編程。體驗起初有點煩人,感覺它好像只是減慢了我的速度,它迫使我過度明確,即使我偶爾錯了,Python 也會讓我做我想做的。有點像你給人發指令而對方總是打斷你讓你闡明你是什麼意思,而有的人則總是點頭,看上去已經理解你,但是你並不確定他們是否已經全部瞭解。它將大大減少與類型相關的 Bug,但是我發現,我仍然需要花幾乎相同的時間編寫測試。
(1)Go 的一個常見缺點是它沒有用戶可實現的泛型類型。雖然這不是一個構建大型可擴展應用程序的必備特性,但它肯定會減緩開發速度。雖然已經有替代模式建議,但是它們中沒有一個和真正的泛型類型一樣有效。
(2)靜態類型系統的一個優點是,可以更簡單快速地閱讀不熟悉的代碼庫。用好類型可以帶來許多動態類型系統中會丟失的額外信息。
3、接口和結構:Go 使用接口和結構,而 Python 使用類。在我看來,這可能是最有趣的區別,因為它迫使我區分定義行為的類型和保存信息的類型這兩個概念。Python 和其他傳統的面向對象的語言都鼓勵你將它們混在一起,但這兩種範式各有利弊:
(1)Go 強烈建議組合而不是繼承。雖然它通過嵌入繼承,不用類,但其數據和方法不是那麼容易傳遞。通常,我認為組合是更好的默認模式,但我不是一個絕對主義者,在某些情況下繼承更合適,所以我不喜歡語言幫我作出這個決定。
(2)接口實現的分離意味著,如果有許多類型彼此相似,你就需要多次編寫類似的代碼。由於缺少泛型類型,在 Go 中,有些情況下我無法重用代碼,不過在 Python 中可以。
(3)然而,由於 Go 是靜態類型的,當你編寫的代碼會導致運行時錯誤時,編譯器 / 源碼分析器(linter)會告訴你。Python 源碼分析器也可以有一點這樣的功能。但在 Python 中,當你試圖訪問一個可能不存在的方法或屬性時,由於語言的動態性,Python 源碼分析器無法確切地知道什麼方法 / 屬性存在,直到運行時。靜態定義的接口和結構是唯一在編譯時和開發過程中知道什麼可用的方法,這使得編譯時報錯的 Go 比運行時報錯的 Python 更可靠。
4、沒有可選參數:Go 只有可變函數,類似於 Python 的關鍵字參數,但不那麼有用,因為參數需要是相同的類型。我發現關鍵字參數是我真正懷念的特性,這主要是你可以把任何類型的一個 kwarg 扔給任何需要它的函數,而無需重寫它的每一個調用,這讓重構簡單了許多。我在工作中經常使用這個特性,它為我節省了很多時間。由於沒有該特性,這使得我在處理是否應該基於命令行標誌創建差異圖像時顯得有些笨拙。
5、冗長:Go 有點冗長(儘管不是像 Java 那麼冗長)。這部分是因為其類型系統沒有泛型,但主要是因為語言本身很小,沒有提供很多特性(你只有一種循環結構可以使用!)。我懷念Python 的列表推導式(list comprehensions)和其他函數式編程特性。如果你熟悉Python,你一兩天就可以學完 Tour of Go ,然後你就瞭解了整個語言。
6、錯誤處理:Python 有異常,而 Go 在可能出錯的地方通過從函數返回元組 value, error 來傳播錯誤。Python 允許你在調用棧中的任何位置捕獲錯誤,而不需要你一次又一次地手動將錯誤傳遞回去。這又使得代碼簡潔,而不像 Go 那樣被其臭名昭著的 if err != nil 模式搞得雜亂無章,不過你需要弄清楚函數及其內部的所有調用可能拋出的異常(使用 except Exception: 通常是一種糟糕的變通方案)。良好的文檔註釋和測試可以提供幫助,你在使用其中任何一種語言編程時都應該添加。Go 的系統絕對更安全。如果你忽視了 err 值,你還是可以搬起石頭砸自己的腳,但該系統使糟糕的主意變得更明顯。
7、第三方模塊:在 Go 模塊出現之前,Go 的包管理器會把所有下載的包扔到 GOPATH/src目錄下,而不是項目的目錄(像大多數其他語言)。
GOPATH/src目錄下,而不是項目的目錄(像大多數其他語言)。GOPATH 下這些模塊的路徑也會從託管包的 URL 構建,所以你的 import 語句將是類似 import " github.com/someuser/somepackage " 這個樣子。在幾乎所有 Go 代碼庫的源代碼中嵌入 github.com 似乎是一個奇怪的選擇。在任何情況下,Go 允許以傳統的方式做事,但 Go 模塊仍然是個很新的特性,所以在一段時間內,在缺少管理的 Go 代碼庫中,這種奇怪的行為仍將很常見。
8、異步性:Goroutines 是啟動異步任務的一種非常方便的方法。在 async/await 之前,Python 的異步解決方案有點令人沮喪。遺憾的是,我沒有用 Python 或 Go 編寫很多真實的異步代碼,而 diffimg 的簡單性似乎並不適合說明異步性的額外開銷,所以我沒有太多要說的,雖然我確實喜歡使用 Go 的channels 來處理多個異步任務。我的理解是,對於性能,Go 仍然佔了上風了,因為 goroutine 可以充分利用多處理器併發,而 Python 的基本 async/await 仍侷限於一個處理器,所以主要用於 I / O 密集型任務。
9、調試:Python 勝出。pdb(以及像 ipdb 這樣更復雜的選項)非常靈活,一旦你進入 REPL,你可以編寫任何你想要的代碼。 Delve 是一個很好的調試器,但和直接放入解釋器不一樣,語言的全部功能都唾手可得。
Go 總結
我對 Go 最初的印象是,由於它的抽象能力(有意為之)有限,所以它不像 Python 那樣有趣。Python 有更多的特性,因此有更多的方法來做一些事情,找到最快、最易讀或“最聰明”的解決方案可能會很有趣。Go 會積極地阻止你變得“聰明”。我認為,Go 的優勢在於它並不聰明。
它的極簡主義和缺乏自由限制了單個開發人員實現一個想法。然而,當項目擴展到幾十個或幾百個開發人員時,這個弱點變成了它的力量——因為每個人都使用同樣的語言特性小工具集,更容易統一,因此可以被他人理解。使用 Go 仍有可能編寫糟糕的代碼,但與那些更“強大”的語言相比,它讓你更難創造怪物。
使用一段時間後,我理解了為什麼像谷歌這樣的公司想要一門這樣的語言。新工程師不斷地加入到大量代碼庫的開發,在更復雜 / 更強大的語言中,在最後期限的壓力下,引入複雜性的速度比它被消除的速度更快。防止這種情況發生的最好方法是使用一種更不容易產生複雜性的語言。
所以說,我很高興工作在一個大型應用程序上下文中的 Go 代碼庫上,有一個多樣化和不斷增長的團隊。事實上,我認為我更喜歡它。我只是不想把它用於我自己的個人項目。
二、進入 Rust
幾周前,我決定嘗試學習 Rust。我之前曾試圖這樣做,但發現類型系統和借用檢查(borrow checker)讓人困惑,沒有足夠的背景信息,我不知道為什麼要把所有這些限制強加給我,對於我想做的任務來說大而笨重。然而,自那時以來,我對程序執行時內存中發生了什麼有了更多的瞭解。我從這本書開始,而不是試圖一頭扎進去。這有很大的幫助,這份介紹比我見過的任何編程語言的介紹都要好。
在我讀過這本書的前十幾章之後,我覺得自己有足夠的信心嘗試 diffimg 的另一個實現(這時,我覺得我的 Rust 經驗與我寫 diffimg-go 時的 Go 經驗一樣多)。這比我用 Go 實現的時間要長,而後者比用 Python 需要的時間更長。我認為,即使考慮到我對 Python 更加熟悉,這也是對的——兩種語言都有更多的東西要寫。
在編寫 diffimg-rs 時,我注意到一些事情。
1、類型系統:現在,我已經習慣 Go 中更基本的靜態類型系統了,但 Rust 更強大(也更復雜)。有了 Go 中接口和結構的基礎(它們都簡單得多),泛型類型、枚舉類型、traits、引用類型、生命週期就是全部我要額外學習的概念了。此外,Rust 使用其類型系統實現其它語言不使用類型系統實現的特性(如 Result ,我很快會介紹)。幸運的是,編譯器 / 源碼解析器非常有幫助,它可以告訴你你做錯了什麼,甚至經常告訴你如何解決這個問題。儘管如此,這比我學習 Go 的類型系統所用的時間要多得多,我還沒有適應它的所有特性。
(1)有一個地方,因為類型系統,我正在使用的圖像庫的實現會導致令人不快的代碼重複量。我最終只要匹配兩個最重要的枚舉類型,但匹配其他類型會導致另外半打左右幾乎相同的代碼行。在這個規模上,這不是一個問題,但它使我生氣。也許這裡使用宏是個不錯的主意,我仍然需要試驗。
2、手動內存管理:Python 和 Go 會幫你撿垃圾。C 允許你亂丟垃圾,但當它踩到你丟的香蕉皮時,它會大發脾氣。Rust 會拍你一下,並要求你自己清理乾淨。這會刺痛我,甚至超過了從動態類型語言遷移到靜態類型語言,因為我被寵壞了,通常都是由語言跟在我後面撿。此外,編譯器會設法儘可能地幫助你,但你仍然需要大量的學習才能理解到底發生了什麼。
(1)直接訪問內存(以及 Rust 的函數式編程特性)的一個好處是,它簡化了差異比的計算,因為我可以簡單地映射原始字節數組,而不必按座標索引每個像素。
3、函數式特性:Rust 強烈鼓勵函數式的方法:它有 FP 友好的類型系統(類似 Haskell)、不可變類型、閉包,迭代器,模式匹配等,但也允許命令式代碼。它類似於編寫 OCaml(有趣的是,最初的 Rust 編譯器是用 OCaml 編寫的)。因此,對於這門與 C 競爭的語言,代碼比你想象得更簡潔。
4、錯誤處理:不同於 Python 使用的異常模型,也不同於 Go 異常處理時返回的元組,Rust 利用枚舉類型:Result 返回 Ok(value) 或 Err(error)。這和 Go 的方式更接近,但更明確一些,而且利用了類型系統。還有一個語法糖用於檢查語句中的 Err 並提前返回:? 操作符(在我看來,Go 可以用類似這樣的東西)。
5、異步性:Rust 的 async/await 還沒有完全準備好,但最終語法最近已經達成一致。Rust 標準庫中也有一些基本的線程特性,看上去比 Python 的更容易使用,但我沒有花太多的時間瞭解它。Go 似乎仍然有最好的特性。
6、工具:rustup 和 cargo 分別是非常優秀的語言版本管理器和包 / 模塊管理器實現。一切“正常”。我特別喜歡自動生成的文檔。對於這些工具,Python 提供的選項有點太簡單,需要小心對待,正如我之前提到的,Go 有一種奇怪的管理模塊方式,但除此之外,它的工具比 Python 的更好。
7、編輯器插件:我的. vimrc 文件大到令人尷尬,至少有三十多個插件。我有一些用於代碼檢查、自動補全以及格式化 Python 和 Go 的插件,但相比於其他兩種語言,Rust 插件更容易設置,更有用、更一致。 rust.vim 和 vim-lsp 插件(以及 Rust 語言服務器)是我獲得一個非常強大的配置所需要的全部。我還沒有測試其他編輯器,但是,藉助 Rust 提供的編輯器無關的優秀工具,我認為它們一樣有幫助。這個設置提供了我使用過的最好的“轉到定義”。它可以很好地適用於本地、標準庫和開箱即用的第三方代碼。
8、調試:我還沒有嘗試過 Rust 的調試器(因為其類型系統和 println! 已經讓我走得很遠),但是你可以使用 rust-gdb 和 rust-lldb,以及 rustup 初始安裝時帶有的 gdb 和 lldb 調試器的封裝器。如果你之前在編寫 C 語言代碼時使用過那些調試器,那麼其使用體驗將是意料之中的。正如前面提到的,編譯器的錯誤消息非常有幫助。
幾年前,我負責重寫一個圖像處理服務。為了弄清楚對於給定的圖像和一個或多個轉換(調整大小、圓形裁剪、修改格式等),我的新服務創建的輸出是否和舊服務一致,我必須自己檢查圖像。顯然,我需要自動化,但我找不到一個現有的 Python 庫可以告訴我,這兩張圖片在像素級上有什麼不同,因此有了diffimg ,它可以給你一個差異比 / 百分比,或生成差異圖像(檢出 readme,裡面有一個例子)。
我最初是用 Python 實現的(我最熟悉的語言),主要部分使用了 Pillow 。它可以用作庫或命令行工具。程序的基本部分非常小,只有幾十行,這要感謝Pillow。構建這個工具並不費力( xkcd 是對的,幾乎所有東西都有一個 Python 模塊),但它至少對於除我自己之外的幾十人是有用的。
幾個月前,我加入了一家公司,他們有幾個服務是用 Go 編寫的,我需要快速上手這門語言。編寫diffimg-go 看起來很有趣,甚至可能是一種有用的方法。這裡有一些來自經驗的興趣點,以及一些在工作中使用它的興趣點。
一、對比 Python 和 Go
(代碼: diffimg (Python)和 diffimg-go )
1、標準庫:Go 有一個相當不錯的 image 標準庫模塊,以及命令行 flag 解析庫。我不需要尋找任何外部依賴;diffimg-go 實現沒有依賴,而 Python 實現使用了相當重量級的第三方模塊(諷刺的是)Pillow。Go 的標準庫更有條理,而且經過深思熟慮,而 Python 的會逐步發展,它們是多年來由許多作者創建的,有許多不同的約定。Go 標準庫的一致性使開發者更易於預測任何給定的模塊將如何發揮作用,而且源代碼有非常好的文檔記錄。
(1)使用標準 image 庫的一個缺點是它不自動檢測圖像是否有一個 alpha 通道;所有圖像類型的像素值都有四個通道(RGBA)。因此,diffimg-go 實現要求用戶指明是否要使用 alpha 通道。這個小小的不便不值得找第三方庫來修復。
(2)一個很大的好處是,標準庫中有足夠的內容,你不需要像 Django 這樣的 Web 框架。用 Go 有可能在沒有任何依賴關係的情況下建立一個真正可用的 Web 服務。Python 號稱“自帶電池(batteries-included)”,但在我看來,Go 做得更好。
2、靜態類型系統:我過去使用靜態類型語言,但我過去幾年一直在使用 Python 編程。體驗起初有點煩人,感覺它好像只是減慢了我的速度,它迫使我過度明確,即使我偶爾錯了,Python 也會讓我做我想做的。有點像你給人發指令而對方總是打斷你讓你闡明你是什麼意思,而有的人則總是點頭,看上去已經理解你,但是你並不確定他們是否已經全部瞭解。它將大大減少與類型相關的 Bug,但是我發現,我仍然需要花幾乎相同的時間編寫測試。
(1)Go 的一個常見缺點是它沒有用戶可實現的泛型類型。雖然這不是一個構建大型可擴展應用程序的必備特性,但它肯定會減緩開發速度。雖然已經有替代模式建議,但是它們中沒有一個和真正的泛型類型一樣有效。
(2)靜態類型系統的一個優點是,可以更簡單快速地閱讀不熟悉的代碼庫。用好類型可以帶來許多動態類型系統中會丟失的額外信息。
3、接口和結構:Go 使用接口和結構,而 Python 使用類。在我看來,這可能是最有趣的區別,因為它迫使我區分定義行為的類型和保存信息的類型這兩個概念。Python 和其他傳統的面向對象的語言都鼓勵你將它們混在一起,但這兩種範式各有利弊:
(1)Go 強烈建議組合而不是繼承。雖然它通過嵌入繼承,不用類,但其數據和方法不是那麼容易傳遞。通常,我認為組合是更好的默認模式,但我不是一個絕對主義者,在某些情況下繼承更合適,所以我不喜歡語言幫我作出這個決定。
(2)接口實現的分離意味著,如果有許多類型彼此相似,你就需要多次編寫類似的代碼。由於缺少泛型類型,在 Go 中,有些情況下我無法重用代碼,不過在 Python 中可以。
(3)然而,由於 Go 是靜態類型的,當你編寫的代碼會導致運行時錯誤時,編譯器 / 源碼分析器(linter)會告訴你。Python 源碼分析器也可以有一點這樣的功能。但在 Python 中,當你試圖訪問一個可能不存在的方法或屬性時,由於語言的動態性,Python 源碼分析器無法確切地知道什麼方法 / 屬性存在,直到運行時。靜態定義的接口和結構是唯一在編譯時和開發過程中知道什麼可用的方法,這使得編譯時報錯的 Go 比運行時報錯的 Python 更可靠。
4、沒有可選參數:Go 只有可變函數,類似於 Python 的關鍵字參數,但不那麼有用,因為參數需要是相同的類型。我發現關鍵字參數是我真正懷念的特性,這主要是你可以把任何類型的一個 kwarg 扔給任何需要它的函數,而無需重寫它的每一個調用,這讓重構簡單了許多。我在工作中經常使用這個特性,它為我節省了很多時間。由於沒有該特性,這使得我在處理是否應該基於命令行標誌創建差異圖像時顯得有些笨拙。
5、冗長:Go 有點冗長(儘管不是像 Java 那麼冗長)。這部分是因為其類型系統沒有泛型,但主要是因為語言本身很小,沒有提供很多特性(你只有一種循環結構可以使用!)。我懷念Python 的列表推導式(list comprehensions)和其他函數式編程特性。如果你熟悉Python,你一兩天就可以學完 Tour of Go ,然後你就瞭解了整個語言。
6、錯誤處理:Python 有異常,而 Go 在可能出錯的地方通過從函數返回元組 value, error 來傳播錯誤。Python 允許你在調用棧中的任何位置捕獲錯誤,而不需要你一次又一次地手動將錯誤傳遞回去。這又使得代碼簡潔,而不像 Go 那樣被其臭名昭著的 if err != nil 模式搞得雜亂無章,不過你需要弄清楚函數及其內部的所有調用可能拋出的異常(使用 except Exception: 通常是一種糟糕的變通方案)。良好的文檔註釋和測試可以提供幫助,你在使用其中任何一種語言編程時都應該添加。Go 的系統絕對更安全。如果你忽視了 err 值,你還是可以搬起石頭砸自己的腳,但該系統使糟糕的主意變得更明顯。
7、第三方模塊:在 Go 模塊出現之前,Go 的包管理器會把所有下載的包扔到 GOPATH/src目錄下,而不是項目的目錄(像大多數其他語言)。
GOPATH/src目錄下,而不是項目的目錄(像大多數其他語言)。GOPATH 下這些模塊的路徑也會從託管包的 URL 構建,所以你的 import 語句將是類似 import " github.com/someuser/somepackage " 這個樣子。在幾乎所有 Go 代碼庫的源代碼中嵌入 github.com 似乎是一個奇怪的選擇。在任何情況下,Go 允許以傳統的方式做事,但 Go 模塊仍然是個很新的特性,所以在一段時間內,在缺少管理的 Go 代碼庫中,這種奇怪的行為仍將很常見。
8、異步性:Goroutines 是啟動異步任務的一種非常方便的方法。在 async/await 之前,Python 的異步解決方案有點令人沮喪。遺憾的是,我沒有用 Python 或 Go 編寫很多真實的異步代碼,而 diffimg 的簡單性似乎並不適合說明異步性的額外開銷,所以我沒有太多要說的,雖然我確實喜歡使用 Go 的channels 來處理多個異步任務。我的理解是,對於性能,Go 仍然佔了上風了,因為 goroutine 可以充分利用多處理器併發,而 Python 的基本 async/await 仍侷限於一個處理器,所以主要用於 I / O 密集型任務。
9、調試:Python 勝出。pdb(以及像 ipdb 這樣更復雜的選項)非常靈活,一旦你進入 REPL,你可以編寫任何你想要的代碼。 Delve 是一個很好的調試器,但和直接放入解釋器不一樣,語言的全部功能都唾手可得。
Go 總結
我對 Go 最初的印象是,由於它的抽象能力(有意為之)有限,所以它不像 Python 那樣有趣。Python 有更多的特性,因此有更多的方法來做一些事情,找到最快、最易讀或“最聰明”的解決方案可能會很有趣。Go 會積極地阻止你變得“聰明”。我認為,Go 的優勢在於它並不聰明。
它的極簡主義和缺乏自由限制了單個開發人員實現一個想法。然而,當項目擴展到幾十個或幾百個開發人員時,這個弱點變成了它的力量——因為每個人都使用同樣的語言特性小工具集,更容易統一,因此可以被他人理解。使用 Go 仍有可能編寫糟糕的代碼,但與那些更“強大”的語言相比,它讓你更難創造怪物。
使用一段時間後,我理解了為什麼像谷歌這樣的公司想要一門這樣的語言。新工程師不斷地加入到大量代碼庫的開發,在更復雜 / 更強大的語言中,在最後期限的壓力下,引入複雜性的速度比它被消除的速度更快。防止這種情況發生的最好方法是使用一種更不容易產生複雜性的語言。
所以說,我很高興工作在一個大型應用程序上下文中的 Go 代碼庫上,有一個多樣化和不斷增長的團隊。事實上,我認為我更喜歡它。我只是不想把它用於我自己的個人項目。
二、進入 Rust
幾周前,我決定嘗試學習 Rust。我之前曾試圖這樣做,但發現類型系統和借用檢查(borrow checker)讓人困惑,沒有足夠的背景信息,我不知道為什麼要把所有這些限制強加給我,對於我想做的任務來說大而笨重。然而,自那時以來,我對程序執行時內存中發生了什麼有了更多的瞭解。我從這本書開始,而不是試圖一頭扎進去。這有很大的幫助,這份介紹比我見過的任何編程語言的介紹都要好。
在我讀過這本書的前十幾章之後,我覺得自己有足夠的信心嘗試 diffimg 的另一個實現(這時,我覺得我的 Rust 經驗與我寫 diffimg-go 時的 Go 經驗一樣多)。這比我用 Go 實現的時間要長,而後者比用 Python 需要的時間更長。我認為,即使考慮到我對 Python 更加熟悉,這也是對的——兩種語言都有更多的東西要寫。
在編寫 diffimg-rs 時,我注意到一些事情。
1、類型系統:現在,我已經習慣 Go 中更基本的靜態類型系統了,但 Rust 更強大(也更復雜)。有了 Go 中接口和結構的基礎(它們都簡單得多),泛型類型、枚舉類型、traits、引用類型、生命週期就是全部我要額外學習的概念了。此外,Rust 使用其類型系統實現其它語言不使用類型系統實現的特性(如 Result ,我很快會介紹)。幸運的是,編譯器 / 源碼解析器非常有幫助,它可以告訴你你做錯了什麼,甚至經常告訴你如何解決這個問題。儘管如此,這比我學習 Go 的類型系統所用的時間要多得多,我還沒有適應它的所有特性。
(1)有一個地方,因為類型系統,我正在使用的圖像庫的實現會導致令人不快的代碼重複量。我最終只要匹配兩個最重要的枚舉類型,但匹配其他類型會導致另外半打左右幾乎相同的代碼行。在這個規模上,這不是一個問題,但它使我生氣。也許這裡使用宏是個不錯的主意,我仍然需要試驗。
2、手動內存管理:Python 和 Go 會幫你撿垃圾。C 允許你亂丟垃圾,但當它踩到你丟的香蕉皮時,它會大發脾氣。Rust 會拍你一下,並要求你自己清理乾淨。這會刺痛我,甚至超過了從動態類型語言遷移到靜態類型語言,因為我被寵壞了,通常都是由語言跟在我後面撿。此外,編譯器會設法儘可能地幫助你,但你仍然需要大量的學習才能理解到底發生了什麼。
(1)直接訪問內存(以及 Rust 的函數式編程特性)的一個好處是,它簡化了差異比的計算,因為我可以簡單地映射原始字節數組,而不必按座標索引每個像素。
3、函數式特性:Rust 強烈鼓勵函數式的方法:它有 FP 友好的類型系統(類似 Haskell)、不可變類型、閉包,迭代器,模式匹配等,但也允許命令式代碼。它類似於編寫 OCaml(有趣的是,最初的 Rust 編譯器是用 OCaml 編寫的)。因此,對於這門與 C 競爭的語言,代碼比你想象得更簡潔。
4、錯誤處理:不同於 Python 使用的異常模型,也不同於 Go 異常處理時返回的元組,Rust 利用枚舉類型:Result 返回 Ok(value) 或 Err(error)。這和 Go 的方式更接近,但更明確一些,而且利用了類型系統。還有一個語法糖用於檢查語句中的 Err 並提前返回:? 操作符(在我看來,Go 可以用類似這樣的東西)。
5、異步性:Rust 的 async/await 還沒有完全準備好,但最終語法最近已經達成一致。Rust 標準庫中也有一些基本的線程特性,看上去比 Python 的更容易使用,但我沒有花太多的時間瞭解它。Go 似乎仍然有最好的特性。
6、工具:rustup 和 cargo 分別是非常優秀的語言版本管理器和包 / 模塊管理器實現。一切“正常”。我特別喜歡自動生成的文檔。對於這些工具,Python 提供的選項有點太簡單,需要小心對待,正如我之前提到的,Go 有一種奇怪的管理模塊方式,但除此之外,它的工具比 Python 的更好。
7、編輯器插件:我的. vimrc 文件大到令人尷尬,至少有三十多個插件。我有一些用於代碼檢查、自動補全以及格式化 Python 和 Go 的插件,但相比於其他兩種語言,Rust 插件更容易設置,更有用、更一致。 rust.vim 和 vim-lsp 插件(以及 Rust 語言服務器)是我獲得一個非常強大的配置所需要的全部。我還沒有測試其他編輯器,但是,藉助 Rust 提供的編輯器無關的優秀工具,我認為它們一樣有幫助。這個設置提供了我使用過的最好的“轉到定義”。它可以很好地適用於本地、標準庫和開箱即用的第三方代碼。
8、調試:我還沒有嘗試過 Rust 的調試器(因為其類型系統和 println! 已經讓我走得很遠),但是你可以使用 rust-gdb 和 rust-lldb,以及 rustup 初始安裝時帶有的 gdb 和 lldb 調試器的封裝器。如果你之前在編寫 C 語言代碼時使用過那些調試器,那麼其使用體驗將是意料之中的。正如前面提到的,編譯器的錯誤消息非常有幫助。
Rust 總結
你至少要讀過這本書的前幾章,否則我絕對不建議你嘗試編寫 Rust 代碼,即使你已經熟悉 C 和內存管理。對於 Go 和 Python,只要你有一些另一種現代命令式編程語言的經驗,它們就不是很難入手,必要時可以參考文檔。Rust 是一門很大的語言。Python 也有很多特性,但是它們大部分是可選的。只要理解一些基本的數據結構和一些內置函數,你就可以完成很多工作。對於 Rust,你需要真正理解類型系統的固有複雜性和借用檢查,否則你會搞得十分複雜。
就我寫 Rust 時的感覺而言,它非常有趣,就像 Python 一樣。它廣泛的特性使它很有表現力。雖然編譯器會經常讓你停下,但它也非常有用,而且它對於如何解決你的借用問題 / 輸入問題的建議通常是有效的。正如我所提到的,這些工具是我遇到的所有語言中最好的,並且不像我使用的其他一些語言那樣給我帶來很多麻煩。我非常喜歡使用這種語言,並將在 Python 的性能還不夠好的地方繼續尋找使用 Rust 的機會。
三、代碼示例
我提取了每個 diffimg 中計算差異比的代碼塊。為了概括介紹 Python 的做法,這需要 Pillow 生成的差異圖像,對所有通道的所有像素值求和,並返回最大可能值(相同大小的純白圖像)除以總和的比值。
Python
幾年前,我負責重寫一個圖像處理服務。為了弄清楚對於給定的圖像和一個或多個轉換(調整大小、圓形裁剪、修改格式等),我的新服務創建的輸出是否和舊服務一致,我必須自己檢查圖像。顯然,我需要自動化,但我找不到一個現有的 Python 庫可以告訴我,這兩張圖片在像素級上有什麼不同,因此有了diffimg ,它可以給你一個差異比 / 百分比,或生成差異圖像(檢出 readme,裡面有一個例子)。
我最初是用 Python 實現的(我最熟悉的語言),主要部分使用了 Pillow 。它可以用作庫或命令行工具。程序的基本部分非常小,只有幾十行,這要感謝Pillow。構建這個工具並不費力( xkcd 是對的,幾乎所有東西都有一個 Python 模塊),但它至少對於除我自己之外的幾十人是有用的。
幾個月前,我加入了一家公司,他們有幾個服務是用 Go 編寫的,我需要快速上手這門語言。編寫diffimg-go 看起來很有趣,甚至可能是一種有用的方法。這裡有一些來自經驗的興趣點,以及一些在工作中使用它的興趣點。
一、對比 Python 和 Go
(代碼: diffimg (Python)和 diffimg-go )
1、標準庫:Go 有一個相當不錯的 image 標準庫模塊,以及命令行 flag 解析庫。我不需要尋找任何外部依賴;diffimg-go 實現沒有依賴,而 Python 實現使用了相當重量級的第三方模塊(諷刺的是)Pillow。Go 的標準庫更有條理,而且經過深思熟慮,而 Python 的會逐步發展,它們是多年來由許多作者創建的,有許多不同的約定。Go 標準庫的一致性使開發者更易於預測任何給定的模塊將如何發揮作用,而且源代碼有非常好的文檔記錄。
(1)使用標準 image 庫的一個缺點是它不自動檢測圖像是否有一個 alpha 通道;所有圖像類型的像素值都有四個通道(RGBA)。因此,diffimg-go 實現要求用戶指明是否要使用 alpha 通道。這個小小的不便不值得找第三方庫來修復。
(2)一個很大的好處是,標準庫中有足夠的內容,你不需要像 Django 這樣的 Web 框架。用 Go 有可能在沒有任何依賴關係的情況下建立一個真正可用的 Web 服務。Python 號稱“自帶電池(batteries-included)”,但在我看來,Go 做得更好。
2、靜態類型系統:我過去使用靜態類型語言,但我過去幾年一直在使用 Python 編程。體驗起初有點煩人,感覺它好像只是減慢了我的速度,它迫使我過度明確,即使我偶爾錯了,Python 也會讓我做我想做的。有點像你給人發指令而對方總是打斷你讓你闡明你是什麼意思,而有的人則總是點頭,看上去已經理解你,但是你並不確定他們是否已經全部瞭解。它將大大減少與類型相關的 Bug,但是我發現,我仍然需要花幾乎相同的時間編寫測試。
(1)Go 的一個常見缺點是它沒有用戶可實現的泛型類型。雖然這不是一個構建大型可擴展應用程序的必備特性,但它肯定會減緩開發速度。雖然已經有替代模式建議,但是它們中沒有一個和真正的泛型類型一樣有效。
(2)靜態類型系統的一個優點是,可以更簡單快速地閱讀不熟悉的代碼庫。用好類型可以帶來許多動態類型系統中會丟失的額外信息。
3、接口和結構:Go 使用接口和結構,而 Python 使用類。在我看來,這可能是最有趣的區別,因為它迫使我區分定義行為的類型和保存信息的類型這兩個概念。Python 和其他傳統的面向對象的語言都鼓勵你將它們混在一起,但這兩種範式各有利弊:
(1)Go 強烈建議組合而不是繼承。雖然它通過嵌入繼承,不用類,但其數據和方法不是那麼容易傳遞。通常,我認為組合是更好的默認模式,但我不是一個絕對主義者,在某些情況下繼承更合適,所以我不喜歡語言幫我作出這個決定。
(2)接口實現的分離意味著,如果有許多類型彼此相似,你就需要多次編寫類似的代碼。由於缺少泛型類型,在 Go 中,有些情況下我無法重用代碼,不過在 Python 中可以。
(3)然而,由於 Go 是靜態類型的,當你編寫的代碼會導致運行時錯誤時,編譯器 / 源碼分析器(linter)會告訴你。Python 源碼分析器也可以有一點這樣的功能。但在 Python 中,當你試圖訪問一個可能不存在的方法或屬性時,由於語言的動態性,Python 源碼分析器無法確切地知道什麼方法 / 屬性存在,直到運行時。靜態定義的接口和結構是唯一在編譯時和開發過程中知道什麼可用的方法,這使得編譯時報錯的 Go 比運行時報錯的 Python 更可靠。
4、沒有可選參數:Go 只有可變函數,類似於 Python 的關鍵字參數,但不那麼有用,因為參數需要是相同的類型。我發現關鍵字參數是我真正懷念的特性,這主要是你可以把任何類型的一個 kwarg 扔給任何需要它的函數,而無需重寫它的每一個調用,這讓重構簡單了許多。我在工作中經常使用這個特性,它為我節省了很多時間。由於沒有該特性,這使得我在處理是否應該基於命令行標誌創建差異圖像時顯得有些笨拙。
5、冗長:Go 有點冗長(儘管不是像 Java 那麼冗長)。這部分是因為其類型系統沒有泛型,但主要是因為語言本身很小,沒有提供很多特性(你只有一種循環結構可以使用!)。我懷念Python 的列表推導式(list comprehensions)和其他函數式編程特性。如果你熟悉Python,你一兩天就可以學完 Tour of Go ,然後你就瞭解了整個語言。
6、錯誤處理:Python 有異常,而 Go 在可能出錯的地方通過從函數返回元組 value, error 來傳播錯誤。Python 允許你在調用棧中的任何位置捕獲錯誤,而不需要你一次又一次地手動將錯誤傳遞回去。這又使得代碼簡潔,而不像 Go 那樣被其臭名昭著的 if err != nil 模式搞得雜亂無章,不過你需要弄清楚函數及其內部的所有調用可能拋出的異常(使用 except Exception: 通常是一種糟糕的變通方案)。良好的文檔註釋和測試可以提供幫助,你在使用其中任何一種語言編程時都應該添加。Go 的系統絕對更安全。如果你忽視了 err 值,你還是可以搬起石頭砸自己的腳,但該系統使糟糕的主意變得更明顯。
7、第三方模塊:在 Go 模塊出現之前,Go 的包管理器會把所有下載的包扔到 GOPATH/src目錄下,而不是項目的目錄(像大多數其他語言)。
GOPATH/src目錄下,而不是項目的目錄(像大多數其他語言)。GOPATH 下這些模塊的路徑也會從託管包的 URL 構建,所以你的 import 語句將是類似 import " github.com/someuser/somepackage " 這個樣子。在幾乎所有 Go 代碼庫的源代碼中嵌入 github.com 似乎是一個奇怪的選擇。在任何情況下,Go 允許以傳統的方式做事,但 Go 模塊仍然是個很新的特性,所以在一段時間內,在缺少管理的 Go 代碼庫中,這種奇怪的行為仍將很常見。
8、異步性:Goroutines 是啟動異步任務的一種非常方便的方法。在 async/await 之前,Python 的異步解決方案有點令人沮喪。遺憾的是,我沒有用 Python 或 Go 編寫很多真實的異步代碼,而 diffimg 的簡單性似乎並不適合說明異步性的額外開銷,所以我沒有太多要說的,雖然我確實喜歡使用 Go 的channels 來處理多個異步任務。我的理解是,對於性能,Go 仍然佔了上風了,因為 goroutine 可以充分利用多處理器併發,而 Python 的基本 async/await 仍侷限於一個處理器,所以主要用於 I / O 密集型任務。
9、調試:Python 勝出。pdb(以及像 ipdb 這樣更復雜的選項)非常靈活,一旦你進入 REPL,你可以編寫任何你想要的代碼。 Delve 是一個很好的調試器,但和直接放入解釋器不一樣,語言的全部功能都唾手可得。
Go 總結
我對 Go 最初的印象是,由於它的抽象能力(有意為之)有限,所以它不像 Python 那樣有趣。Python 有更多的特性,因此有更多的方法來做一些事情,找到最快、最易讀或“最聰明”的解決方案可能會很有趣。Go 會積極地阻止你變得“聰明”。我認為,Go 的優勢在於它並不聰明。
它的極簡主義和缺乏自由限制了單個開發人員實現一個想法。然而,當項目擴展到幾十個或幾百個開發人員時,這個弱點變成了它的力量——因為每個人都使用同樣的語言特性小工具集,更容易統一,因此可以被他人理解。使用 Go 仍有可能編寫糟糕的代碼,但與那些更“強大”的語言相比,它讓你更難創造怪物。
使用一段時間後,我理解了為什麼像谷歌這樣的公司想要一門這樣的語言。新工程師不斷地加入到大量代碼庫的開發,在更復雜 / 更強大的語言中,在最後期限的壓力下,引入複雜性的速度比它被消除的速度更快。防止這種情況發生的最好方法是使用一種更不容易產生複雜性的語言。
所以說,我很高興工作在一個大型應用程序上下文中的 Go 代碼庫上,有一個多樣化和不斷增長的團隊。事實上,我認為我更喜歡它。我只是不想把它用於我自己的個人項目。
二、進入 Rust
幾周前,我決定嘗試學習 Rust。我之前曾試圖這樣做,但發現類型系統和借用檢查(borrow checker)讓人困惑,沒有足夠的背景信息,我不知道為什麼要把所有這些限制強加給我,對於我想做的任務來說大而笨重。然而,自那時以來,我對程序執行時內存中發生了什麼有了更多的瞭解。我從這本書開始,而不是試圖一頭扎進去。這有很大的幫助,這份介紹比我見過的任何編程語言的介紹都要好。
在我讀過這本書的前十幾章之後,我覺得自己有足夠的信心嘗試 diffimg 的另一個實現(這時,我覺得我的 Rust 經驗與我寫 diffimg-go 時的 Go 經驗一樣多)。這比我用 Go 實現的時間要長,而後者比用 Python 需要的時間更長。我認為,即使考慮到我對 Python 更加熟悉,這也是對的——兩種語言都有更多的東西要寫。
在編寫 diffimg-rs 時,我注意到一些事情。
1、類型系統:現在,我已經習慣 Go 中更基本的靜態類型系統了,但 Rust 更強大(也更復雜)。有了 Go 中接口和結構的基礎(它們都簡單得多),泛型類型、枚舉類型、traits、引用類型、生命週期就是全部我要額外學習的概念了。此外,Rust 使用其類型系統實現其它語言不使用類型系統實現的特性(如 Result ,我很快會介紹)。幸運的是,編譯器 / 源碼解析器非常有幫助,它可以告訴你你做錯了什麼,甚至經常告訴你如何解決這個問題。儘管如此,這比我學習 Go 的類型系統所用的時間要多得多,我還沒有適應它的所有特性。
(1)有一個地方,因為類型系統,我正在使用的圖像庫的實現會導致令人不快的代碼重複量。我最終只要匹配兩個最重要的枚舉類型,但匹配其他類型會導致另外半打左右幾乎相同的代碼行。在這個規模上,這不是一個問題,但它使我生氣。也許這裡使用宏是個不錯的主意,我仍然需要試驗。
2、手動內存管理:Python 和 Go 會幫你撿垃圾。C 允許你亂丟垃圾,但當它踩到你丟的香蕉皮時,它會大發脾氣。Rust 會拍你一下,並要求你自己清理乾淨。這會刺痛我,甚至超過了從動態類型語言遷移到靜態類型語言,因為我被寵壞了,通常都是由語言跟在我後面撿。此外,編譯器會設法儘可能地幫助你,但你仍然需要大量的學習才能理解到底發生了什麼。
(1)直接訪問內存(以及 Rust 的函數式編程特性)的一個好處是,它簡化了差異比的計算,因為我可以簡單地映射原始字節數組,而不必按座標索引每個像素。
3、函數式特性:Rust 強烈鼓勵函數式的方法:它有 FP 友好的類型系統(類似 Haskell)、不可變類型、閉包,迭代器,模式匹配等,但也允許命令式代碼。它類似於編寫 OCaml(有趣的是,最初的 Rust 編譯器是用 OCaml 編寫的)。因此,對於這門與 C 競爭的語言,代碼比你想象得更簡潔。
4、錯誤處理:不同於 Python 使用的異常模型,也不同於 Go 異常處理時返回的元組,Rust 利用枚舉類型:Result 返回 Ok(value) 或 Err(error)。這和 Go 的方式更接近,但更明確一些,而且利用了類型系統。還有一個語法糖用於檢查語句中的 Err 並提前返回:? 操作符(在我看來,Go 可以用類似這樣的東西)。
5、異步性:Rust 的 async/await 還沒有完全準備好,但最終語法最近已經達成一致。Rust 標準庫中也有一些基本的線程特性,看上去比 Python 的更容易使用,但我沒有花太多的時間瞭解它。Go 似乎仍然有最好的特性。
6、工具:rustup 和 cargo 分別是非常優秀的語言版本管理器和包 / 模塊管理器實現。一切“正常”。我特別喜歡自動生成的文檔。對於這些工具,Python 提供的選項有點太簡單,需要小心對待,正如我之前提到的,Go 有一種奇怪的管理模塊方式,但除此之外,它的工具比 Python 的更好。
7、編輯器插件:我的. vimrc 文件大到令人尷尬,至少有三十多個插件。我有一些用於代碼檢查、自動補全以及格式化 Python 和 Go 的插件,但相比於其他兩種語言,Rust 插件更容易設置,更有用、更一致。 rust.vim 和 vim-lsp 插件(以及 Rust 語言服務器)是我獲得一個非常強大的配置所需要的全部。我還沒有測試其他編輯器,但是,藉助 Rust 提供的編輯器無關的優秀工具,我認為它們一樣有幫助。這個設置提供了我使用過的最好的“轉到定義”。它可以很好地適用於本地、標準庫和開箱即用的第三方代碼。
8、調試:我還沒有嘗試過 Rust 的調試器(因為其類型系統和 println! 已經讓我走得很遠),但是你可以使用 rust-gdb 和 rust-lldb,以及 rustup 初始安裝時帶有的 gdb 和 lldb 調試器的封裝器。如果你之前在編寫 C 語言代碼時使用過那些調試器,那麼其使用體驗將是意料之中的。正如前面提到的,編譯器的錯誤消息非常有幫助。
Rust 總結
你至少要讀過這本書的前幾章,否則我絕對不建議你嘗試編寫 Rust 代碼,即使你已經熟悉 C 和內存管理。對於 Go 和 Python,只要你有一些另一種現代命令式編程語言的經驗,它們就不是很難入手,必要時可以參考文檔。Rust 是一門很大的語言。Python 也有很多特性,但是它們大部分是可選的。只要理解一些基本的數據結構和一些內置函數,你就可以完成很多工作。對於 Rust,你需要真正理解類型系統的固有複雜性和借用檢查,否則你會搞得十分複雜。
就我寫 Rust 時的感覺而言,它非常有趣,就像 Python 一樣。它廣泛的特性使它很有表現力。雖然編譯器會經常讓你停下,但它也非常有用,而且它對於如何解決你的借用問題 / 輸入問題的建議通常是有效的。正如我所提到的,這些工具是我遇到的所有語言中最好的,並且不像我使用的其他一些語言那樣給我帶來很多麻煩。我非常喜歡使用這種語言,並將在 Python 的性能還不夠好的地方繼續尋找使用 Rust 的機會。
三、代碼示例
我提取了每個 diffimg 中計算差異比的代碼塊。為了概括介紹 Python 的做法,這需要 Pillow 生成的差異圖像,對所有通道的所有像素值求和,並返回最大可能值(相同大小的純白圖像)除以總和的比值。
Python
對於 Go 和 Rust,方法有點不同:我們不用創建一個差異圖像,我們只要遍歷兩幅輸入圖像,並對每個像素的差異求和。在 Go 中,我們用座標索引每幅圖像……
Go
幾年前,我負責重寫一個圖像處理服務。為了弄清楚對於給定的圖像和一個或多個轉換(調整大小、圓形裁剪、修改格式等),我的新服務創建的輸出是否和舊服務一致,我必須自己檢查圖像。顯然,我需要自動化,但我找不到一個現有的 Python 庫可以告訴我,這兩張圖片在像素級上有什麼不同,因此有了diffimg ,它可以給你一個差異比 / 百分比,或生成差異圖像(檢出 readme,裡面有一個例子)。
我最初是用 Python 實現的(我最熟悉的語言),主要部分使用了 Pillow 。它可以用作庫或命令行工具。程序的基本部分非常小,只有幾十行,這要感謝Pillow。構建這個工具並不費力( xkcd 是對的,幾乎所有東西都有一個 Python 模塊),但它至少對於除我自己之外的幾十人是有用的。
幾個月前,我加入了一家公司,他們有幾個服務是用 Go 編寫的,我需要快速上手這門語言。編寫diffimg-go 看起來很有趣,甚至可能是一種有用的方法。這裡有一些來自經驗的興趣點,以及一些在工作中使用它的興趣點。
一、對比 Python 和 Go
(代碼: diffimg (Python)和 diffimg-go )
1、標準庫:Go 有一個相當不錯的 image 標準庫模塊,以及命令行 flag 解析庫。我不需要尋找任何外部依賴;diffimg-go 實現沒有依賴,而 Python 實現使用了相當重量級的第三方模塊(諷刺的是)Pillow。Go 的標準庫更有條理,而且經過深思熟慮,而 Python 的會逐步發展,它們是多年來由許多作者創建的,有許多不同的約定。Go 標準庫的一致性使開發者更易於預測任何給定的模塊將如何發揮作用,而且源代碼有非常好的文檔記錄。
(1)使用標準 image 庫的一個缺點是它不自動檢測圖像是否有一個 alpha 通道;所有圖像類型的像素值都有四個通道(RGBA)。因此,diffimg-go 實現要求用戶指明是否要使用 alpha 通道。這個小小的不便不值得找第三方庫來修復。
(2)一個很大的好處是,標準庫中有足夠的內容,你不需要像 Django 這樣的 Web 框架。用 Go 有可能在沒有任何依賴關係的情況下建立一個真正可用的 Web 服務。Python 號稱“自帶電池(batteries-included)”,但在我看來,Go 做得更好。
2、靜態類型系統:我過去使用靜態類型語言,但我過去幾年一直在使用 Python 編程。體驗起初有點煩人,感覺它好像只是減慢了我的速度,它迫使我過度明確,即使我偶爾錯了,Python 也會讓我做我想做的。有點像你給人發指令而對方總是打斷你讓你闡明你是什麼意思,而有的人則總是點頭,看上去已經理解你,但是你並不確定他們是否已經全部瞭解。它將大大減少與類型相關的 Bug,但是我發現,我仍然需要花幾乎相同的時間編寫測試。
(1)Go 的一個常見缺點是它沒有用戶可實現的泛型類型。雖然這不是一個構建大型可擴展應用程序的必備特性,但它肯定會減緩開發速度。雖然已經有替代模式建議,但是它們中沒有一個和真正的泛型類型一樣有效。
(2)靜態類型系統的一個優點是,可以更簡單快速地閱讀不熟悉的代碼庫。用好類型可以帶來許多動態類型系統中會丟失的額外信息。
3、接口和結構:Go 使用接口和結構,而 Python 使用類。在我看來,這可能是最有趣的區別,因為它迫使我區分定義行為的類型和保存信息的類型這兩個概念。Python 和其他傳統的面向對象的語言都鼓勵你將它們混在一起,但這兩種範式各有利弊:
(1)Go 強烈建議組合而不是繼承。雖然它通過嵌入繼承,不用類,但其數據和方法不是那麼容易傳遞。通常,我認為組合是更好的默認模式,但我不是一個絕對主義者,在某些情況下繼承更合適,所以我不喜歡語言幫我作出這個決定。
(2)接口實現的分離意味著,如果有許多類型彼此相似,你就需要多次編寫類似的代碼。由於缺少泛型類型,在 Go 中,有些情況下我無法重用代碼,不過在 Python 中可以。
(3)然而,由於 Go 是靜態類型的,當你編寫的代碼會導致運行時錯誤時,編譯器 / 源碼分析器(linter)會告訴你。Python 源碼分析器也可以有一點這樣的功能。但在 Python 中,當你試圖訪問一個可能不存在的方法或屬性時,由於語言的動態性,Python 源碼分析器無法確切地知道什麼方法 / 屬性存在,直到運行時。靜態定義的接口和結構是唯一在編譯時和開發過程中知道什麼可用的方法,這使得編譯時報錯的 Go 比運行時報錯的 Python 更可靠。
4、沒有可選參數:Go 只有可變函數,類似於 Python 的關鍵字參數,但不那麼有用,因為參數需要是相同的類型。我發現關鍵字參數是我真正懷念的特性,這主要是你可以把任何類型的一個 kwarg 扔給任何需要它的函數,而無需重寫它的每一個調用,這讓重構簡單了許多。我在工作中經常使用這個特性,它為我節省了很多時間。由於沒有該特性,這使得我在處理是否應該基於命令行標誌創建差異圖像時顯得有些笨拙。
5、冗長:Go 有點冗長(儘管不是像 Java 那麼冗長)。這部分是因為其類型系統沒有泛型,但主要是因為語言本身很小,沒有提供很多特性(你只有一種循環結構可以使用!)。我懷念Python 的列表推導式(list comprehensions)和其他函數式編程特性。如果你熟悉Python,你一兩天就可以學完 Tour of Go ,然後你就瞭解了整個語言。
6、錯誤處理:Python 有異常,而 Go 在可能出錯的地方通過從函數返回元組 value, error 來傳播錯誤。Python 允許你在調用棧中的任何位置捕獲錯誤,而不需要你一次又一次地手動將錯誤傳遞回去。這又使得代碼簡潔,而不像 Go 那樣被其臭名昭著的 if err != nil 模式搞得雜亂無章,不過你需要弄清楚函數及其內部的所有調用可能拋出的異常(使用 except Exception: 通常是一種糟糕的變通方案)。良好的文檔註釋和測試可以提供幫助,你在使用其中任何一種語言編程時都應該添加。Go 的系統絕對更安全。如果你忽視了 err 值,你還是可以搬起石頭砸自己的腳,但該系統使糟糕的主意變得更明顯。
7、第三方模塊:在 Go 模塊出現之前,Go 的包管理器會把所有下載的包扔到 GOPATH/src目錄下,而不是項目的目錄(像大多數其他語言)。
GOPATH/src目錄下,而不是項目的目錄(像大多數其他語言)。GOPATH 下這些模塊的路徑也會從託管包的 URL 構建,所以你的 import 語句將是類似 import " github.com/someuser/somepackage " 這個樣子。在幾乎所有 Go 代碼庫的源代碼中嵌入 github.com 似乎是一個奇怪的選擇。在任何情況下,Go 允許以傳統的方式做事,但 Go 模塊仍然是個很新的特性,所以在一段時間內,在缺少管理的 Go 代碼庫中,這種奇怪的行為仍將很常見。
8、異步性:Goroutines 是啟動異步任務的一種非常方便的方法。在 async/await 之前,Python 的異步解決方案有點令人沮喪。遺憾的是,我沒有用 Python 或 Go 編寫很多真實的異步代碼,而 diffimg 的簡單性似乎並不適合說明異步性的額外開銷,所以我沒有太多要說的,雖然我確實喜歡使用 Go 的channels 來處理多個異步任務。我的理解是,對於性能,Go 仍然佔了上風了,因為 goroutine 可以充分利用多處理器併發,而 Python 的基本 async/await 仍侷限於一個處理器,所以主要用於 I / O 密集型任務。
9、調試:Python 勝出。pdb(以及像 ipdb 這樣更復雜的選項)非常靈活,一旦你進入 REPL,你可以編寫任何你想要的代碼。 Delve 是一個很好的調試器,但和直接放入解釋器不一樣,語言的全部功能都唾手可得。
Go 總結
我對 Go 最初的印象是,由於它的抽象能力(有意為之)有限,所以它不像 Python 那樣有趣。Python 有更多的特性,因此有更多的方法來做一些事情,找到最快、最易讀或“最聰明”的解決方案可能會很有趣。Go 會積極地阻止你變得“聰明”。我認為,Go 的優勢在於它並不聰明。
它的極簡主義和缺乏自由限制了單個開發人員實現一個想法。然而,當項目擴展到幾十個或幾百個開發人員時,這個弱點變成了它的力量——因為每個人都使用同樣的語言特性小工具集,更容易統一,因此可以被他人理解。使用 Go 仍有可能編寫糟糕的代碼,但與那些更“強大”的語言相比,它讓你更難創造怪物。
使用一段時間後,我理解了為什麼像谷歌這樣的公司想要一門這樣的語言。新工程師不斷地加入到大量代碼庫的開發,在更復雜 / 更強大的語言中,在最後期限的壓力下,引入複雜性的速度比它被消除的速度更快。防止這種情況發生的最好方法是使用一種更不容易產生複雜性的語言。
所以說,我很高興工作在一個大型應用程序上下文中的 Go 代碼庫上,有一個多樣化和不斷增長的團隊。事實上,我認為我更喜歡它。我只是不想把它用於我自己的個人項目。
二、進入 Rust
幾周前,我決定嘗試學習 Rust。我之前曾試圖這樣做,但發現類型系統和借用檢查(borrow checker)讓人困惑,沒有足夠的背景信息,我不知道為什麼要把所有這些限制強加給我,對於我想做的任務來說大而笨重。然而,自那時以來,我對程序執行時內存中發生了什麼有了更多的瞭解。我從這本書開始,而不是試圖一頭扎進去。這有很大的幫助,這份介紹比我見過的任何編程語言的介紹都要好。
在我讀過這本書的前十幾章之後,我覺得自己有足夠的信心嘗試 diffimg 的另一個實現(這時,我覺得我的 Rust 經驗與我寫 diffimg-go 時的 Go 經驗一樣多)。這比我用 Go 實現的時間要長,而後者比用 Python 需要的時間更長。我認為,即使考慮到我對 Python 更加熟悉,這也是對的——兩種語言都有更多的東西要寫。
在編寫 diffimg-rs 時,我注意到一些事情。
1、類型系統:現在,我已經習慣 Go 中更基本的靜態類型系統了,但 Rust 更強大(也更復雜)。有了 Go 中接口和結構的基礎(它們都簡單得多),泛型類型、枚舉類型、traits、引用類型、生命週期就是全部我要額外學習的概念了。此外,Rust 使用其類型系統實現其它語言不使用類型系統實現的特性(如 Result ,我很快會介紹)。幸運的是,編譯器 / 源碼解析器非常有幫助,它可以告訴你你做錯了什麼,甚至經常告訴你如何解決這個問題。儘管如此,這比我學習 Go 的類型系統所用的時間要多得多,我還沒有適應它的所有特性。
(1)有一個地方,因為類型系統,我正在使用的圖像庫的實現會導致令人不快的代碼重複量。我最終只要匹配兩個最重要的枚舉類型,但匹配其他類型會導致另外半打左右幾乎相同的代碼行。在這個規模上,這不是一個問題,但它使我生氣。也許這裡使用宏是個不錯的主意,我仍然需要試驗。
2、手動內存管理:Python 和 Go 會幫你撿垃圾。C 允許你亂丟垃圾,但當它踩到你丟的香蕉皮時,它會大發脾氣。Rust 會拍你一下,並要求你自己清理乾淨。這會刺痛我,甚至超過了從動態類型語言遷移到靜態類型語言,因為我被寵壞了,通常都是由語言跟在我後面撿。此外,編譯器會設法儘可能地幫助你,但你仍然需要大量的學習才能理解到底發生了什麼。
(1)直接訪問內存(以及 Rust 的函數式編程特性)的一個好處是,它簡化了差異比的計算,因為我可以簡單地映射原始字節數組,而不必按座標索引每個像素。
3、函數式特性:Rust 強烈鼓勵函數式的方法:它有 FP 友好的類型系統(類似 Haskell)、不可變類型、閉包,迭代器,模式匹配等,但也允許命令式代碼。它類似於編寫 OCaml(有趣的是,最初的 Rust 編譯器是用 OCaml 編寫的)。因此,對於這門與 C 競爭的語言,代碼比你想象得更簡潔。
4、錯誤處理:不同於 Python 使用的異常模型,也不同於 Go 異常處理時返回的元組,Rust 利用枚舉類型:Result 返回 Ok(value) 或 Err(error)。這和 Go 的方式更接近,但更明確一些,而且利用了類型系統。還有一個語法糖用於檢查語句中的 Err 並提前返回:? 操作符(在我看來,Go 可以用類似這樣的東西)。
5、異步性:Rust 的 async/await 還沒有完全準備好,但最終語法最近已經達成一致。Rust 標準庫中也有一些基本的線程特性,看上去比 Python 的更容易使用,但我沒有花太多的時間瞭解它。Go 似乎仍然有最好的特性。
6、工具:rustup 和 cargo 分別是非常優秀的語言版本管理器和包 / 模塊管理器實現。一切“正常”。我特別喜歡自動生成的文檔。對於這些工具,Python 提供的選項有點太簡單,需要小心對待,正如我之前提到的,Go 有一種奇怪的管理模塊方式,但除此之外,它的工具比 Python 的更好。
7、編輯器插件:我的. vimrc 文件大到令人尷尬,至少有三十多個插件。我有一些用於代碼檢查、自動補全以及格式化 Python 和 Go 的插件,但相比於其他兩種語言,Rust 插件更容易設置,更有用、更一致。 rust.vim 和 vim-lsp 插件(以及 Rust 語言服務器)是我獲得一個非常強大的配置所需要的全部。我還沒有測試其他編輯器,但是,藉助 Rust 提供的編輯器無關的優秀工具,我認為它們一樣有幫助。這個設置提供了我使用過的最好的“轉到定義”。它可以很好地適用於本地、標準庫和開箱即用的第三方代碼。
8、調試:我還沒有嘗試過 Rust 的調試器(因為其類型系統和 println! 已經讓我走得很遠),但是你可以使用 rust-gdb 和 rust-lldb,以及 rustup 初始安裝時帶有的 gdb 和 lldb 調試器的封裝器。如果你之前在編寫 C 語言代碼時使用過那些調試器,那麼其使用體驗將是意料之中的。正如前面提到的,編譯器的錯誤消息非常有幫助。
Rust 總結
你至少要讀過這本書的前幾章,否則我絕對不建議你嘗試編寫 Rust 代碼,即使你已經熟悉 C 和內存管理。對於 Go 和 Python,只要你有一些另一種現代命令式編程語言的經驗,它們就不是很難入手,必要時可以參考文檔。Rust 是一門很大的語言。Python 也有很多特性,但是它們大部分是可選的。只要理解一些基本的數據結構和一些內置函數,你就可以完成很多工作。對於 Rust,你需要真正理解類型系統的固有複雜性和借用檢查,否則你會搞得十分複雜。
就我寫 Rust 時的感覺而言,它非常有趣,就像 Python 一樣。它廣泛的特性使它很有表現力。雖然編譯器會經常讓你停下,但它也非常有用,而且它對於如何解決你的借用問題 / 輸入問題的建議通常是有效的。正如我所提到的,這些工具是我遇到的所有語言中最好的,並且不像我使用的其他一些語言那樣給我帶來很多麻煩。我非常喜歡使用這種語言,並將在 Python 的性能還不夠好的地方繼續尋找使用 Rust 的機會。
三、代碼示例
我提取了每個 diffimg 中計算差異比的代碼塊。為了概括介紹 Python 的做法,這需要 Pillow 生成的差異圖像,對所有通道的所有像素值求和,並返回最大可能值(相同大小的純白圖像)除以總和的比值。
Python
對於 Go 和 Rust,方法有點不同:我們不用創建一個差異圖像,我們只要遍歷兩幅輸入圖像,並對每個像素的差異求和。在 Go 中,我們用座標索引每幅圖像……
Go
……但在 Rust 中,我們將圖像視為它們真的是在內存中,是一系列可以壓縮到一起並消費的字節。
Rust
幾年前,我負責重寫一個圖像處理服務。為了弄清楚對於給定的圖像和一個或多個轉換(調整大小、圓形裁剪、修改格式等),我的新服務創建的輸出是否和舊服務一致,我必須自己檢查圖像。顯然,我需要自動化,但我找不到一個現有的 Python 庫可以告訴我,這兩張圖片在像素級上有什麼不同,因此有了diffimg ,它可以給你一個差異比 / 百分比,或生成差異圖像(檢出 readme,裡面有一個例子)。
我最初是用 Python 實現的(我最熟悉的語言),主要部分使用了 Pillow 。它可以用作庫或命令行工具。程序的基本部分非常小,只有幾十行,這要感謝Pillow。構建這個工具並不費力( xkcd 是對的,幾乎所有東西都有一個 Python 模塊),但它至少對於除我自己之外的幾十人是有用的。
幾個月前,我加入了一家公司,他們有幾個服務是用 Go 編寫的,我需要快速上手這門語言。編寫diffimg-go 看起來很有趣,甚至可能是一種有用的方法。這裡有一些來自經驗的興趣點,以及一些在工作中使用它的興趣點。
一、對比 Python 和 Go
(代碼: diffimg (Python)和 diffimg-go )
1、標準庫:Go 有一個相當不錯的 image 標準庫模塊,以及命令行 flag 解析庫。我不需要尋找任何外部依賴;diffimg-go 實現沒有依賴,而 Python 實現使用了相當重量級的第三方模塊(諷刺的是)Pillow。Go 的標準庫更有條理,而且經過深思熟慮,而 Python 的會逐步發展,它們是多年來由許多作者創建的,有許多不同的約定。Go 標準庫的一致性使開發者更易於預測任何給定的模塊將如何發揮作用,而且源代碼有非常好的文檔記錄。
(1)使用標準 image 庫的一個缺點是它不自動檢測圖像是否有一個 alpha 通道;所有圖像類型的像素值都有四個通道(RGBA)。因此,diffimg-go 實現要求用戶指明是否要使用 alpha 通道。這個小小的不便不值得找第三方庫來修復。
(2)一個很大的好處是,標準庫中有足夠的內容,你不需要像 Django 這樣的 Web 框架。用 Go 有可能在沒有任何依賴關係的情況下建立一個真正可用的 Web 服務。Python 號稱“自帶電池(batteries-included)”,但在我看來,Go 做得更好。
2、靜態類型系統:我過去使用靜態類型語言,但我過去幾年一直在使用 Python 編程。體驗起初有點煩人,感覺它好像只是減慢了我的速度,它迫使我過度明確,即使我偶爾錯了,Python 也會讓我做我想做的。有點像你給人發指令而對方總是打斷你讓你闡明你是什麼意思,而有的人則總是點頭,看上去已經理解你,但是你並不確定他們是否已經全部瞭解。它將大大減少與類型相關的 Bug,但是我發現,我仍然需要花幾乎相同的時間編寫測試。
(1)Go 的一個常見缺點是它沒有用戶可實現的泛型類型。雖然這不是一個構建大型可擴展應用程序的必備特性,但它肯定會減緩開發速度。雖然已經有替代模式建議,但是它們中沒有一個和真正的泛型類型一樣有效。
(2)靜態類型系統的一個優點是,可以更簡單快速地閱讀不熟悉的代碼庫。用好類型可以帶來許多動態類型系統中會丟失的額外信息。
3、接口和結構:Go 使用接口和結構,而 Python 使用類。在我看來,這可能是最有趣的區別,因為它迫使我區分定義行為的類型和保存信息的類型這兩個概念。Python 和其他傳統的面向對象的語言都鼓勵你將它們混在一起,但這兩種範式各有利弊:
(1)Go 強烈建議組合而不是繼承。雖然它通過嵌入繼承,不用類,但其數據和方法不是那麼容易傳遞。通常,我認為組合是更好的默認模式,但我不是一個絕對主義者,在某些情況下繼承更合適,所以我不喜歡語言幫我作出這個決定。
(2)接口實現的分離意味著,如果有許多類型彼此相似,你就需要多次編寫類似的代碼。由於缺少泛型類型,在 Go 中,有些情況下我無法重用代碼,不過在 Python 中可以。
(3)然而,由於 Go 是靜態類型的,當你編寫的代碼會導致運行時錯誤時,編譯器 / 源碼分析器(linter)會告訴你。Python 源碼分析器也可以有一點這樣的功能。但在 Python 中,當你試圖訪問一個可能不存在的方法或屬性時,由於語言的動態性,Python 源碼分析器無法確切地知道什麼方法 / 屬性存在,直到運行時。靜態定義的接口和結構是唯一在編譯時和開發過程中知道什麼可用的方法,這使得編譯時報錯的 Go 比運行時報錯的 Python 更可靠。
4、沒有可選參數:Go 只有可變函數,類似於 Python 的關鍵字參數,但不那麼有用,因為參數需要是相同的類型。我發現關鍵字參數是我真正懷念的特性,這主要是你可以把任何類型的一個 kwarg 扔給任何需要它的函數,而無需重寫它的每一個調用,這讓重構簡單了許多。我在工作中經常使用這個特性,它為我節省了很多時間。由於沒有該特性,這使得我在處理是否應該基於命令行標誌創建差異圖像時顯得有些笨拙。
5、冗長:Go 有點冗長(儘管不是像 Java 那麼冗長)。這部分是因為其類型系統沒有泛型,但主要是因為語言本身很小,沒有提供很多特性(你只有一種循環結構可以使用!)。我懷念Python 的列表推導式(list comprehensions)和其他函數式編程特性。如果你熟悉Python,你一兩天就可以學完 Tour of Go ,然後你就瞭解了整個語言。
6、錯誤處理:Python 有異常,而 Go 在可能出錯的地方通過從函數返回元組 value, error 來傳播錯誤。Python 允許你在調用棧中的任何位置捕獲錯誤,而不需要你一次又一次地手動將錯誤傳遞回去。這又使得代碼簡潔,而不像 Go 那樣被其臭名昭著的 if err != nil 模式搞得雜亂無章,不過你需要弄清楚函數及其內部的所有調用可能拋出的異常(使用 except Exception: 通常是一種糟糕的變通方案)。良好的文檔註釋和測試可以提供幫助,你在使用其中任何一種語言編程時都應該添加。Go 的系統絕對更安全。如果你忽視了 err 值,你還是可以搬起石頭砸自己的腳,但該系統使糟糕的主意變得更明顯。
7、第三方模塊:在 Go 模塊出現之前,Go 的包管理器會把所有下載的包扔到 GOPATH/src目錄下,而不是項目的目錄(像大多數其他語言)。
GOPATH/src目錄下,而不是項目的目錄(像大多數其他語言)。GOPATH 下這些模塊的路徑也會從託管包的 URL 構建,所以你的 import 語句將是類似 import " github.com/someuser/somepackage " 這個樣子。在幾乎所有 Go 代碼庫的源代碼中嵌入 github.com 似乎是一個奇怪的選擇。在任何情況下,Go 允許以傳統的方式做事,但 Go 模塊仍然是個很新的特性,所以在一段時間內,在缺少管理的 Go 代碼庫中,這種奇怪的行為仍將很常見。
8、異步性:Goroutines 是啟動異步任務的一種非常方便的方法。在 async/await 之前,Python 的異步解決方案有點令人沮喪。遺憾的是,我沒有用 Python 或 Go 編寫很多真實的異步代碼,而 diffimg 的簡單性似乎並不適合說明異步性的額外開銷,所以我沒有太多要說的,雖然我確實喜歡使用 Go 的channels 來處理多個異步任務。我的理解是,對於性能,Go 仍然佔了上風了,因為 goroutine 可以充分利用多處理器併發,而 Python 的基本 async/await 仍侷限於一個處理器,所以主要用於 I / O 密集型任務。
9、調試:Python 勝出。pdb(以及像 ipdb 這樣更復雜的選項)非常靈活,一旦你進入 REPL,你可以編寫任何你想要的代碼。 Delve 是一個很好的調試器,但和直接放入解釋器不一樣,語言的全部功能都唾手可得。
Go 總結
我對 Go 最初的印象是,由於它的抽象能力(有意為之)有限,所以它不像 Python 那樣有趣。Python 有更多的特性,因此有更多的方法來做一些事情,找到最快、最易讀或“最聰明”的解決方案可能會很有趣。Go 會積極地阻止你變得“聰明”。我認為,Go 的優勢在於它並不聰明。
它的極簡主義和缺乏自由限制了單個開發人員實現一個想法。然而,當項目擴展到幾十個或幾百個開發人員時,這個弱點變成了它的力量——因為每個人都使用同樣的語言特性小工具集,更容易統一,因此可以被他人理解。使用 Go 仍有可能編寫糟糕的代碼,但與那些更“強大”的語言相比,它讓你更難創造怪物。
使用一段時間後,我理解了為什麼像谷歌這樣的公司想要一門這樣的語言。新工程師不斷地加入到大量代碼庫的開發,在更復雜 / 更強大的語言中,在最後期限的壓力下,引入複雜性的速度比它被消除的速度更快。防止這種情況發生的最好方法是使用一種更不容易產生複雜性的語言。
所以說,我很高興工作在一個大型應用程序上下文中的 Go 代碼庫上,有一個多樣化和不斷增長的團隊。事實上,我認為我更喜歡它。我只是不想把它用於我自己的個人項目。
二、進入 Rust
幾周前,我決定嘗試學習 Rust。我之前曾試圖這樣做,但發現類型系統和借用檢查(borrow checker)讓人困惑,沒有足夠的背景信息,我不知道為什麼要把所有這些限制強加給我,對於我想做的任務來說大而笨重。然而,自那時以來,我對程序執行時內存中發生了什麼有了更多的瞭解。我從這本書開始,而不是試圖一頭扎進去。這有很大的幫助,這份介紹比我見過的任何編程語言的介紹都要好。
在我讀過這本書的前十幾章之後,我覺得自己有足夠的信心嘗試 diffimg 的另一個實現(這時,我覺得我的 Rust 經驗與我寫 diffimg-go 時的 Go 經驗一樣多)。這比我用 Go 實現的時間要長,而後者比用 Python 需要的時間更長。我認為,即使考慮到我對 Python 更加熟悉,這也是對的——兩種語言都有更多的東西要寫。
在編寫 diffimg-rs 時,我注意到一些事情。
1、類型系統:現在,我已經習慣 Go 中更基本的靜態類型系統了,但 Rust 更強大(也更復雜)。有了 Go 中接口和結構的基礎(它們都簡單得多),泛型類型、枚舉類型、traits、引用類型、生命週期就是全部我要額外學習的概念了。此外,Rust 使用其類型系統實現其它語言不使用類型系統實現的特性(如 Result ,我很快會介紹)。幸運的是,編譯器 / 源碼解析器非常有幫助,它可以告訴你你做錯了什麼,甚至經常告訴你如何解決這個問題。儘管如此,這比我學習 Go 的類型系統所用的時間要多得多,我還沒有適應它的所有特性。
(1)有一個地方,因為類型系統,我正在使用的圖像庫的實現會導致令人不快的代碼重複量。我最終只要匹配兩個最重要的枚舉類型,但匹配其他類型會導致另外半打左右幾乎相同的代碼行。在這個規模上,這不是一個問題,但它使我生氣。也許這裡使用宏是個不錯的主意,我仍然需要試驗。
2、手動內存管理:Python 和 Go 會幫你撿垃圾。C 允許你亂丟垃圾,但當它踩到你丟的香蕉皮時,它會大發脾氣。Rust 會拍你一下,並要求你自己清理乾淨。這會刺痛我,甚至超過了從動態類型語言遷移到靜態類型語言,因為我被寵壞了,通常都是由語言跟在我後面撿。此外,編譯器會設法儘可能地幫助你,但你仍然需要大量的學習才能理解到底發生了什麼。
(1)直接訪問內存(以及 Rust 的函數式編程特性)的一個好處是,它簡化了差異比的計算,因為我可以簡單地映射原始字節數組,而不必按座標索引每個像素。
3、函數式特性:Rust 強烈鼓勵函數式的方法:它有 FP 友好的類型系統(類似 Haskell)、不可變類型、閉包,迭代器,模式匹配等,但也允許命令式代碼。它類似於編寫 OCaml(有趣的是,最初的 Rust 編譯器是用 OCaml 編寫的)。因此,對於這門與 C 競爭的語言,代碼比你想象得更簡潔。
4、錯誤處理:不同於 Python 使用的異常模型,也不同於 Go 異常處理時返回的元組,Rust 利用枚舉類型:Result 返回 Ok(value) 或 Err(error)。這和 Go 的方式更接近,但更明確一些,而且利用了類型系統。還有一個語法糖用於檢查語句中的 Err 並提前返回:? 操作符(在我看來,Go 可以用類似這樣的東西)。
5、異步性:Rust 的 async/await 還沒有完全準備好,但最終語法最近已經達成一致。Rust 標準庫中也有一些基本的線程特性,看上去比 Python 的更容易使用,但我沒有花太多的時間瞭解它。Go 似乎仍然有最好的特性。
6、工具:rustup 和 cargo 分別是非常優秀的語言版本管理器和包 / 模塊管理器實現。一切“正常”。我特別喜歡自動生成的文檔。對於這些工具,Python 提供的選項有點太簡單,需要小心對待,正如我之前提到的,Go 有一種奇怪的管理模塊方式,但除此之外,它的工具比 Python 的更好。
7、編輯器插件:我的. vimrc 文件大到令人尷尬,至少有三十多個插件。我有一些用於代碼檢查、自動補全以及格式化 Python 和 Go 的插件,但相比於其他兩種語言,Rust 插件更容易設置,更有用、更一致。 rust.vim 和 vim-lsp 插件(以及 Rust 語言服務器)是我獲得一個非常強大的配置所需要的全部。我還沒有測試其他編輯器,但是,藉助 Rust 提供的編輯器無關的優秀工具,我認為它們一樣有幫助。這個設置提供了我使用過的最好的“轉到定義”。它可以很好地適用於本地、標準庫和開箱即用的第三方代碼。
8、調試:我還沒有嘗試過 Rust 的調試器(因為其類型系統和 println! 已經讓我走得很遠),但是你可以使用 rust-gdb 和 rust-lldb,以及 rustup 初始安裝時帶有的 gdb 和 lldb 調試器的封裝器。如果你之前在編寫 C 語言代碼時使用過那些調試器,那麼其使用體驗將是意料之中的。正如前面提到的,編譯器的錯誤消息非常有幫助。
Rust 總結
你至少要讀過這本書的前幾章,否則我絕對不建議你嘗試編寫 Rust 代碼,即使你已經熟悉 C 和內存管理。對於 Go 和 Python,只要你有一些另一種現代命令式編程語言的經驗,它們就不是很難入手,必要時可以參考文檔。Rust 是一門很大的語言。Python 也有很多特性,但是它們大部分是可選的。只要理解一些基本的數據結構和一些內置函數,你就可以完成很多工作。對於 Rust,你需要真正理解類型系統的固有複雜性和借用檢查,否則你會搞得十分複雜。
就我寫 Rust 時的感覺而言,它非常有趣,就像 Python 一樣。它廣泛的特性使它很有表現力。雖然編譯器會經常讓你停下,但它也非常有用,而且它對於如何解決你的借用問題 / 輸入問題的建議通常是有效的。正如我所提到的,這些工具是我遇到的所有語言中最好的,並且不像我使用的其他一些語言那樣給我帶來很多麻煩。我非常喜歡使用這種語言,並將在 Python 的性能還不夠好的地方繼續尋找使用 Rust 的機會。
三、代碼示例
我提取了每個 diffimg 中計算差異比的代碼塊。為了概括介紹 Python 的做法,這需要 Pillow 生成的差異圖像,對所有通道的所有像素值求和,並返回最大可能值(相同大小的純白圖像)除以總和的比值。
Python
對於 Go 和 Rust,方法有點不同:我們不用創建一個差異圖像,我們只要遍歷兩幅輸入圖像,並對每個像素的差異求和。在 Go 中,我們用座標索引每幅圖像……
Go
……但在 Rust 中,我們將圖像視為它們真的是在內存中,是一系列可以壓縮到一起並消費的字節。
Rust
對於這些例子,有一些事情需要注意:
- Python 的代碼最少。顯然,這在很大程度上取決於使用的圖像庫,但這說明了使用 Python 的一般體驗。在許多情況下,那些庫為你做了很多工作,因為生態系統非常發達,任何東西都有成熟的解決方案。
- 在 Go 和 Rust 示例中有類型轉換。每個代碼塊中都使用了三個數值類型:用於像素通道值的 uint8/u8(Go 和 Rust 有類型推斷,所以你看不到任何明確提及的類型)、用於總和的 uint64/u64 和用於最終比值的 float64/f64。對於 Go 和 Rust,需要花時間統一類型,而 Python 將隱式地完成這一切。
- Go 實現的風格是命令式的,但也是明確和可以理解的(使其稍顯欠缺的是我前面提到的 ignoreAlpha),甚至是對那些不習慣該語言的人也是如此。Python 示例相當清晰,一旦你理解了 ImageStat 在做什麼。對於那些不熟悉這種語言的人來說,Rust 肯定更加難懂:
- .raw_pixels() 獲取圖像,生成 8 位無符號整數向量;
- .iter() 為該向量創建一個迭代器。默認情況下,向量是不可遍歷的;
- .zip() 你可能瞭解,它接受兩個迭代器,然後生成一個迭代器,每個元素是一個元組:(來自第一個向量的元素,來自第二個向量的元素);
- 在 diffsum 聲明中,我們需要一個 mut,因為變量默認是不可變的;
- 如果你熟悉 C,你就會明白為什麼 for (&p1, &p2) 中有 &:迭代器生成像素值的引用,但 abs_diff() 自己取得它們的值。Go 支持指針(和引用不太一樣),但它們不同於 Rust 中常用的引用。
- 函數中的最後一個語句用於在沒有行結束符 ; 的情況作為返回值。其他一些函數式語言也是這樣做的。
這段是為了讓你瞭解需要掌握多少特定於語言的知識才能有效地使用 Rust。
四、性能
現在來做一個科學的比較。我首先生成三張不同尺寸的隨機圖像:1x1、2000x2000、10000x10000。然後我測量每個(語言、圖像大小)組合的性能,每個 diffimg 計算 10 次,然後取平均值,使用 time 命令的 real 值給出的值。diffimg-rs 使用–release 構建,diffimg-go 使用 go build,而 Python diffimg 通過 python3 - m diffimg 調用。以下是在 2015 年的 Macbook Pro 上獲得的結果:
幾年前,我負責重寫一個圖像處理服務。為了弄清楚對於給定的圖像和一個或多個轉換(調整大小、圓形裁剪、修改格式等),我的新服務創建的輸出是否和舊服務一致,我必須自己檢查圖像。顯然,我需要自動化,但我找不到一個現有的 Python 庫可以告訴我,這兩張圖片在像素級上有什麼不同,因此有了diffimg ,它可以給你一個差異比 / 百分比,或生成差異圖像(檢出 readme,裡面有一個例子)。
我最初是用 Python 實現的(我最熟悉的語言),主要部分使用了 Pillow 。它可以用作庫或命令行工具。程序的基本部分非常小,只有幾十行,這要感謝Pillow。構建這個工具並不費力( xkcd 是對的,幾乎所有東西都有一個 Python 模塊),但它至少對於除我自己之外的幾十人是有用的。
幾個月前,我加入了一家公司,他們有幾個服務是用 Go 編寫的,我需要快速上手這門語言。編寫diffimg-go 看起來很有趣,甚至可能是一種有用的方法。這裡有一些來自經驗的興趣點,以及一些在工作中使用它的興趣點。
一、對比 Python 和 Go
(代碼: diffimg (Python)和 diffimg-go )
1、標準庫:Go 有一個相當不錯的 image 標準庫模塊,以及命令行 flag 解析庫。我不需要尋找任何外部依賴;diffimg-go 實現沒有依賴,而 Python 實現使用了相當重量級的第三方模塊(諷刺的是)Pillow。Go 的標準庫更有條理,而且經過深思熟慮,而 Python 的會逐步發展,它們是多年來由許多作者創建的,有許多不同的約定。Go 標準庫的一致性使開發者更易於預測任何給定的模塊將如何發揮作用,而且源代碼有非常好的文檔記錄。
(1)使用標準 image 庫的一個缺點是它不自動檢測圖像是否有一個 alpha 通道;所有圖像類型的像素值都有四個通道(RGBA)。因此,diffimg-go 實現要求用戶指明是否要使用 alpha 通道。這個小小的不便不值得找第三方庫來修復。
(2)一個很大的好處是,標準庫中有足夠的內容,你不需要像 Django 這樣的 Web 框架。用 Go 有可能在沒有任何依賴關係的情況下建立一個真正可用的 Web 服務。Python 號稱“自帶電池(batteries-included)”,但在我看來,Go 做得更好。
2、靜態類型系統:我過去使用靜態類型語言,但我過去幾年一直在使用 Python 編程。體驗起初有點煩人,感覺它好像只是減慢了我的速度,它迫使我過度明確,即使我偶爾錯了,Python 也會讓我做我想做的。有點像你給人發指令而對方總是打斷你讓你闡明你是什麼意思,而有的人則總是點頭,看上去已經理解你,但是你並不確定他們是否已經全部瞭解。它將大大減少與類型相關的 Bug,但是我發現,我仍然需要花幾乎相同的時間編寫測試。
(1)Go 的一個常見缺點是它沒有用戶可實現的泛型類型。雖然這不是一個構建大型可擴展應用程序的必備特性,但它肯定會減緩開發速度。雖然已經有替代模式建議,但是它們中沒有一個和真正的泛型類型一樣有效。
(2)靜態類型系統的一個優點是,可以更簡單快速地閱讀不熟悉的代碼庫。用好類型可以帶來許多動態類型系統中會丟失的額外信息。
3、接口和結構:Go 使用接口和結構,而 Python 使用類。在我看來,這可能是最有趣的區別,因為它迫使我區分定義行為的類型和保存信息的類型這兩個概念。Python 和其他傳統的面向對象的語言都鼓勵你將它們混在一起,但這兩種範式各有利弊:
(1)Go 強烈建議組合而不是繼承。雖然它通過嵌入繼承,不用類,但其數據和方法不是那麼容易傳遞。通常,我認為組合是更好的默認模式,但我不是一個絕對主義者,在某些情況下繼承更合適,所以我不喜歡語言幫我作出這個決定。
(2)接口實現的分離意味著,如果有許多類型彼此相似,你就需要多次編寫類似的代碼。由於缺少泛型類型,在 Go 中,有些情況下我無法重用代碼,不過在 Python 中可以。
(3)然而,由於 Go 是靜態類型的,當你編寫的代碼會導致運行時錯誤時,編譯器 / 源碼分析器(linter)會告訴你。Python 源碼分析器也可以有一點這樣的功能。但在 Python 中,當你試圖訪問一個可能不存在的方法或屬性時,由於語言的動態性,Python 源碼分析器無法確切地知道什麼方法 / 屬性存在,直到運行時。靜態定義的接口和結構是唯一在編譯時和開發過程中知道什麼可用的方法,這使得編譯時報錯的 Go 比運行時報錯的 Python 更可靠。
4、沒有可選參數:Go 只有可變函數,類似於 Python 的關鍵字參數,但不那麼有用,因為參數需要是相同的類型。我發現關鍵字參數是我真正懷念的特性,這主要是你可以把任何類型的一個 kwarg 扔給任何需要它的函數,而無需重寫它的每一個調用,這讓重構簡單了許多。我在工作中經常使用這個特性,它為我節省了很多時間。由於沒有該特性,這使得我在處理是否應該基於命令行標誌創建差異圖像時顯得有些笨拙。
5、冗長:Go 有點冗長(儘管不是像 Java 那麼冗長)。這部分是因為其類型系統沒有泛型,但主要是因為語言本身很小,沒有提供很多特性(你只有一種循環結構可以使用!)。我懷念Python 的列表推導式(list comprehensions)和其他函數式編程特性。如果你熟悉Python,你一兩天就可以學完 Tour of Go ,然後你就瞭解了整個語言。
6、錯誤處理:Python 有異常,而 Go 在可能出錯的地方通過從函數返回元組 value, error 來傳播錯誤。Python 允許你在調用棧中的任何位置捕獲錯誤,而不需要你一次又一次地手動將錯誤傳遞回去。這又使得代碼簡潔,而不像 Go 那樣被其臭名昭著的 if err != nil 模式搞得雜亂無章,不過你需要弄清楚函數及其內部的所有調用可能拋出的異常(使用 except Exception: 通常是一種糟糕的變通方案)。良好的文檔註釋和測試可以提供幫助,你在使用其中任何一種語言編程時都應該添加。Go 的系統絕對更安全。如果你忽視了 err 值,你還是可以搬起石頭砸自己的腳,但該系統使糟糕的主意變得更明顯。
7、第三方模塊:在 Go 模塊出現之前,Go 的包管理器會把所有下載的包扔到 GOPATH/src目錄下,而不是項目的目錄(像大多數其他語言)。
GOPATH/src目錄下,而不是項目的目錄(像大多數其他語言)。GOPATH 下這些模塊的路徑也會從託管包的 URL 構建,所以你的 import 語句將是類似 import " github.com/someuser/somepackage " 這個樣子。在幾乎所有 Go 代碼庫的源代碼中嵌入 github.com 似乎是一個奇怪的選擇。在任何情況下,Go 允許以傳統的方式做事,但 Go 模塊仍然是個很新的特性,所以在一段時間內,在缺少管理的 Go 代碼庫中,這種奇怪的行為仍將很常見。
8、異步性:Goroutines 是啟動異步任務的一種非常方便的方法。在 async/await 之前,Python 的異步解決方案有點令人沮喪。遺憾的是,我沒有用 Python 或 Go 編寫很多真實的異步代碼,而 diffimg 的簡單性似乎並不適合說明異步性的額外開銷,所以我沒有太多要說的,雖然我確實喜歡使用 Go 的channels 來處理多個異步任務。我的理解是,對於性能,Go 仍然佔了上風了,因為 goroutine 可以充分利用多處理器併發,而 Python 的基本 async/await 仍侷限於一個處理器,所以主要用於 I / O 密集型任務。
9、調試:Python 勝出。pdb(以及像 ipdb 這樣更復雜的選項)非常靈活,一旦你進入 REPL,你可以編寫任何你想要的代碼。 Delve 是一個很好的調試器,但和直接放入解釋器不一樣,語言的全部功能都唾手可得。
Go 總結
我對 Go 最初的印象是,由於它的抽象能力(有意為之)有限,所以它不像 Python 那樣有趣。Python 有更多的特性,因此有更多的方法來做一些事情,找到最快、最易讀或“最聰明”的解決方案可能會很有趣。Go 會積極地阻止你變得“聰明”。我認為,Go 的優勢在於它並不聰明。
它的極簡主義和缺乏自由限制了單個開發人員實現一個想法。然而,當項目擴展到幾十個或幾百個開發人員時,這個弱點變成了它的力量——因為每個人都使用同樣的語言特性小工具集,更容易統一,因此可以被他人理解。使用 Go 仍有可能編寫糟糕的代碼,但與那些更“強大”的語言相比,它讓你更難創造怪物。
使用一段時間後,我理解了為什麼像谷歌這樣的公司想要一門這樣的語言。新工程師不斷地加入到大量代碼庫的開發,在更復雜 / 更強大的語言中,在最後期限的壓力下,引入複雜性的速度比它被消除的速度更快。防止這種情況發生的最好方法是使用一種更不容易產生複雜性的語言。
所以說,我很高興工作在一個大型應用程序上下文中的 Go 代碼庫上,有一個多樣化和不斷增長的團隊。事實上,我認為我更喜歡它。我只是不想把它用於我自己的個人項目。
二、進入 Rust
幾周前,我決定嘗試學習 Rust。我之前曾試圖這樣做,但發現類型系統和借用檢查(borrow checker)讓人困惑,沒有足夠的背景信息,我不知道為什麼要把所有這些限制強加給我,對於我想做的任務來說大而笨重。然而,自那時以來,我對程序執行時內存中發生了什麼有了更多的瞭解。我從這本書開始,而不是試圖一頭扎進去。這有很大的幫助,這份介紹比我見過的任何編程語言的介紹都要好。
在我讀過這本書的前十幾章之後,我覺得自己有足夠的信心嘗試 diffimg 的另一個實現(這時,我覺得我的 Rust 經驗與我寫 diffimg-go 時的 Go 經驗一樣多)。這比我用 Go 實現的時間要長,而後者比用 Python 需要的時間更長。我認為,即使考慮到我對 Python 更加熟悉,這也是對的——兩種語言都有更多的東西要寫。
在編寫 diffimg-rs 時,我注意到一些事情。
1、類型系統:現在,我已經習慣 Go 中更基本的靜態類型系統了,但 Rust 更強大(也更復雜)。有了 Go 中接口和結構的基礎(它們都簡單得多),泛型類型、枚舉類型、traits、引用類型、生命週期就是全部我要額外學習的概念了。此外,Rust 使用其類型系統實現其它語言不使用類型系統實現的特性(如 Result ,我很快會介紹)。幸運的是,編譯器 / 源碼解析器非常有幫助,它可以告訴你你做錯了什麼,甚至經常告訴你如何解決這個問題。儘管如此,這比我學習 Go 的類型系統所用的時間要多得多,我還沒有適應它的所有特性。
(1)有一個地方,因為類型系統,我正在使用的圖像庫的實現會導致令人不快的代碼重複量。我最終只要匹配兩個最重要的枚舉類型,但匹配其他類型會導致另外半打左右幾乎相同的代碼行。在這個規模上,這不是一個問題,但它使我生氣。也許這裡使用宏是個不錯的主意,我仍然需要試驗。
2、手動內存管理:Python 和 Go 會幫你撿垃圾。C 允許你亂丟垃圾,但當它踩到你丟的香蕉皮時,它會大發脾氣。Rust 會拍你一下,並要求你自己清理乾淨。這會刺痛我,甚至超過了從動態類型語言遷移到靜態類型語言,因為我被寵壞了,通常都是由語言跟在我後面撿。此外,編譯器會設法儘可能地幫助你,但你仍然需要大量的學習才能理解到底發生了什麼。
(1)直接訪問內存(以及 Rust 的函數式編程特性)的一個好處是,它簡化了差異比的計算,因為我可以簡單地映射原始字節數組,而不必按座標索引每個像素。
3、函數式特性:Rust 強烈鼓勵函數式的方法:它有 FP 友好的類型系統(類似 Haskell)、不可變類型、閉包,迭代器,模式匹配等,但也允許命令式代碼。它類似於編寫 OCaml(有趣的是,最初的 Rust 編譯器是用 OCaml 編寫的)。因此,對於這門與 C 競爭的語言,代碼比你想象得更簡潔。
4、錯誤處理:不同於 Python 使用的異常模型,也不同於 Go 異常處理時返回的元組,Rust 利用枚舉類型:Result 返回 Ok(value) 或 Err(error)。這和 Go 的方式更接近,但更明確一些,而且利用了類型系統。還有一個語法糖用於檢查語句中的 Err 並提前返回:? 操作符(在我看來,Go 可以用類似這樣的東西)。
5、異步性:Rust 的 async/await 還沒有完全準備好,但最終語法最近已經達成一致。Rust 標準庫中也有一些基本的線程特性,看上去比 Python 的更容易使用,但我沒有花太多的時間瞭解它。Go 似乎仍然有最好的特性。
6、工具:rustup 和 cargo 分別是非常優秀的語言版本管理器和包 / 模塊管理器實現。一切“正常”。我特別喜歡自動生成的文檔。對於這些工具,Python 提供的選項有點太簡單,需要小心對待,正如我之前提到的,Go 有一種奇怪的管理模塊方式,但除此之外,它的工具比 Python 的更好。
7、編輯器插件:我的. vimrc 文件大到令人尷尬,至少有三十多個插件。我有一些用於代碼檢查、自動補全以及格式化 Python 和 Go 的插件,但相比於其他兩種語言,Rust 插件更容易設置,更有用、更一致。 rust.vim 和 vim-lsp 插件(以及 Rust 語言服務器)是我獲得一個非常強大的配置所需要的全部。我還沒有測試其他編輯器,但是,藉助 Rust 提供的編輯器無關的優秀工具,我認為它們一樣有幫助。這個設置提供了我使用過的最好的“轉到定義”。它可以很好地適用於本地、標準庫和開箱即用的第三方代碼。
8、調試:我還沒有嘗試過 Rust 的調試器(因為其類型系統和 println! 已經讓我走得很遠),但是你可以使用 rust-gdb 和 rust-lldb,以及 rustup 初始安裝時帶有的 gdb 和 lldb 調試器的封裝器。如果你之前在編寫 C 語言代碼時使用過那些調試器,那麼其使用體驗將是意料之中的。正如前面提到的,編譯器的錯誤消息非常有幫助。
Rust 總結
你至少要讀過這本書的前幾章,否則我絕對不建議你嘗試編寫 Rust 代碼,即使你已經熟悉 C 和內存管理。對於 Go 和 Python,只要你有一些另一種現代命令式編程語言的經驗,它們就不是很難入手,必要時可以參考文檔。Rust 是一門很大的語言。Python 也有很多特性,但是它們大部分是可選的。只要理解一些基本的數據結構和一些內置函數,你就可以完成很多工作。對於 Rust,你需要真正理解類型系統的固有複雜性和借用檢查,否則你會搞得十分複雜。
就我寫 Rust 時的感覺而言,它非常有趣,就像 Python 一樣。它廣泛的特性使它很有表現力。雖然編譯器會經常讓你停下,但它也非常有用,而且它對於如何解決你的借用問題 / 輸入問題的建議通常是有效的。正如我所提到的,這些工具是我遇到的所有語言中最好的,並且不像我使用的其他一些語言那樣給我帶來很多麻煩。我非常喜歡使用這種語言,並將在 Python 的性能還不夠好的地方繼續尋找使用 Rust 的機會。
三、代碼示例
我提取了每個 diffimg 中計算差異比的代碼塊。為了概括介紹 Python 的做法,這需要 Pillow 生成的差異圖像,對所有通道的所有像素值求和,並返回最大可能值(相同大小的純白圖像)除以總和的比值。
Python
對於 Go 和 Rust,方法有點不同:我們不用創建一個差異圖像,我們只要遍歷兩幅輸入圖像,並對每個像素的差異求和。在 Go 中,我們用座標索引每幅圖像……
Go
……但在 Rust 中,我們將圖像視為它們真的是在內存中,是一系列可以壓縮到一起並消費的字節。
Rust
對於這些例子,有一些事情需要注意:
- Python 的代碼最少。顯然,這在很大程度上取決於使用的圖像庫,但這說明了使用 Python 的一般體驗。在許多情況下,那些庫為你做了很多工作,因為生態系統非常發達,任何東西都有成熟的解決方案。
- 在 Go 和 Rust 示例中有類型轉換。每個代碼塊中都使用了三個數值類型:用於像素通道值的 uint8/u8(Go 和 Rust 有類型推斷,所以你看不到任何明確提及的類型)、用於總和的 uint64/u64 和用於最終比值的 float64/f64。對於 Go 和 Rust,需要花時間統一類型,而 Python 將隱式地完成這一切。
- Go 實現的風格是命令式的,但也是明確和可以理解的(使其稍顯欠缺的是我前面提到的 ignoreAlpha),甚至是對那些不習慣該語言的人也是如此。Python 示例相當清晰,一旦你理解了 ImageStat 在做什麼。對於那些不熟悉這種語言的人來說,Rust 肯定更加難懂:
- .raw_pixels() 獲取圖像,生成 8 位無符號整數向量;
- .iter() 為該向量創建一個迭代器。默認情況下,向量是不可遍歷的;
- .zip() 你可能瞭解,它接受兩個迭代器,然後生成一個迭代器,每個元素是一個元組:(來自第一個向量的元素,來自第二個向量的元素);
- 在 diffsum 聲明中,我們需要一個 mut,因為變量默認是不可變的;
- 如果你熟悉 C,你就會明白為什麼 for (&p1, &p2) 中有 &:迭代器生成像素值的引用,但 abs_diff() 自己取得它們的值。Go 支持指針(和引用不太一樣),但它們不同於 Rust 中常用的引用。
- 函數中的最後一個語句用於在沒有行結束符 ; 的情況作為返回值。其他一些函數式語言也是這樣做的。
這段是為了讓你瞭解需要掌握多少特定於語言的知識才能有效地使用 Rust。
四、性能
現在來做一個科學的比較。我首先生成三張不同尺寸的隨機圖像:1x1、2000x2000、10000x10000。然後我測量每個(語言、圖像大小)組合的性能,每個 diffimg 計算 10 次,然後取平均值,使用 time 命令的 real 值給出的值。diffimg-rs 使用–release 構建,diffimg-go 使用 go build,而 Python diffimg 通過 python3 - m diffimg 調用。以下是在 2015 年的 Macbook Pro 上獲得的結果:
我損失了很多精度,因為 time 只精確到 10ms(因為計算平均值的緣故,這裡多顯示了一個數字)。該任務只需要一個非常特定類型的計算,所以不同的或更復雜的任務得出的數值可能差別很大。話雖如此,我們還是可以從這些數據中瞭解到一些東西。
對於 1x1 的圖像,幾乎所有的時間都花費在設置中,沒有比例計算。Rust 獲勝,儘管它使用了兩個第三方庫( clap 和 image ),Go 只使用了標準庫。我並不驚訝 Python 的啟動那麼緩慢,因為導入大庫 Pillow 是它的一個步驟,time python -c ’ '其實只用了 0.030 秒。
對於 2000x2000 的圖像,Go 和 Python 與 Rust 的差距就縮小了,這大概是因為與計算相比,用於設置的時間更少。然而,對於 10000 年 x10000 的圖像,Rust 相比較之下性能更好,我想這是由於其編譯器優化所生成的機器代碼最小(循環 1 億次),設置時間相對就比較少了。從不需要暫停進行垃圾收集也是一個因素。
Python 實現肯定還有很大的改進餘地,因為像 Pillow 那麼高效,我們仍然是在內存中創建一張差異圖像(遍歷輸入圖像),然後累加每個像素的通道值。更直接的方法,比如 Go 和 Rust 實現,可能會稍微快一些。然而,純 Python 實現會非常慢,因為 Pillow 主要是用 C 完成其工作。因為另外兩個是純粹用一種語言實現的,這不是一個真正公平的比較,雖然在某些方面是,因為得益於 C 擴展(Python 和 C 的關係一般都非常緊密 ),Python 有一大堆高性能庫可以使用。
我還應該提下二進制文件的大小:Rust 的 2.1MB,使用–release 構建,Go 的大小差不多,為 2.5 MB。Python 不創建二進制文件,但是.pyc 文件有一定的可比性,和 diffimg 的.pyc 文件總共約 3 KB。它的源代碼也只有 3KB,但是包括 Pillow 依賴的話,它將達 24MB。再說一次,這不是一個公平的比較,因為我使用了一個第三方圖像庫,但應該提一下。
五、結論
顯然,這三種截然不同的語言實現滿足不同的細分市場需求。我經常聽到 Go 和 Rust 被一起提及,但我認為,Go 和 Python 是兩種類似 / 存在競爭關係的語言。它們都很適合編寫服務器端應用程序邏輯(我在工作中大部分時間都在做這項工作)。僅比較原生代碼的性能,Go 完勝 Python,但許多有速度要求的 Python 庫是對速度更快的 C 實現的封裝——實際情況比這種天真的比較更復雜。編寫一個用於 Python 的 C 擴展不能完全算是 Python 了(你需要了解 C),但這個選項是對你開放的。
對於你的後端服務器需求,Python 已被證明它對於大多數應用程序都“足夠快”,但是如果你需要更好的性能,Go 可以,Rust 更是如此,但是你要付出更多的開發時間。Go 在這方面並沒有超出 Python 很多,雖然開發肯定是慢一些,這主要是由於其較小的特性集。Rust 的特性非常齊全,但管理內存總是比由語言自己管理會花費更多的時間,這好過處理 Go 的極簡性。
還應該提一下,世界上有很多很多 Python 開發人員,有些有幾十年的經驗。如果你選擇 Python,那麼找到更多有語言經驗的人加入到你的後端團隊中可能並不難。然而,Go 開發人員並不是特別少,而且很容易發展,因為這門語言很容易學習。由於 Rust 這種語言需要更長的時間內化,所以開發人員更少,也更難發展。
至於系統類型:靜態類型系統更容易編寫更多正確的代碼,但它不是萬能的。無論使用何種語言,你仍然需要編寫綜合測試。它需要更多的訓練,但是我發現,我使用 Python 編寫的代碼並不一定比 Go 更容易出錯,只要我能夠編寫一個好的測試套件。儘管如此,相比於 Go,我更喜歡 Rust 的類型系統:它支持泛型、模式匹配、錯誤處理,它通常為你做得更多。
最後,這種比較有點愚蠢,因為儘管這些語言的用例重疊,但它們佔領著不同的細分市場。Python 開發速度快、性能低,而 Rust 恰恰相反,Go 則介於兩者之間。我喜歡 Python 和 Rust 超過 Go(這可能令人奇怪),不過我會繼續在工作中愉快地使用 Go(以及 Python),因為它真的是一種構建穩定、可維護的應用程序的偉大語言,它有許多來自不同背景的貢獻者。它的僵硬和極簡主義使它使用起來不那麼令人愉快(對我來說),但這也正是它的力量所在。如果我要為一個新的 Web 應用程序選擇後端語言的話,那將是 Go。
我對這三種語言所涵蓋的編程任務範圍相當滿意——實際上,沒有哪個項目不能把它們中的一種視為很好的選擇。
查看英文原文:One Program Written in Python, Go, and Rust