網站首頁 編程語言 正文
前言
原文:A gentle introduction to generics in Go byDominik Braun
萬俊峰Kevin:我看了覺得文章非常簡單易懂,就征求了作者同意,翻譯出來給大家分享一下。
本文是對泛型的基本思想及其在 Go 中的實現的一個比較容易理解的介紹,同時也是對圍繞泛型的各種性能討論的簡單總結。首先,我們來看看泛型所解決的核心問題。
問題
假設我們想實現一個簡單的tree
數據結構。每個節點持有一個值。在 Go 1.18 之前,實現這種結構的典型方法如下。
type?Node?struct?{ ????value?interface{} }
這在大多數情況下都很好用,但它也有一些缺點。
首先,interface{}
可以是任何東西。如果我們想限制value
可能持有的類型,例如整數和浮點數,我們只能在運行時檢查這個限制。
func?(n?Node)?IsValid()?bool?{ ????switch?n.value.(type)?{ ????????case?int,?float32,?float64: ????????????return?true ????????default: ????????????return?false ????} }
這樣并不可能在編譯時限制類型,像上面這樣的類型判斷在許多 Go 庫中都是很常見的做法。這里有 go-zero 項目中的例子。
第二,對 Node 中的值進行處理是非常繁瑣和容易出錯的。對值做任何事情都會涉及到某種類型的斷言,即使你可以安全地假設值持有一個int
值。
number,?ok?:=?node.value.(int) if?!ok?{ ????//?... } double?:=?number?*?2
這些只是使用interface{}
的一些不便之處,它沒有提供類型安全,并有可能導致難以恢復的運行時錯誤。
解決方法
我們不打算接受任意數據類型或具體類型,而是定義一個叫做T
的占位符類型作為值的類型。請注意,這段代碼還不會通過編譯。
type?Node[T]?struct?{ ????value?T }
首先需要聲明泛型類型T
,這是在結構或函數名稱后面方括號里面使用的。
T
可以是任何類型,只有在實例化一個具有明確類型的Node
時,T
才會被推導為該類型。
n?:=?Node[int]{ ????value:?5, }
泛型Node
被實例化為Node[int]
(整數節點),所以T
是一個int
。
類型約束
上面的實現里,T
的聲明缺少一個必要的信息:類型約束。
類型約束用于進一步限制可以作為T
的可能類型。Go 本身提供了一些預定義的類型約束,但也可以使用自定義的類型約束。
type?Node[T?any]?struct?{ ????value?T }
任意類型(any)約束允許T
實際上是任何類型。如果節點值需要進行比較,有一個comparable
類型約束,滿足這個預定義約束的類型可以使用==
進行比較。
type?Node[T?comparable]?struct?{ ????value?T }
任何類型都可以作為一個類型約束。Go 1.18 引入了一種新的interface
語法,可以嵌入其他數據類型。
type?Numeric?interface?{ ????int?|?float32?|?float64 }
這意味著一個接口不僅可以定義一組方法,還可以定義一組類型。使用Numeric
接口作為類型約束,意味著值可以是整數或浮點數。
type?Node[T?Numeric]?struct?{ ????value?T }
重獲類型安全
相對于使用interface{}
,泛型類型參數的巨大優勢在于,T
的最終類型在編譯時就會被推導出來。為T
定義一個類型約束,完全消除了運行時檢查。如果用作T
的類型不滿足類型約束,代碼就不會編譯通過。
在編寫泛型代碼時,你可以像已經知道T
的最終類型一樣寫代碼。
func?(n?Node[T])?Value()?T?{ ????return?n.value }
上面的函數返回n.Value
,它的類型是T
。因此,返回值是T
,如果T
是一個整數,那么返回類型就已知是int
。因此,返回值可以直接作為一個整數使用,不需要任何類型斷言。
n?:=?Node[int]{ ????value:?5, } double?:=?n.Value()?*?2
在編譯時恢復類型安全使 Go 代碼更可靠,更不容易出錯。
泛型使用場景
在Ian Lance Taylor
的 When To Use Generics 中列出了泛型的典型使用場景,歸結為三種主要情況:
- 使用內置的容器類型,如
slices
、maps
和channels
- 實現通用的數據結構,如
linked list
或tree
- 編寫一個函數,其實現對許多類型來說都是一樣的,比如一個排序函數
一般來說,當你不想對你所操作的值的內容做出假設時,可以考慮使用泛型。我們例子中的Node
并不太關心它持有的值。
當不同的類型有不同的實現時,泛型就不是一個好的選擇。另外,不要把Read(r io.Reader)
這樣的接口函數簽名改為Read[T io.Reader](r T)
這樣的通用簽名。
性能
要了解泛型的性能及其在 Go 中的實現,首先需要了解一般情況下實現泛型的兩種最常見方式。
這是對各種性能的深入研究和圍繞它們進行的討論的簡要介紹。你大概率不太需要關心 Go 中泛型的性能。
虛擬方法表
在編譯器中實現泛型的一種方法是使用Virtual Method Table
。泛型函數被修改成只接受指針作為參數的方式。然后,這些值被分配到堆上,這些值的指針被傳遞給泛型函數。這樣做是因為指針看起來總是一樣的,不管它指向的是什么類型。
如果這些值是對象,而泛型函數需要調用這些對象的方法,它就不能再這樣做了。該函數只有一個指向對象的指針,不知道它們的方法在哪里。因此,它需要一個可以查詢方法的內存地址的表格:Virtual Method Table
。這種所謂的動態調度已經被 Go 和 Java 等語言中的接口所使用。
Virtual Method Table
不僅可以用來實現泛型,還可以用來實現其他類型的多態性。然而,推導這些指針和調用虛擬函數要比直接調用函數慢,而且使用Virtual Method Table
會阻止編譯器進行優化。
單態化
一個更簡單的方法是單態化(Monomorphization
),編譯器為每個被調用的數據類型生成一個泛型函數的副本。
func?max[T?Numeric](a,?b?T)?T?{ ????//?... } larger?:=?max(3,?5)
由于上面顯示的max函數是用兩個整數調用的,編譯器在對代碼進行單態化時將為int
生成一個max
的副本。
func?maxInt(a,?b?int)?int?{ ????//?... } larger?:=?maxInt(3,?5)
最大的優勢是,Monomorphization
帶來的運行時性能明顯好于使用Virtual Method Table
。直接方法調用不僅更有效率,而且還能適用整個編譯器的優化鏈。不過,這樣做的代價是編譯時長,為所有相關類型生成泛型函數的副本是非常耗時的。
Go 的實現
這兩種方法中哪一種最適合 Go?快速編譯很重要,但運行時性能也很重要。為了滿足這些要求,Go 團隊決定在實現泛型時混合兩種方法。
Go 使用Monomorphization
,但試圖減少需要生成的函數副本的數量。它不是為每個類型創建一個副本,而是為內存中的每個布局生成一個副本:int
、float64
、Node
和其他所謂的"值類型"
在內存中看起來都不一樣,因此泛型函數將為所有這些類型復制副本。
與值類型相反,指針和接口在內存中總是有相同的布局。編譯器將為指針和接口的調用生成一個泛型函數的副本。就像Virtual Method Table
一樣,泛型函數接收指針,因此需要一個表來動態地查找方法地址。在 Go 實現中的字典與虛擬方法表的性能特點相同。
結論
這種混合方法的好處是,你在使用值類型的調用中獲得了Monomorphization
的性能優勢,而只在使用指針或接口的調用中付出了Virtual Method Table
的成本。
在性能討論中經常被忽略的是,所有這些好處和成本只涉及到函數的調用。通常情況下,大部分的執行時間是在函數內部使用的。調用方法的性能開銷可能不會成為性能瓶頸,即使是這樣,也要考慮先優化函數實現,再考慮調用開銷。
原文鏈接:https://mp.weixin.qq.com/s/srqZeFSYT-pkUXWY_vrIVg
相關推薦
- 2022-05-21 C語言實現銷售管理系統課程設計_C 語言
- 2022-03-27 C++編輯距離(動態規劃)_C 語言
- 2022-09-20 C#單線程和多線程端口掃描器詳解_C#教程
- 2022-09-14 Android自定義視圖中圖片的處理_Android
- 2024-01-12 com.fasterxml.jackson.databind.exc.InvalidDefiniti
- 2022-08-22 Python中可以用三種方法判斷文件是否存在_python
- 2022-11-25 PostgreSQL自增主鍵用法及在mybatis中的使用教程_PostgreSQL
- 2022-08-01 Android開發之Flutter與webview通信橋梁實現_Android
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支