在引入泛型之前,有幾種方法來實現支援不同資料類型的泛型函數:
方法 1:為每個資料型別實作一個函數
這種方式會導致程式碼極度冗餘和維護成本高昂。任何修改都需要對所有函數執行相同的操作。而且,由於Go語言不支援同名函數重載,因此暴露這些函數供外部模組呼叫也不方便。
方法二:使用範圍最大的資料型別
為了避免程式碼冗餘,另一種方法是使用範圍最大的資料類型,即方法2。典型的例子是math.Max,它會傳回兩個數字中較大的一個。為了能夠比較各種數據類型的數據,math.Max使用了Go中數值類型中範圍最大的float64數據類型作為輸入和輸出參數,從而避免了精度損失。雖然這在一定程度上解決了程式碼冗餘問題,但任何類型的資料都需要先轉換為float64類型。例如,當比較 int 和 int 時,仍然需要進行類型轉換,這不僅會降低效能,而且看起來不自然。
方法 3:使用介面{}型別
使用interface{}類型有效解決了上述問題。然而,interface{}類型引入了一定的運行時開銷,因為它需要在運行時進行類型斷言或類型判斷,這可能會導致一些效能下降。另外,當使用interface{}類型時,編譯器無法進行靜態類型檢查,因此某些類型錯誤可能只能在執行時發現。
Go 1.18 引入了對泛型的支持,這是 Go 語言開源以來的重大變化。
泛型是程式語言的一個特性。它允許程式設計師在程式設計中使用泛型類型而不是實際類型。然後在實際呼叫時透過明確傳遞或自動推導,取代泛型類型,達到程式碼重複使用的目的。在使用泛型的過程中,將要操作的資料類型指定為參數。這樣的參數類型在類別、介面和方法中分別稱為泛型類別、泛型介面和泛型方法。
泛型的主要優點是提高程式碼的可重複使用性和類型安全性。與傳統的形式參數相比,泛型使得編寫通用程式碼更加簡潔靈活,提供了處理不同類型資料的能力,進一步增強了Go語言的表達能力和復用性。同時,由於泛型的具體類型是在編譯時確定的,因此可以提供類型檢查,避免類型轉換錯誤。
在Go語言中,interface{}和泛型都是處理多種資料類型的工具。為了討論它們的差別,我們先來看看interface{}和泛型的實作原理。
interface{} 是一個空接口,介面類型中沒有方法。由於所有類型都實作了interface{},因此它可用於建立可接受任何類型的函數、方法或資料結構。 interface{}在執行時的底層結構表示為eface,其結構如下所示,主要包含_type和data兩個欄位。
type eface struct { _type *_type data unsafe.Pointer } type type struct { Size uintptr PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers Hash uint32 // hash of type; avoids computation in hash tables TFlag TFlag // extra type information flags Align_ uint8 // alignment of variable with this type FieldAlign_ uint8 // alignment of struct field with this type Kind_ uint8 // enumeration for C // function for comparing objects of this type // (ptr to object A, ptr to object B) -> ==? Equal func(unsafe.Pointer, unsafe.Pointer) bool // GCData stores the GC type data for the garbage collector. // If the KindGCProg bit is set in kind, GCData is a GC program. // Otherwise it is a ptrmask bitmap. See mbitmap.go for details. GCData *byte Str NameOff // string form PtrToThis TypeOff // type for pointer to this type, may be zero }
_type 是指向 _type 結構的指針,其中包含實際值的大小、種類、雜湊函數和字串表示等資訊。 data 是指向實際資料的指標。如果實際資料的大小小於或等於指標的大小,則將資料直接儲存到資料欄位中;否則,資料欄位將儲存指向實際資料的指標。
當特定類型的物件被賦值給interface{}類型的變數時,Go語言會隱式執行eface的裝箱操作,將_type欄位設定為值的類型,將data欄位設定為值的資料。例如,當執行語句 var i interface{} = 123 時,Go 會建立一個 eface 結構體,其中 _type 欄位代表 int 類型,data 欄位代表值 123。
當從interface{}中檢索儲存的值時,會發生一個拆箱過程,即類型斷言或類型判斷。此過程需要明確指定預期類型。如果interface{}中儲存的值的類型與預期類型匹配,則類型斷言將成功,並且可以檢索該值。否則,類型斷言將會失敗,這種情況需要額外的處理。
var i interface{} = "hello" s, ok := i.(string) if ok { fmt.Println(s) // Output "hello" } else { fmt.Println("not a string") }
可以看出,interface{}透過運行時的裝箱和拆箱操作,支援多種資料類型的操作。
Go核心團隊在評估Go泛型的實現方案時非常謹慎。共提交了三個實施方案:
Stenciling方案也是C、Rust等語言實作泛型所採用的實作方案。其實現原理是,在編譯期間,根據呼叫泛型函數時的特定類型參數或約束中的類型元素,為每個類型參數產生泛型函數的單獨實現,以確保類型安全性和效能最優。然而,這種方法會減慢編譯速度。因為當呼叫多種資料類型時,泛型函數需要為每種資料類型產生獨立的函數,這可能會導致編譯後的檔案非常大。同時,由於CPU快取未命中、指令分支預測等問題,產生的程式碼可能無法有效率地運作。
Dictionaries 方案只為泛型函數產生一個函數邏輯,但加入了一個參數 dict 作為函數的第一個參數。 dict 參數在呼叫泛型函數時儲存類型參數的類型相關訊息,並在函數呼叫期間使用 AX 暫存器(AMD)傳遞字典資訊。這種方案的優點是減少了編譯階段的開銷,並且不會增加二進位檔案的大小。但增加了運行時開銷,無法在編譯階段進行函數最佳化,並且存在字典遞歸等問題。
type eface struct { _type *_type data unsafe.Pointer } type type struct { Size uintptr PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers Hash uint32 // hash of type; avoids computation in hash tables TFlag TFlag // extra type information flags Align_ uint8 // alignment of variable with this type FieldAlign_ uint8 // alignment of struct field with this type Kind_ uint8 // enumeration for C // function for comparing objects of this type // (ptr to object A, ptr to object B) -> ==? Equal func(unsafe.Pointer, unsafe.Pointer) bool // GCData stores the GC type data for the garbage collector. // If the KindGCProg bit is set in kind, GCData is a GC program. // Otherwise it is a ptrmask bitmap. See mbitmap.go for details. GCData *byte Str NameOff // string form PtrToThis TypeOff // type for pointer to this type, may be zero }
Go 最終綜合了上述兩種方案,提出了 GC Shape Stenciling 方案進行通用實現。它以類型的 GC Shape 為單位產生函數代碼。具有相同 GC Shape 的類型重複使用相同的程式碼(類型的 GC Shape 指的是它在 Go 記憶體分配器/垃圾收集器中的表示)。所有指標類型都重複使用 *uint8 類型。對於具有相同 GC Shape 的類型,使用共享的實例化函數程式碼。此方案還會自動為每個實例化的函數程式碼新增一個dict參數,以區分具有相同GC Shape的不同類型。
var i interface{} = "hello" s, ok := i.(string) if ok { fmt.Println(s) // Output "hello" } else { fmt.Println("not a string") }
從interface{}和泛型的底層實作原理可以發現,它們的主要差異是interface{}支援在執行時間處理不同的資料類型,而泛型支援在編譯階段靜態處理不同的資料類型。實際使用主要有以下區別:
(1) 效能差異:將不同類型的資料指派給介面{}或從介面{}擷取不同類型的資料時執行的裝箱和拆箱作業成本高昂,並會帶來額外的開銷。相較之下,泛型不需要裝箱和拆箱操作,並且泛型產生的程式碼針對特定類型進行了最佳化,避免了運行時效能開銷。
(2)型別安全:使用interface{}類型時,編譯器無法進行靜態型別檢查,只能在執行時進行型別斷言。因此,某些類型錯誤可能只能在運行時才能發現。相比之下,Go 的泛型程式碼是在編譯時產生的,因此泛型程式碼可以在編譯時獲取類型信息,保證類型安全。
在Go語言中,類型參數是不允許與nil直接比較的,因為類型參數是在編譯時進行類型檢查的,而nil是運行時的一個特殊值。由於類型參數的底層類型在編譯時是未知的,因此編譯器無法確定類型參數的底層類型是否支援與 nil 進行比較。因此,為了維護類型安全並避免潛在的運行時錯誤,Go語言不允許類型參數與nil直接比較。
type eface struct { _type *_type data unsafe.Pointer } type type struct { Size uintptr PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers Hash uint32 // hash of type; avoids computation in hash tables TFlag TFlag // extra type information flags Align_ uint8 // alignment of variable with this type FieldAlign_ uint8 // alignment of struct field with this type Kind_ uint8 // enumeration for C // function for comparing objects of this type // (ptr to object A, ptr to object B) -> ==? Equal func(unsafe.Pointer, unsafe.Pointer) bool // GCData stores the GC type data for the garbage collector. // If the KindGCProg bit is set in kind, GCData is a GC program. // Otherwise it is a ptrmask bitmap. See mbitmap.go for details. GCData *byte Str NameOff // string form PtrToThis TypeOff // type for pointer to this type, may be zero }
底層元素的類型T必須是基底類型,不能是介面類型。
type eface struct { _type *_type data unsafe.Pointer } type type struct { Size uintptr PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers Hash uint32 // hash of type; avoids computation in hash tables TFlag TFlag // extra type information flags Align_ uint8 // alignment of variable with this type FieldAlign_ uint8 // alignment of struct field with this type Kind_ uint8 // enumeration for C // function for comparing objects of this type // (ptr to object A, ptr to object B) -> ==? Equal func(unsafe.Pointer, unsafe.Pointer) bool // GCData stores the GC type data for the garbage collector. // If the KindGCProg bit is set in kind, GCData is a GC program. // Otherwise it is a ptrmask bitmap. See mbitmap.go for details. GCData *byte Str NameOff // string form PtrToThis TypeOff // type for pointer to this type, may be zero }
聯合型別元素不能是型別參數,非介面元素必須成對不相交。如果有多個元素,則不能包含具有非空方法的介面類型,也不能進行比較或嵌入比較。
var i interface{} = "hello" s, ok := i.(string) if ok { fmt.Println(s) // Output "hello" } else { fmt.Println("not a string") }
type Op interface{ int|float } func Add[T Op](m, n T) T { return m + n } // After generation => const dict = map[type] typeInfo{ int : intInfo{ newFunc, lessFucn, //...... }, float : floatInfo } func Add(dict[T], m, n T) T{}
為了用好泛型,在使用過程中應注意以下幾點:
type V interface{ int|float|*int|*float } func F[T V](m, n T) {} // 1. Generate templates for regular types int/float func F[go.shape.int_0](m, n int){} func F[go.shape.float_0](m, n int){} // 2. Pointer types reuse the same template func F[go.shape.*uint8_0](m, n int){} // 3. Add dictionary passing during the call const dict = map[type] typeInfo{ int : intInfo{}, float : floatInfo{} } func F[go.shape.int_0](dict[int],m, n int){}
上面的程式碼會報錯:無效運算:ptr(受 *int | *uint 限制的 T 型別變數)的指標必須具有相同的基底型別。出現這個錯誤的原因是T是型別參數,而型別參數不是指針,不支援解引用操作。這可以透過將定義更改為以下內容來解決:
// Wrong example func ZeroValue0[T any](v T) bool { return v == nil } // Correct example 1 func Zero1[T any]() T { return *new(T) } // Correct example 2 func Zero2[T any]() T { var t T return t } // Correct example 3 func Zero3[T any]() (t T) { return }
總的來說,仿製藥的好處可以概括為三個面向:
最後跟大家介紹最適合部署Go服務的平台Leapcell。
在文件中探索更多內容!
Leapcell Twitter:https://x.com/LeapcellHQ
以上是Go 泛型:深入探討的詳細內容。更多資訊請關注PHP中文網其他相關文章!