成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

Golang 中的 ORM 編程雜談

開發
對我們來說,我們的數據庫不僅僅是我們讀取和寫入的信息容器。這是我們思想的表達;我們如何簡化和模擬行業挑戰的復雜性。刪除 ORM 將所有這些細節置于人們腦海中的前端中心,創造了對數據模型和 Postgres 的更多所有權,減少了“越界”的心態。這也許是最好的收獲。

我們最近決定在Bridge Financial Technology放棄我們的對象關系映射器 (ORM) ,并且一直很喜歡這個決定。放棄你的 ORM 需要挑戰和承諾,但利潤是真實的。我將介紹:

  • 背景:描述軟件開發中所謂的越南問題,理解和分類映射器。
  • 我們的旅程:我們轉型的原因和方式以及經驗教訓。
  • 解決的問題和收獲:性能、可擴展性、可維護性和更多云原生代碼的改進以及以數據為中心的透明文化。

越南問題的背景與思考

在2010 年左右的DjangoCon上,有人告訴我使用 ORM 就像美國在越南開戰一樣。該評論指的是Ted Neward在他 2006 年題為“計算機科學的越南”的博文中創造了這個詞并將他的比較形式化。

這種比較引起了我的共鳴,但我從未考慮過放棄 ORM。從類比中吸取了完全錯誤的教訓,我花了幾年時間尋找完美的 ORM,無論是通過我自己的決策還是其他人的決策,我都接觸到了名副其實的環法自行車賽:Python、Active Record、Linq 中的 Django 和 SQLAlchemy, Hibernate、實體框架,以及最近在 Bridge Financial Technology 的 Golang 支持的后端中的 Gorm。

當我們在軟件開發中發現痛苦時,傾其所有是很重要的。讓它痛到你無法忍受為止。疼痛越嚴重,您就越能更好地描述和確定疼痛的來源。我們的痛苦讓我重新評估了所謂的越南問題,并質疑 ORM 是否有意義。在對面向對象和關系世界之間的不匹配進行全面分析之前,我不會去。為此,我建議閱讀 Ted Neward 的永恒帖子。但我將總結重要的要點。

越南:對象-關系不匹配

我首先以大多數歷史學家今天所看到的方式過度簡化了越南戰爭。越南人正在打一場內戰以統一他們的國家。美國與蘇聯和共產主義本身進行了代理人戰爭,以防止其蔓延。也就是說,越南人將其視為南北問題,而美國則將其視為東/西問題。

整件事都不值得。越南今天是一個統一的共產主義國家(越共得到了它想要的東西),而沒有目睹共產主義學說在亞洲以外的中國傳播,中國于 1949 年成為共產主義國家(美國得到了它想要的東西)。然而,它跨越了近 15 年,3 屆總統政府和超過 100 萬人傷亡(橫跨美國、越南北部和南部、雙方的盟友以及軍事和文職人員)。從表面上看,更好的策略是完全脫離接觸。

轉向軟件開發:對象和關系同樣是根本不同的東西。他們來自不同的地方,有著不同的目標,沒有錯也沒有壞處。當然,技術可以在這些世界中進行堆棧排名,使用您的團隊知道、尊重和信任的堆棧,您將獲得更多里程。對我們來說,我們相信 Postgres 是關系數據庫(比 MySQL 或 SQL Server 更好)的同類最佳實現。同樣,我們喜歡并欣賞 Go,特別是它沒有通過繼承實現面向對象的設計目標。如果您在數據庫或編程語言方面遇到困難,我建議您先解決該問題,然后再解決交叉路口的問題,或者至少是我們的軌跡,我們對結果感到滿意。充實下面的類比是對這些“方面”的概述:

面向對象原則

