首頁 >後端開發 >Golang >Go 泛型:深入探討

Go 泛型:深入探討

Mary-Kate Olsen
Mary-Kate Olsen原創
2025-01-01 01:51:091008瀏覽

Go Generics: A Deep Dive

1. 不使用泛型

在引入泛型之前,有幾種方法來實現支援不同資料類型的泛型函數:

方法 1:為每個資料型別實作一個函數
這種方式會導致程式碼極度冗餘和維護成本高昂。任何修改都需要對所有函數執行相同的操作。而且,由於Go語言不支援同名函數重載,因此暴露這些函數供外部模組呼叫也不方便。

方法二:使用範圍最大的資料型別
為了避免程式碼冗餘,另一種方法是使用範圍最大的資料類型,即方法2。典型的例子是math.Max,它會傳回兩個數字中較大的一個。為了能夠比較各種數據類型的數據,math.Max使用了Go中數值類型中範圍最大的float64數據類型作為輸入和輸出參數,從而避免了精度損失。雖然這在一定程度上解決了程式碼冗餘問題,但任何類型的資料都需要先轉換為float64類型。例如,當比較 int 和 int 時,仍然需要進行類型轉換,這不僅會降低效能,而且看起來不自然。

方法 3:使用介面{}型別
使用interface{}類型有效解決了上述問題。然而,interface{}類型引入了一定的運行時開銷,因為它需要在運行時進行類型斷言或類型判斷,這可能會導致一些效能下降。另外,當使用interface{}類型時,編譯器無法進行靜態類型檢查,因此某些類型錯誤可能只能在執行時發現。

2. 泛型的優點

Go 1.18 引入了對泛型的支持,這是 Go 語言開源以來的重大變化。
泛型是程式語言的一個特性。它允許程式設計師在程式設計中使用泛型類型而不是實際類型。然後在實際呼叫時透過明確傳遞或自動推導,取代泛型類型,達到程式碼重複使用的目的。在使用泛型的過程中,將要操作的資料類型指定為參數。這樣的參數類型在類別、介面和方法中分別稱為泛型類別、泛型介面和泛型方法。
泛型的主要優點是提高程式碼的可重複使用性和類型安全性。與傳統的形式參數相比,泛型使得編寫通用程式碼更加簡潔靈活,提供了處理不同類型資料的能力,進一步增強了Go語言的表達能力和復用性。同時,由於泛型的具體類型是在編譯時確定的,因此可以提供類型檢查,避免類型轉換錯誤。

3. 泛型和介面的差別{}

在Go語言中,interface{}和泛型都是處理多種資料類型的工具。為了討論它們的差別,我們先來看看interface{}和泛型的實作原理。

3.1 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{}透過運行時的裝箱和拆箱操作,支援多種資料類型的操作。

3.2 泛型實現原理

Go核心團隊在評估Go泛型的實現方案時非常謹慎。共提交了三個實施方案:

  • 模板方案
  • 字典計畫
  • GC形狀模板方案

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")
}

3.3 差異

從interface{}和泛型的底層實作原理可以發現,它們的主要差異是interface{}支援在執行時間處理不同的資料類型,而泛型支援在編譯階段靜態處理不同的資料類型。實際使用主要有以下區別:
(1) 效能差異:將不同類型的資料指派給介面{}或從介面{}擷取不同類型的資料時執行的裝箱和拆箱作業成本高昂,並會帶來額外的開銷。相較之下,泛型不需要裝箱和拆箱操作,並且泛型產生的程式碼針對特定類型進行了最佳化,避免了運行時效能開銷。
(2)型別安全:使用interface{}類型時,編譯器無法進行靜態型別檢查,只能在執行時進行型別斷言。因此,某些類型錯誤可能只能在運行時才能發現。相比之下,Go 的泛型程式碼是在編譯時產生的,因此泛型程式碼可以在編譯時獲取類型信息,保證類型安全。

4. 泛型的場景

