您好,登錄后才能下訂單哦!
這篇文章主要講解了“怎么將Java代碼移植到Go中”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“怎么將Java代碼移植到Go中”吧!
測試,代碼覆蓋率
自動化測試和代碼覆蓋率追蹤,可以讓大型項目獲益匪淺。
我使用 TravisCI 和 AppVeyor 進行測試。Codecov.io 用來檢測代碼覆蓋率。還有許多其他的類似服務。
我同時使用 AppVeyor 和 TravisCI,是因為 Travis 在一年前不再支持 Windows,而 AppVeyor 不支持 Linux。
如果現在讓我重新選擇這些工具,我將只使用 AppVeyor,因為它現在支持 Linux 和 Windows 平臺的測試,而 TravisCI 在被私募股權公司收購并炒掉原始開發團隊后,前景并不明朗。
Codecov 幾乎無法勝任代碼覆蓋率檢測。對于 Go,它將非代碼的行(比如注釋)當做是未執行的代碼。使用這個工具不可能得到 100% 的代碼覆蓋率。Coveralls 看起來也有同樣的問題。
聊勝于無,但這些工具可以讓情況變得更好,尤其是對 Go 程序而言。
Go 的競態檢測非常棒
一部分代碼使用了并發,而并發很容易出錯。
Go 提供了競態檢測器,在編譯時使用 -race 字段可以開啟它。
它會讓程序變慢,但額外的檢查可以探測是否在同時修改同一個內存位置。
我一直開啟 -race 運行測試,通過它的報警,我可以很快地修復那些競爭問題。
構建用于測試的特定工具
大型項目很難通過肉眼檢查驗證正確性。代碼太多,你的大腦很難一次記住。
當測試失敗時,僅從測試失敗的信息中找到原因也是一個挑戰。
數據庫客戶端驅動與 RavenDB 數據庫服務端使用 HTTP 協議連接,傳輸的命令和響應的結果使用 JSON 編碼。
當把 Java 測試代碼移植到 Go 時,如果可以獲取 Java 客戶端與服務端的 HTTP 流量,并與移植到 Go 的代碼生成的 HTTP 流量對比,這個信息將非常有用。
我構建了一些特定的工具,幫我完成這些工作。
為了獲取 Java 客戶端的 HTTP 流量,我使用 Go 構建了一個 logging HTTP 代理,Java 客戶端使用這個代理與服務端交互。
對于 Go 客戶端,我構建了一個可以攔截 HTTP 請求的鉤子。我使用它把流量記錄在文件中。
然后我就可以對比 Java 客戶端與 Go 移植的客戶端生成的 HTTP 流量的區別了。
移植的過程
你不能隨機開始遷移 5 萬行代碼。我確信,如果每一個小步驟之后不進行測試和驗證的話,我都會被整體代碼的復雜性給打敗。
對于 RavenDB 和 Java 代碼庫,我是新手。所以我的***步是深入理解這份 Java 代碼的工作原理。
客戶端的核心是與服務端通過 HTTP 協議交互。我捕獲并研究了流量,編寫最簡單的與服務器交互的 Go 代碼。
當這么做有效果之后,我自信可以復制這些功能。
我的***個里程碑是移植足夠的代碼,可以通過移植最簡單的 Java 測試代碼的測試。
我使用了自底向上和自上到下結合的方法。
自底向上的部分是指,我定位并移植那些用于向服務器發送命令和解析響應的調用鏈底層的代碼。
自上到下的部分是指,我逐步跟蹤要移植的測試代碼,來確定需要移植實現的功能代碼部分。
在成功完成***步移植后,剩下的工作就是一次移植一個測試,同時移植可通過這個測試的所有需要的代碼。
當測試移植并測試通過后,我做了一些讓代碼更加 Go 風格的改進。
我相信這種一步一步漸進的方法,對于完成移植工作是很重要的。
從心理學角度來看,在面對一個長年累月的項目時,設置簡短的中間態里程碑是很重要的。不斷的完成這些里程碑讓我干勁十足。
一直讓代碼保持可編譯、可運行和可通過測試的狀態也很好。當最終要面對那些日積月累的缺陷時,你將很難下手解決。
移植 Java 到 Go 的挑戰
移植的目標是要盡可能與 Java 代碼庫一致,因為移植的代碼需要與 Java 未來的變化保持同步。
有時我吃驚于自己以一行一行的方式移植的代碼量。而移植過程中,最耗費時間的部分是顛倒變量的聲明順序,Java 的聲明順序是 type name ,而 Go 的聲明順序是 name type 。我真心希望有工具可以幫我完成這部分工作。
String vs. string
在 Java 中, String 是一個本質上是引用(指針)的對象。因此,字符串可以為 null 。
在 Go 中 string 是一個值類型。它不可能是 nil ,僅僅為空。
這并不是什么大問題,大多情況下我可以無腦地將 null 替換為 "" 。
Errors vs. exceptions
Java 使用異常來傳遞錯誤。
Go 返回 error 接口的值。
移植不難,但需要修改大量的函數簽名,來支持返回錯誤值并在調用棧上傳播。
泛型
Go (目前)并不支持泛型。
移植泛型的接口是***的挑戰。
下面是 Java 中一個泛型方法的例子:
public <T> T load(Class<T> clazz, String id) {
調用者:
Foo foo = load(Foo.class, "id")
在 Go 中,我使用兩種策略。
其中之一是使用 interface{} ,它由值和類型組成,與 Java 中的 object 類似。不推薦使用這種方法。雖然有效,但對于這個庫的用戶而言,操作 interface{} 并不恰當。
在一些情況下我可以使用反射,上面的代碼可以移植為:
func Load(result interface{}, id string) error
我可以使用反射來獲取 result 的類型,再從 JSON 文檔中創建這個類型的值。
調用方的代碼:
var result *Foo err := Load(&result, "id")
函數重載
Go 不支持(很大可能永遠不會支持)函數重載。
我不確定我是否找到了正確的方式來移植這種代碼。
在一些情況下,重載用于創建更簡短的幫助函數:
void foo(int a, String b) {} void foo(int a) { foo(a, null); }
有時我會直接丟掉更簡短的幫助函數。
有時我會寫兩個函數:
func foo(a int) {} func fooWithB(a int, b string) {}
當潛在的參數數量很大時,有時我會這么做:
type FooArgs struct { A int B string } func foo(args *FooArgs) { }
繼承
Go 并不是面向對象語言,沒有繼承。
簡單情況下的繼承可以使用嵌套的方法移植。
class B : A { }
有時可以移植為:
type A struct { } type B struct { A }
我們把 A 嵌入到 B 中,因此 B 繼承了 A 所有的方法和字段。
這種方法對于虛函數無效。
并沒有好方法移植那些使用虛函數的代碼。
模擬虛函數的一個方式是將結構體和函數指針嵌套。這本質上來說,是重新實現了 Java 免費提供的,作為 object 實現一部分的虛表。
另一種方式是寫一個獨立的函數,通過類型判斷來調度給定類型的正確函數。
接口
Java 和 Go 都有接口,但它們是不一樣的內容,就像蘋果和意大利香腸的區別一樣。
在很少的情況下,我確實會創建 Go 的接口類型來復制 Java 接口。
大多數情況下,我放棄使用接口,而是在 API 中暴露具體的結構體。
依賴包的循環引入
Java 允許包的循環引入。
Go 不允許。
結果就是,我無法在移植中復制 Java 代碼的包結構。
為了簡化,我使用一個包。這種方法不太理想,因為這個包***會變得很臃腫。實際上,這個包臃腫到在 Windows 下 Go 1.10 無法處理單個包內的那么多源文件。幸運的是,Go 1.11 修復了這個問題。
私有(private)、公開(public)、保護(protected)
Go 的設計師們被低估了。他們簡化概念的能力是***的,權限控制就是其中的一個例子。
其他語言傾向于細粒度的權限控制:(每個類的字段和方法)指定最小可能粒度的公開、私有和保護。
結果就是當外部代碼使用這個庫時,這個庫實現的一些功能和這個庫中其他的類有一樣的訪問權限。
Go 簡化了這個概念,只擁有公開和私有,訪問的范圍限制在包的級別。
這更合理一些。
當我想要寫一個庫,比如說,解析 markdown,我不想把內部實現暴漏給這個庫的使用者。但對于我自己隱藏這些內部實現,效果恰恰相反。
Java 開發者注意到這個問題,有時會使用接口作為修復過度暴漏的類的技巧。通過返回一個接口,而不是具體的類,這個類的使用者就無法看到一些可用的公開接口。
并發
簡單來說,Go 的并發是***的,內建的競態檢測器非常有助于解決并發的問題。
我剛才說過,我進行的***個移植是模擬 Java 接口。比如,我實現了 Java CompletableFuture 類的復制。
只有在代碼可以運行后,我才會重新組織代碼,讓代碼更加符合 Go 的風格。
流暢的函數鏈式調用
RavenDB 擁有復雜的查詢能力。Java 客戶端使用鏈式方法構建查詢:
List<ReduceResult> results = session.query(User.class) .groupBy("name") .selectKey() .selectCount() .orderByDescending("count") .ofType(ReduceResult.class) .toList();
鏈式調用僅在通過異常進行錯誤交互的語言中有效。當一個函數額外返回一個錯誤,就沒法向上面那樣進行鏈式調用。
為了在 Go 中復制鏈式調用,我使用了一個“狀態錯誤(stateful error)”的方法:
type Query struct { err error } func (q *Query) WhereEquals(field string, val interface{}) *Query { if q.err != nil { return q } // logic that might set q.err return q } func (q *Query) GroupBy(field string) *Query { if q.err != nil { return q } // logic that might set q.err return q } func (q *Query) Execute(result inteface{}) error { if q.err != nil { return q.err } // do logic }
鏈式調用可以這么寫:
var result *Foo err := NewQuery().WhereEquals("Name", "Frank").GroupBy("Age").Execute(&result)
JSON 解析
Java 沒有內建的 JSON 解析函數,客戶端使用 Jackson JSON 庫。
Go 在標準庫中有 JSON 的支持,但它沒有提供足夠多的鉤子函數來展現 JSON 解析的過程。
我并沒有嘗試匹配所有的 Java 功能,因為 Go 內置的 JSON 支持看起來已經足夠靈活。
Go 代碼更短
簡短不是 Java 的屬性,而是寫出符合語言習慣代碼的文化的屬性。
在 Java 中,setter 和 getter 方法很常見。比如,Java 代碼:
class Foo { private int bar; public void setBar(int bar) { this.bar = bar; } public int getBar() { return this.bar; } }
Go 語言版本如下:
type Foo struct { Bar int }
3 行 vs 11 行。當你有大量的類,類內有很多成員時,這么做可以不斷累加這些類。
大部分其他的代碼***長度基本差不多。
使用 Notion 來組織工作
我是 Notion.so 的重度用戶。用最簡單的話來說,Notion 是一個多級筆記記錄應用。可以把它看做是 Evernote 和 wiki 的結合,是由***軟件設計師精心設計和實現的。
下面是我使用 Notion 組織 Go 移植工作的方式:
下面是具體的內容:
我有一個沒有在上面展示的帶日歷視圖的頁面,用來記錄在特定時間的工作內容和花費時間的簡短筆記。因為這次合約是按小時收費,所以工作時長的統計是很重要的信息。感謝這些筆記,我知道我在 11 個月里在這次開發上花費了 601 個小時。
客戶喜歡了解進展。我有一個頁面,記錄了每月的工作總結,如下所示:
這些頁面與客戶共享。
當開始每天的工作時,短期的 todo list 很有用。
我甚至用 Notion 頁面管理發票,使用“導出為 PDF”功能來生成發票的 PDF 版本。
待招聘的 Go 程序員
你的公司還需要 Go 開發者嗎?你可以雇用我
額外的資源
針對問題,我提供了一些額外的說明:
Hacker News discussion /r/golang discussion
感謝各位的閱讀,以上就是“怎么將Java代碼移植到Go中”的內容了,經過本文的學習后,相信大家對怎么將Java代碼移植到Go中這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。