面向對象的系統旨在提供:

  • 身份管理:將狀態的等價性與對象本身分開。當兩個對象的值相同時發生對象等價,而當兩個對象相同時發生身份等價,也就是說,它們都指向內存中的相同位置。
  • 狀態管理:將幾個原語關聯成一個更大的捆綁包的能力,該捆綁包代表有關世界或問題的某些東西。
  • 行為:操作所述狀態的操作集合。
  • 封裝:將對象的簡化的、導出的表面區域定義到系統的其余部分的能力,從而隱藏不必要的細節。
  • 多態性:同質處理不同對象的能力,盡管它們是不同的東西,但它們可以以相似的方式做出反應。

大多數編程語言通過繼承模型來實現這些原則。但值得注意的是,許多非常成功的語言確實是沒有繼承的面向對象的:特別是 Go、Erlang 和 Rust。

關系原則

關系存儲引擎旨在規范化數據并記錄世界的事實。SQL 提供操作以與圍繞集合論建立的數據進行交互,以確保數學正確性并在數據可變性期間實現屬性;即ACID 合規性。在事務中執行的 SQL 操作是原子的、一致的、隔離的和持久的。規范化通常是通過適當的設計來實現的,黃金標準是第三正常或 Boyce-Codd 形式。

(一些)差異

這些是完全不同的系統。他們的一些區別包括:

  • 對象對聚合狀態有意義,而關系試圖將其分割成多個表。
  • 對象世界中的數據可變性問題圍繞并發系統中的意外覆蓋問題,并通過封裝進行保護。關系世界中的可變性需要交易來保證狀態改變時的正確性。在數據被持久化之前,不太需要在改變對象狀態時獲得正確性。
  • 存在對象集合以實現跨這些對象的行為,而存在關系集合以建立關于世界的事實。

概念問題

以下是由這些差異引起的問題列表。

映射問題。由于以下問題,使用任何映射器都很難將表映射到對象:

  • 對象關系將通過將一個對象與另一個對象組合來表示。但是,相關對象依賴JOIN于 SQL 中的隱式操作,否則相關對象將不會被數據初始化。
  • 關系引擎中的多對多表涉及 3 個表:兩個數據源和第三個連接表。然而,這種關系只需要兩個對象以及另一個對象的列表。

如果您的編程語言支持繼承,那么以這種方式對對象建模可能很誘人,但數據庫中沒有 IS-A 類型的關系。

誰/什么擁有架構定義?編程語言,以及應用程序開發人員,還是數據庫的 DDL,以及 DBA?即使您在組織中沒有區分這些角色,您仍然會遇到這個問題,即所謂的“O”受制于“R”,反之亦然。

元數據去哪兒了?大多數字段自然會有 1:1 的對應關系。例如,整數、布爾值等將在數據庫和您的編程語言中具有眾所周知的類型。但是枚舉呢?很可能這些是帶有有限數量選項的字符串或整數,并且數據庫和編程語言都可以強制執行約束。那么你把這些元數據放在哪里呢?應用程序?數據庫?兩個都?

所有這些問題都需要通過將一方(對象或關系)聲明為權威來解決。這導致了 ORM 的分類——它對這個權威有什么看法?對象還是關系?

理解和分類 ORM

實際上,您不能簡單地“退出”并擺脫問題。這篇文章的標題有點營銷動機。畢竟,您不會將對象持久保存到平面文件中并將其稱為“數據庫”,也不會使用 SQL 構建應用程序。這些事情必須在某個時候相遇。但我認為,許多 ORM 采用的通用方法是有限的,主要是出于方便,通常以犧牲一方為代價。

從廣義上講,有兩種類型的 ORM:代碼優先和數據庫優先。

  • 在代碼優先方法中,您將對象模型定義定義為將映射到數據庫實體(一個或多個表)的類或類型。這種方法即時生成 SQL 并大量使用反射。Go 社區中的一個例子是gorm。
  • 數據庫優先方法通常依賴于從您的數據庫定義語言 (DDL) 生成對象定義代碼。Golang 示例是SQLBoiler。

您可能會對其中之一產生直觀和直覺的反應。這種觀點可以頑固地持有。就個人而言,我總是優先考慮對象而不是關系,并使用代碼優先的 ORM。但最終我將想法轉變為優先考慮數據庫。該決定通過生成用于與數據庫交互的代碼,促使將 ORM 全部丟棄。

我們放棄 ORM 的旅程

以上幾點都是理論上的。我們的旅程從我們從 ORM Gorm 開始感受到的實際痛苦和問題開始。

問題 1:封裝的 API

更新數據庫中的一些記錄是一個早期的臉面時刻。執行此操作的 SQL 是:

UPDATE <table> 
SET <values>
WHERE <conditions>

但是 Gorm API 改變了接受值和條件的順序。

// 預期
db.Model(<table>).Where(<conditions>).Updates(<values>)// 更新整個表,忽略條件
db.Model(<table>).Updates(<values>).Where(<conditions>)

我們經歷了一個艱難的過程,即犯錯會帶來災難性的后果。ORM 的 API 是可鏈接的,但它并不完全是惰性的。某些聲明正在敲定,包括更新。如果您顛倒順序,更新將應用,但 Where 條件不生效,這意味著您已更新模型中的所有內容。

現在你可以爭辯說 SQL 有這種倒退,而 ORM 只是對 API 的真正含義進行了更正。你可以說我們的團隊應該更清楚。或者 API 可以有更好的文檔記錄。無論爭論如何,結果都是一樣的:我們度過了糟糕的一天。更廣泛的觀點是:ORM 實際上是 SQL 的包裝器。

在我職業生涯的早期,一位工程經理告訴我要對包裝器持懷疑態度,因為它們只會給工作量增加認知負擔。我反駁說,根據合乎邏輯的結論,他會用匯編編寫所有代碼。

兩個極端都不正確:抽象是關于實現平衡的行為。但隨著我職業生涯的發展,我將 ORM 置于不必要的包裝陣營中。為什么我們要處理一個與幾乎所有開發人員都接受過培訓的超級穩定和眾所周知的 ANSI SQL API 相反的中間件?每個開發人員都知道如何使用 SQL 更新表中的記錄(或者可以輕松地通過 Google 搜索),并且在出錯時可能會出現錯誤。不是每個開發者,實際上是極少數的開發者,都知道 Gorm 的抽象。對于您使用的任何 ORM,(在新員工中)也是如此;您將不斷地在堆棧的關鍵任務部分培訓人員。

問題2:性能和過多的內存消耗

我們的后端在內存容量有限的無服務器堆棧 (AWS Fargate) 上運行。隨著時間的推移,我們不得不不斷增加實例的內存容量,最終達到最大值,然后看著我們的容器死亡。隨著數據量的增長,我們看到容器的數量以某種線性方式增長。人們會希望后端改為亞線性擴展。

ORM 是一個自然的罪魁禍首:很容易看出許多 ORM 將使用對象自省來構建 SQL 語句、水合結果或兩者兼而有之。Gorm 的內存占用是極端的,但遺憾的是并不少見。Bridge 在后端使用 Python 開始,我們使用 Django 的 ORM 與存在類似問題的數據庫進行交互。

直到我們最終將其從堆棧中刪除以給我們一個比較點時,我們才意識到問題的嚴重程度。詳細信息包含在下一節中,但作為預覽:我們將執行性能提高了約 2 倍,并將內存占用減少了近 10 倍。

問題 3:了解我們的 I/O 配置文件

隨著時間的推移,我們注意到自己使用數據庫日志工具來了解我們自己的用例,并認為這是不匹配的。我們在 AWS 上設置了RDS Performance Insights并pganalyze來識別數據庫中的瓶頸。這些工具很早就證明了它們的價值,我們最終使用它們來了解我們與數據庫交互的方式。我們是否過度獲取列?我們是否正在運行未索引的查詢?

當然,這些問題都有已知的、確定的答案。事實上,我們需要一個外部工具來闡明這個問題,這是代碼中一個明顯的結構缺陷。對我來說,潛在的問題是 ORM 讓與數據庫的交互變得太容易了。代碼沒有集中或模塊化到代碼庫中的中間件層。相反,它自始至終都被意大利面條化了。了解我們的數據庫交互性需要對與業務邏輯有關的內容進行廣泛的代碼審計和審查,而不是讀寫。

備擇方案

使用 ORM 的替代方案似乎相當有限:使用低級數據庫驅動程序,在運行時構建 SQL 查詢,然后自己將結果映射回對象。當然,ORM 以自動化的方式完成所有這些事情,因此走這條路線將對可維護性做出巨大犧牲。我們的團隊得出結論(相當容易且沒有太多決策),無論這里有什么好處,成本都太高了,無法考慮。

然而,還有第三條路線:使用代碼生成器來自動化這些步驟。我們將 Go 社區中的項目分為兩條線:

  • 在運行時生成代碼的 SQL(例如:squirrel)
  • 在編譯時生成應用程序代碼(示例:jet,sqlc)

生成 SQL 代碼是一個有趣的想法,與生成應用程序代碼相比,它需要更少的工具和承諾。然而,我們認為這將是我們代碼可維護性的橫向移動。SQL 生成將需要字符串插值,這意味著在應用數據庫遷移時審核代碼,這是我們希望結束的一個勞動密集型且耗費精力的過程。

嬰兒在洗澡水里?

我們想了很久,想著要不要把嬰兒和洗澡水一起倒掉。也許問題不在于 ORM 本身,而在于代碼優先子集。在 Go 社區中, sqlboiler是一個有趣的項目,它從您的 DDL 生成模型定義。

我們決定不使用這個項目,原因如下:

  • 有太多的代碼生成這樣的事情。生成的代碼需要靈活的配置來控制輸出,這是一條好走的路。一方面,您不希望交換代碼以進行配置,并在代碼庫中放置大量的 yaml 或 toml 文件,這些文件需要自己的一組維護問題。在另一個極端,如果您想要控制或自定義配置中未公開的生成代碼的某些內容,那么您就不走運了。
  • Sqlboiler 很大程度上受到 Active Record 的啟發,我們覺得它過于抽象了數據庫。我們試圖擁抱數據庫,因為從文化上講,我們是一個以數據為中心的組織,希望我們的數據庫在我們的應用程序和 API 中更加透明。

選擇代碼生成器

我們仔細研究了兩個代碼生成器:jet和sqlc,最終選擇了 sqlc。使用 jet,您可以在應用程序中將 SQL 作為 DSL 編寫。但因為它生成代碼,所以它比 squirrel 等運行時 SQL 生成器提供的功能更進一步。模型和字段是一流的可引用類型,而不是需要字符串插值,這避免了當您想要進行更改時需要在審計過程中通過代碼進行 grep。

更吸引人的是,它提供了一種在數據庫中聚合或反規范化數據的方法。ORM 的目標是使關系遍歷變得容易,而 Jet 的目標是在完整且類型良好的結構中提供數據包,清楚地宣傳其中的可用內容。這是一個例子:

stmt := SELECT( 
Actor.ActorID, Actor.FirstName, Actor.LastName, Actor.LastUpdate,
Film.AllColumns,
Language.AllColumns,
Category.AllColumns,
).FROM(
Actor.INNER_JOIN
(FilmActor, Actor.ActorID.EQ(FilmActor .ActorID)).
INNER_JOIN(Film, Film.FilmID.EQ(FilmActor.FilmID)).
INNER_JOIN(Language, Language.LanguageID.EQ(Film.LanguageID)).
INNER_JOIN(FilmCategory, FilmCategory.FilmID.EQ(Film.FilmID) )).
INNER_JOIN(Category, Category.CategoryID.EQ(FilmCategory.CategoryID)),
).WHERE(
Language.Name.EQ(String("English")).
AND(Category.Name.NOT_EQ(String("Action" ))).
AND(Film.Length.GT(Int(180))),
).ORDER_BY(
Actor.ActorID.ASC(),
Film.FilmID.ASC(),
)var dest []struct {
model.Actor 電影 []struct {
model.Film 語言模型.語言
類別 []model.Category
}
}// 執行查詢并存儲結果
err = stmt.Query(db, &dest)