4.1 適用場景

  • 實現通用資料結構時:透過使用泛型,您可以編寫一次程式碼並在不同的資料類型上重複使用它。這減少了程式碼重複並提高了程式碼的可維護性和可擴展性。
  • 在Go 中操作原生容器類型時:如果函數使用Go 內建容器類型(例如切片、映射或通道)的參數,且函數程式碼沒有對容器中的元素類型做出任何特定假設,使用泛型可以將容器演算法與容器中的元素類型完全解耦。在泛型語法出現之前,通常會使用反射來實現,但是反射使得程式碼可讀性較差,無法進行靜態類型檢查,大大增加了程式的運行時開銷。
  • 當不同資料類型的方法實現的邏輯相同時:當不同資料類型的方法功能邏輯相同,唯一差異是輸入參數的資料類型時,可以使用泛型來減少程式碼冗餘。

4.2 不適用場景

  • 不要用型別參數取代介面類型:介面支援某種意義上的泛型程式設計。如果對某些類型的變數的操作只呼叫該類型的方法,則直接使用介面類型即可,無需使用泛型。例如,io.Reader 使用介面從檔案和隨機數產生器讀取各種類型的資料。 io.Reader 從程式碼角度看很容易閱讀,效率很高,函數執行效率幾乎沒有差別,所以不需要使用型別參數。
  • 當不同資料類型的方法實作細節不同時:如果每種類型的方法實作不同,則應使用介面類型而不是泛型。
  • 執行時動態性較強的場景:例如使用switch進行型別判斷的場景,直接使用interface{}會有較好的效果。

5. 泛型中的陷阱

5.1 無比較

在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
}

5.2 無效的底層元素

底層元素的類型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
}

5.3 無效的聯合型別元素

聯合型別元素不能是型別參數,非介面元素必須成對不相交。如果有多個元素,則不能包含具有非空方法的介面類型,也不能進行比較或嵌入比較。

var i interface{} = "hello"
s, ok := i.(string)
if ok {
    fmt.Println(s) // Output "hello"
} else {
    fmt.Println("not a string")
}

5.4 介面類型不能遞歸嵌入

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{}

6. 最佳實踐

為了用好泛型,在使用過程中應注意以下幾點:

  1. 避免過度概括。 泛型並不適合所有場景,需要仔細考慮適合哪些場景。適當的時候可以使用反射:Go 有運行時反射。反射機制支援一定意義上的泛型程式設計。如果某些操作需要支援以下場景,可以考慮反射: (1) 對沒有方法的類型進行操作,其中介面類型不適用。 (2) 當各個類型的操作邏輯不同時,泛型不適用。一個例子是encoding/json套件的實作。由於不希望每個要編碼的類型實作 MarshalJson 方法,因此不能使用介面類型。並且由於不同類型的編碼邏輯不同,所以不應該使用泛型。
  2. 明確使用 *T、[]T 和 map[T1]T2,而不是讓 T 代表指標類型、切片或映射。 與 C 中的型別參數是佔位符並會替換為實際型別不同,Go 中的型別參數 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 
}

概括

總的來說,仿製藥的好處可以概括為三個面向:

  1. 類型在編譯期間決定,確保型別安全。放進去的就是取出來的。
  2. 可讀性提高。實際的資料類型在編碼階段就已經明確知道。
  3. 泛型合併了針對相同類型的處理程式碼,提高了程式碼重複使用率,增加了程式的通用靈活性。 然而,泛型並不是一般資料類型所必需的。還是需要根據實際使用情況慎重考慮是否使用泛型。

Leapcell:Go Web 託管、非同步任務和 Redis 的高級平台

Go Generics: A Deep Dive

最後跟大家介紹最適合部署Go服務的平台Leapcell。

1. 多語言支持

  • 使用 JavaScript、Python、Go 或 Rust 進行開發。

2.免費部署無限個項目

  • 只需支付使用費用-無請求,不收費。

3. 無與倫比的成本效益

  • 即用即付,無閒置費用。
  • 範例:25 美元支援 694 萬個請求,平均回應時間為 60 毫秒。

4.簡化的開發者體驗

  • 直覺的使用者介面,輕鬆設定。
  • 完全自動化的 CI/CD 管道和 GitOps 整合。
  • 即時指標和日誌記錄以獲取可操作的見解。

5. 輕鬆的可擴充性和高效能

  • 自動擴充以輕鬆處理高並發。
  • 零營運開銷-只需專注於建置。

在文件中探索更多內容!

Leapcell Twitter:https://x.com/LeapcellHQ

以上是Go 泛型:深入探討的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn