Go 泛型:深入探討
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. 最佳實踐
為了用好泛型,在使用過程中應注意以下幾點:
- 避免過度概括。 泛型並不適合所有場景,需要仔細考慮適合哪些場景。適當的時候可以使用反射:Go 有運行時反射。反射機制支援一定意義上的泛型程式設計。如果某些操作需要支援以下場景,可以考慮反射: (1) 對沒有方法的類型進行操作,其中介面類型不適用。 (2) 當各個類型的操作邏輯不同時,泛型不適用。一個例子是encoding/json套件的實作。由於不希望每個要編碼的類型實作 MarshalJson 方法,因此不能使用介面類型。並且由於不同類型的編碼邏輯不同,所以不應該使用泛型。
- 明確使用 *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 }
概括
總的來說,仿製藥的好處可以概括為三個面向:
- 類型在編譯期間決定,確保型別安全。放進去的就是取出來的。
- 可讀性提高。實際的資料類型在編碼階段就已經明確知道。
- 泛型合併了針對相同類型的處理程式碼,提高了程式碼重複使用率,增加了程式的通用靈活性。 然而,泛型並不是一般資料類型所必需的。還是需要根據實際使用情況慎重考慮是否使用泛型。
Leapcell:Go Web 託管、非同步任務和 Redis 的高級平台
最後跟大家介紹最適合部署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中文網其他相關文章!

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

Go語言在構建高效且可擴展的系統中表現出色,其優勢包括:1.高性能:編譯成機器碼,運行速度快;2.並發編程:通過goroutines和channels簡化多任務處理;3.簡潔性:語法簡潔,降低學習和維護成本;4.跨平台:支持跨平台編譯,方便部署。

Golang在並發性上優於C ,而C 在原始速度上優於Golang。 1)Golang通過goroutine和channel實現高效並發,適合處理大量並發任務。 2)C 通過編譯器優化和標準庫,提供接近硬件的高性能,適合需要極致優化的應用。

Golang和Python各有优势:Golang适合高性能和并发编程,Python适用于数据科学和Web开发。Golang以其并发模型和高效性能著称,Python则以简洁语法和丰富库生态系统著称。

Golang在性能和可擴展性方面優於Python。 1)Golang的編譯型特性和高效並發模型使其在高並發場景下表現出色。 2)Python作為解釋型語言,執行速度較慢,但通過工具如Cython可優化性能。

Golang和C 在性能競賽中的表現各有優勢:1)Golang適合高並發和快速開發,2)C 提供更高性能和細粒度控制。選擇應基於項目需求和團隊技術棧。

goimpactsdevelopmentpositationality throughspeed,效率和模擬性。 1)速度:gocompilesquicklyandrunseff,IdealforlargeProjects.2)效率:效率:ITScomprehenSevestAndardArdardArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdEcceSteral Depentencies,增強的Depleflovelmentimency.3)簡單性。

C 更適合需要直接控制硬件資源和高性能優化的場景,而Golang更適合需要快速開發和高並發處理的場景。 1.C 的優勢在於其接近硬件的特性和高度的優化能力,適合遊戲開發等高性能需求。 2.Golang的優勢在於其簡潔的語法和天然的並發支持,適合高並發服務開發。

Golang和C 在性能上的差異主要體現在內存管理、編譯優化和運行時效率等方面。 1)Golang的垃圾回收機制方便但可能影響性能,2)C 的手動內存管理和編譯器優化在遞歸計算中表現更為高效。