這里有很多數據聚合。正在建立的應用程序端模型是一個演員,其中包含他們參與過的所有電影、電影所使用的語言以及其所屬的類別。

我們最初被這個設計所吸引,但在嘗試了一下之后感覺不太對勁。在這個例子中,查詢在應用程序中驅動數據模型,而不是相反,我們擔心這種方法會導致大量丟棄的聚合模型。我們的目標是推廣具有大量業務邏輯和可變性的可重用模型,這些模型在其類型的方法中捕獲。

此外,我們的偏好是將 SQL 完全從代碼中移出。這里的問題是任何開發人員都可以隨心所欲地查詢數據庫。雖然這是最初的生產力勝利,但其代價是代碼和運行時性能的長期可維護性。如果開發人員在不使用索引的情況下以次優方式查詢數據庫怎么辦?隨著數據模型變得更大和復雜,這種風險很高,因為它是從 SQL 中刪除的一個步驟。雖然 DSL 很受歡迎,但我們仍然覺得它最終像包裝器一樣。

答案:sqlc

我們決定使用sqlc,一個可配置的可選 sql 編譯器。這種方法引起了我們的共鳴;我們喜歡它不會生成您不需要的東西,并且生成的代碼可以根據我們定義的類型和標簽進行定制。它使代碼感覺像我們的,同時提供了遷移我們當前實現的明顯路徑。我將在以后的文章中詳細說明我們如何讓 sqlc 為我們工作。

刪除 ORM 的好處

這個項目是一項艱巨的任務,不僅需要我們的開發人員,還需要我們的產品團隊和整個公司的承諾。我們遇到了功能凍結、將 ORM 與生成的代碼并行運行的問題,并且必須仔細規劃我們的遷移和部署路徑。所有這一切都是在一家資源有限的小型(但不斷發展的)公司的背景下進行的。有了所有這些成本,收益最好是顯著的,而且確實如此。其中,我們在后端運行時實現了更好的性能和可擴展性,更好的代碼庫可維護性,更少依賴數據庫日志來理解我們的數據 I/O 配置文件,更云原生的實現和后端數據模型的透明度我們所有的開發人員,無論他們每天是否接近堆棧中的數據庫。

性能和規模

如果您的 ORM 是動態的,沒有使用生成的代碼或使用泛型類型或接口,那么它可能在幕后進行了某種程度的反射。在我們的例子中,Gorm 大量使用反射,因為 Go 不支持泛型,而且 Gorm 沒有定義很多接口,除了要求您聲明對應于應用程序模型的表名。因此,我們期望在這里獲得巨大的收益,但是當我們開始對我們的系統進行基準測試時,我們高興地印象深刻。

性能是關于實現低運行時執行。我們通過識別后端中典型的各種工作負載來對結果進行基準測試,這可能是因為 API 正在執行它們,也可能是由于脫機或批處理進程導致了對數據庫的大量 I/O。在下圖中,我們在橫軸上有用例;藍色表示我們的 sqlc 驅動的數據交互層,紅色表示我們當前使用 Gorm ORM 的延遲。越低越好。

在沒有 ORM 的情況下,運行時性能提高了 52%

在我們的工作負載中,我們正在享受大約 2 倍的執行性能加速。令人高興的是,當工作負載獲取更多數據時,這個數字往往會更高。

可擴展性是指消耗盡可能少的內存,這對我們來說尤其重要,因為我們在無服務器后端 (AWS Fargate) 上運行所有工作負載,因此我們更適合橫向擴展而不是向上擴展。我們在每個實例上使用的內存越少,意味著需要上線的實例就越少才能達到結果,這意味著成本更低,整體使用率更高。換句話說,如果您需要的實例數量是當前使用數量的一半(預算內),您應該能夠處理雙倍的數據量,而無需與您的 CFO 交談。

沒有 ORM 的內存消耗減少了 78%

我們平均減少了 78% 的內存消耗。現在你可能會爭辯說,也許 Gorm 在這里做的事情效率太低了,而其他 ORM 可能會更好,但從根本上說,大多數映射器都需要類型自省,這將導致糟糕的內存配置文件。

這兩項改進都是由每個操作需要發生的分配數量減少驅動的,我們已經將其基準為另一個 80% 的下降:

在沒有 ORM 的情況下,每個操作的分配量減少了 80%

代碼可維護性

我認為所有與數據層交互的代碼都是中間件。當然,如果您使用的是 ORM,您可能沒有將這個中間件顯式地打包到一個包或一組函數中,我認為這會讓您的情況變得更糟:中間件仍然存在,但它不是孤立的。相反,數據庫交互性在整個代碼庫中被意大利面條化了。

當我們想要檢索、更新、創建或刪除數據時,我們會調用為我們執行此操作的函數:

q.GetAccounts(ctx, ids)// 更復雜的查詢采用生成的 Params 類型
q.GetAccountsPage(ctx, db.GetAccountsPageParams{...})

我們的端點甚至不這樣做;他們通過調用接口抽象出細節:

結果,錯誤:= fetch.Page(ctx, fetch.PageParams{ 
Fetcher: accounts.Fetcher{},
...
})

了解我們的數據 I/O 配置文件

當您擁有 ORM 時,您正在邀請組織中的所有軟件開發人員以可能無法解釋的方式訪問數據庫。盡管盡最大努力培訓您的團隊了解哪些索引可用或設置 DBA 角色,但最終您將擁有無法在沒有代碼審查的情況下解釋的數據庫交互代碼。這不可避免地導致人們轉向數據庫日志和監控解決方案,以了解如何訪問數據庫。這些工具對于任何審查運行時性能和實現 SLA 的流程都是受歡迎的補充,但如果您使用它們來了解如何訪問您的數據庫,那就太遲了。

我們仍然使用RDS Performance Insights和pganalyze等工具,但我們不再依賴它們來了解一般配置文件,或者擔心我們是否正在使用索引。這項工作已轉移到我們的集中存儲庫,它充當我們所有數據庫 I/O 的中間件,我們簡稱為數據存儲庫。

它不是沒有進程,但現在它是一個托管進程。當應用程序開發人員需要一個新查詢時,她需要在數據倉庫中打開一個 PR,該 PR 將附帶代碼審查,人們可以在其中詢問是否正在使用索引或事務。誠然,這樣的代碼審查標準應該適用于所有存儲庫,但數據庫 I/O 將只是下游應用程序中的一個要點。我們的數據倉庫只關注一件事,而且只關注一件事:托管數據庫交互。此外,事后進行代碼審計也很容易。DDL 和 SQL 查詢都是并排的,因此很容易知道查詢是否正確地使用了索引。

更多云原生實現

您的里程可能會有所不同,但我們使用的兩個 ORM(Gorm 和 Django)都包裝了數據庫連接,導致了兩個問題。首先,在這兩種情況下,包裝對象暴露的功能都少于底層驅動程序中可用的功能。隨著數據庫和驅動程序更新以滿足特定需求,這可能會變得非常令人沮喪。

其次,特別是在 Django 的情況下,它讓我們遠離了云原生設計。我們特別努力的一個方面是從我們的 Lambda 函數中訪問數據。諸如 Lambda 之類的函數即服務平臺將希望您將數據庫連接定義為全局變量,以便它是可凍結的。這個任務在 Django 上基本上是不可能的。盡管我們在 Gorm 中解決這個問題的麻煩較少,但我們在獲得我們想要的連接池特性方面遇到了其他問題,即使在云中長期存在的計算層上也是如此。

最終能否實現云原生設計取決于您選擇的數據庫驅動程序,并且您需要確保您的 ORM 支持該驅動程序。我們很幸運使用了 Postgres,更幸運的是 Go 社區有專門的驅動程序:jackc/pgx。能夠在沒有 ORM 的情況下直接使用此驅動程序,使我們在云原生設計方面具有更大的靈活性,并能夠利用 Postgres 特定的功能,這些功能通常被其他優先考慮廣泛的跨數據庫支持的驅動程序所遺漏。

數據模型透明度

最后,也許是最重要的一點,放棄我們的 ORM 通過提高數據模型的透明度改變了我們的工程文化,使其更加以數據為中心。Bridge 是一家數據處理公司。我們為注冊投資顧問、企業和其他金融科技平臺做標準化和豐富金融數據的工作。

我們重視數據的完整性、準確性和一致性以實現這些目標,除非每個人都認為他們了解數據模型,否則我們無法做到這一點。許多 ORM 在哲學上是圍繞在開發過程中隱藏或抽象數據庫而構建的,這最終將導致您的團隊密切關注“O”并降低“R”的優先級。并且“O”被鎖定在單個存儲庫中,任何一個人都可能知道也可能不知道。但是每個人都可以了解數據庫的結構安排:組織成模式、DDL、E/R 圖等。

對我們來說,我們的數據庫不僅僅是我們讀取和寫入的信息容器。這是我們思想的表達;我們如何簡化和模擬行業挑戰的復雜性。刪除 ORM 將所有這些細節置于人們腦海中的前端中心,創造了對數據模型和 Postgres 的更多所有權,減少了“越界”的心態。這也許是最好的收獲。

責任編輯:未麗燕 來源: 今日頭條
相關推薦

2009-08-13 17:14:55

2023-05-22 09:27:11

GMPGolang

2023-08-21 07:34:37

GolangGMP

2023-10-22 20:20:37

FiberGo

2025-03-11 10:00:20

Golang編程函數

2021-05-28 05:34:06

Golang語言編程

2020-07-03 07:56:34

Golang編程語言

2023-11-13 21:55:12

Go編程

2009-07-02 13:51:05

對象和范圍屬性

2024-04-24 10:57:54

Golang編程

2022-07-20 08:04:06

net包DNScontext

2010-05-25 17:41:20

2023-01-27 23:11:25

GolangNetHttp

2010-07-02 12:02:11

eMule協議

2023-08-29 09:00:00

JavaScript工具

2022-08-12 12:23:55

golangmap數據結構

2015-09-29 10:07:58

中文編碼

2023-10-24 16:03:34

GoGolang

2023-11-30 07:15:36

GolangRecover

2012-03-08 13:58:39

蘋果新iPad
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 欧美成人精品欧美一级 | 伊人狠狠干| 日韩欧美精品 | 成人一级毛片 | 国产精品毛片无码 | 岛国视频 | 四虎永久在线精品免费一区二 | 91精品国产91久久综合桃花 | 久久久久久久久久久久91 | av网址在线| 奇米超碰在线 | 狠狠操电影 | 看羞羞视频 | 亚洲二区在线 | 国产三区av | 久久av综合| 在线观看欧美日韩视频 | 国产精品视频免费观看 | 成人免费在线 | h视频免费在线观看 | 欧美精品一区二区在线观看 | 精品一区二区三区在线视频 | 99热在线免费 | 久久免费视频观看 | 日本男人天堂 | 国产一区欧美一区 | 99re视频在线免费观看 | 一区二区免费 | 岛国精品 | 亚洲九色| 精品久久久网站 | 九九综合九九 | 亚洲成人一区 | 国产激情一区二区三区 | 久久国产亚洲 | 精品欧美一区二区三区久久久 | 亚洲一区在线日韩在线深爱 | 久久99精品久久久久婷婷 | 国产精品一区二区在线 | 午夜精品久久久久久久久久久久久 | 国产视频一视频二 |