Skip to main content

Command Palette

Search for a command to run...

GO Generic 入門筆記

Updated
4 min read

類型安全

過往使用interface{}

a和b的類型在執行時才會被檢查,這就增加了出錯的可能性。

func Add(a, b interface{}) interface{} {
    return a.(int) + b.(int)  // 需要type assertion,且不安全
}

Generic 類型約束

泛型透過在編譯時期進行類型檢查,来解决這個問題。

func Add[T Addable](a, b T) T {
    return a + b  // 類型安全
}

Addable是一個類型約束,只允許那些满足某些條件的類型(比如,可以進行加法操作的類型)作为泛型参数。

性能議題

Generic 由于其高度抽象,可能會讓人擔心性能損失議題. 但事實上, 在Go語言中, Generic的實現方式是在編譯時期產生特定類型的程式碼, 因此性能損失議題是可以控制評估的.

// 編譯時期會產生以下程式碼
func Add_int(a, b int) int {
    return a + b
}

func Add_float64(a, b float64) float64 {
    return a + b
}

類型參數

基礎語法

在Go中,泛型的類型參數通常使用中括號來聲明,緊隨在函數或結構體名稱之後。

func Add[T any](a, b T) T {
    return a + b
}

T是一個類型參數, 並且使用了any類型約束, 意味著它可以是任何類型.

多類型參數

Go泛型不僅支持單一個類型參數, 還可以定義多個類型參數.

func Pair[T, U any](a T, b U) (T, U) {
    return a, b
}

Pair函數接受兩個不同類型約束的參數a和b, 並回傳這兩個參數類型.

類型约束

内建约束

Go内置了幾種類型约束,如 any,表示任何類型都可以作為參數。

func PrintSlice[T any](s []T) {
    for _, v := range s {
        fmt.Println(v)
    }
}

自定義約束

除了内建約束,Go還允許開發者定義自己要的約束條件。這通常是透過interface来實現的。

// Addable只允許int 或 float64類型
type Addable interface {
    int | float64
}

func Add[T Addable](a, b T) T {
    return a + b
}

Addable 是一個自定義的類型約束.

底層類型(Underlying Type)

在Go中,每個類型都有一個底層類型: 對於預先定義的類型,比如 int、float64 等,底層類型就是它們自己。對於類型定義(比如 type MyInt int),底層類型是在類型定義之前的類型。

~ 符號的作用

~ 符號用于表示與指定类型有相同底層類型的所有類型。當你在類型參數的约束中使用 ~ 符號時,你指定了一個類型集合,這個集合包含所有底層類型與约束中指定的類型相同的類型。

假阿設我們有以下類型定義:

type MyInt int
type YourInt int

這裡,MyIntYourInt 都是基于 int 類型定義的。它們的底層類型都是 int。

如果我定義一個函數,我們希望這個函數接受任何底層類型為 int 的類型,我们可以使用 ~ 符號来實現:

func PrintInt[T ~int](t T) {
    fmt.Println(t)
}

使用時

var a int = 5
var b MyInt = 10
var c YourInt = 15

PrintInt(a) 
PrintInt(b) 
PrintInt(c)

在這個例子中,PrintInt 可以接受 int、MyInt 和 YourInt 類型的參數類型,因为它們的底層類型都是 int。

通過使用 ~ 符號,Go的泛型允许你編寫更靈活和通用的程式碼架構,同時保持類型安全。

泛型函数与泛型結構體

泛型函数

Max函數接受a和b只要滿足comparable約束的類型都能作為參數, 並且回傳也是滿足comparable約束的類型.

func Max[T comparable](a, b T) T {
    if a > b {
        return a
    }
    return b
}

泛型結構體

除了常用在函數上, 也支持泛型結構體。

Box 是一個泛型結構體,它有一個Content 屬性,類型為 T。

type Box[T any] struct {
    Content T
}

泛型成員函數

在泛型結構體中,你还可以定義泛型成員方法。

func (b Box[T]) Empty() bool {
    return b.Content == nil
}

Go泛型進階特性

類型列表

類型組合

Go泛型允许使用類型組合,在一個约束中指定多種允許的類型。

type Numeric interface {
    int | float64
}

func Sum[T Numeric](s []T) T {
    var total T
    for _, v := range s {
        total += v
    }
    return total
}

Numeric 約束允许 int 和 float64 類型,使得 Sum 函数能在這兩種類型的slice上進行操作。

多约束

多约束的概念,即一個介面類型需要满足多個介面(Union)。 介面的聯合(Union)。這允許一個介面可以由多個介面組成,一個類型只需要滿足其中任何一個介面即可。

type Serializable interface {
    json.Marshaler | xml.Marshaler
}

Serializable 是一個接口,它由兩個介面組成:json.Marshalerxml.Marshaler。這意味著,任何實現了 json.Marshalerxml.Marshaler 介面的類型都被認為實現了 Serializable 介面。這稱為多約束或介面聯合。

泛型與介面的交互

泛型作为介面的方法

你可以在介面中定義包含泛型的方法。

type Container[T any] interface {
    Add(element T)
    Get(index int) T
}

這裡的 Container 介面可以用於任何類型的容器,如slice、list或自定義容器類型,只要這些容器實現了 Add 和 Get 方法。

使用介面约束泛型

與泛型约束相似,介面也可以用于约束泛型類型。

type HumanLike interface {
    IsHuman() bool
}


func PrintIfHuman[T HumanLike](entity T) {
    if entity.IsHuman() {
        fmt.Println(entity)
    }
}

HumanLike 是一個介面,IsHuman 是它的一個方法。 PrintIfHuman 是一個函數,它接受一個泛型參數 T,並且這個參數被約束為必須滿足 HumanLike 介面。

泛型的常見場景

泛型資料結構

在實際應用中,泛型通常用于實現通用的資料結構,比如鏈表、佇列和堆棧。

type Stack[T any] struct {
    elements []T
}

func (s *Stack[T]) Push(element T) {
    s.elements = append(s.elements, element)
}

func (s *Stack[T]) Pop() T {
    element := s.elements[len(s.elements)-1]
    s.elements = s.elements[:len(s.elements)-1]
    return element
}

Stack[T any] 定義了一個泛型結構體,T 是一個類型參數,可以是任何類型。 elements []T 是一個slice,用于儲存stack中的元素。slice的元素類型是泛型類型 T。

這個泛型stack的實現是類型安全的,意味著如果建立了一個 Stack[int],你只能向其中添加 int 類型的元素,嘗試使用其他類型的元素会在編譯時報錯。這提供了强類型檢查的同時保持了程式碼的靈活性和可重用性。

用于演算法的實現

泛型也在算法的實現中有廣泛被應用,特别是那些不依赖于具體類型的演算法。

func Sort[T Ordered](arr []T) []T {
    // 排序演算法的實現
}

[T Ordered] 是泛型類型参数部分。T 是類型参数,而 Ordered 是對 T 的約束。 Ordered 是Go的一個预先定義好的介面,用于表示類型是可以排序的(即支持 <, <=, >, >= 操作)。 (arr []T) 是函数的参数,表示函数接受一個 T 類型的slice。 []T 是函数回傳類型,表示函数回傳一個 T 類型的slice。

Go泛型實作例子

泛型實作一個简单的array list

定義

一個泛型array list需要能夠進行Add、Delete和Get元素。我们可以使用泛型来定義這樣的資料結構。

type ArrayList[T any] struct {
    items []T
}


func (al *ArrayList[T]) Add(item T) {
    al.items = append(al.items, item)
}

func (al *ArrayList[T]) Get(index int) (T, error) {
    if index < 0 || index >= len(al.items) {
        return zero(T), errors.New("Index out of bounds")
    }
    return al.items[index], nil
}

func (al *ArrayList[T]) Delete(index int) error {
    if index < 0 || index >= len(al.items) {
        return errors.New("Index out of bounds")
    }
    al.items = append(al.items[:index], al.items[index+1:]...)
    return nil
}

使用

有一個ArrayList[int],我们加入整數 1 和 2,然後嘗試取得索引為 1 的元素。

al := &ArrayList[int]{}
al.Add(1)
al.Add(2)
element, err := al.Get(1) // output:element=2, err=nil
err = al.Delete(0) // 删除索引为 0 的元素

使用泛型打造快取元件

定義

快取元件通常需要儲存任意類型的數據並能够在给定的時間内存取它们。我我們可以使用泛型和Go的内建 map 類型来實現。

type Cache[T any] struct {
    store map[string]T
}

func (c *Cache[T]) Set(key string, value T) {
    c.store[key] = value
}

func (c *Cache[T]) Get(key string) (T, bool) {
    value, exists := c.store[key]
    return value, exists
}

使用

c := &Cache[string]{store: make(map[string]string)}
c.Set("name", "John")
value, exists := c.Get("name") // output:value="John", exists=true

泛型實作快速排序

定義

快速排序不依賴于具體的資料類型,因此很適合使用泛型来實現。

func QuickSort[T comparable](arr []T) []T {
    if len(arr) < 2 {
        return arr
    }
    pivot := arr[len(arr)/2]
    var less, pivotList, greater []T
    for _, x := range arr {
        if x < pivot {
            less = append(less, x)
        } else if x > pivot {
            greater = append(greater, x)
        } else {
            pivotList = append(pivotList, x)
        }
    }
    less = QuickSort(less)
    greater = QuickSort(greater)
    // 合併結果
    less = append(less, pivotList...)
    less = append(less, greater...)
    return less
}
  arr := []int{3, 1, 4, 1, 5, 9, 2, 6, 5}
    sortedArr := QuickSort(arr)
    fmt.Println(sortedArr) // output: [1 1 2 3 4 5 5 6 9]

參考Tutorial: Getting started with generics

1.6K views

More from this blog

Claude Code 監控秘錄:OpenTelemetry(OTel/OTLP)實戰指南

稟告主公:此乃司馬懿進呈之兵書,詳解如何以 OpenTelemetry 陣法,令臥龍神算之一舉一動盡在掌握,知糧草消耗、察兵器效能、辨戰報異常,使主公運籌帷幄於大帳之中。 為何需要斥候情報? 司馬懿稟告主公: 臥龍神算(Claude Code)乃當世利器,然若無斥候回報,主公便如蒙眼行軍——兵器耗損幾何、糧草消費幾許、哪路斥候出了差錯,一概不知。臣以為,此乃兵家大忌。 無情報之弊,有四: 軍

Feb 19, 202610 min read163
Claude Code 監控秘錄:OpenTelemetry(OTel/OTLP)實戰指南

工程師的 Claude Code 實戰指南:從零開始到高效開發

工程師的 Claude Code 實戰指南:從零開始到高效開發 本文整合 Anthropic 官方 Best Practices 與社群實戰 Tips,帶你由淺入深掌握 Claude Code。 什麼是 Claude Code?為什麼值得學? 如果你還在用「複製程式碼貼到 ChatGPT,再複製答案貼回去」的工作流程,Claude Code 會讓你大開眼界。 Claude Code 是 Anthropic 推出的命令列工具,它直接活在你的 terminal 裡,能夠讀懂你的整個 codeb...

Feb 18, 20265 min read75
工程師的 Claude Code 實戰指南:從零開始到高效開發

System Design Interview Ch 12 Digital Wallet

確立問題與設計範疇 角色對話內容 面試者我們應該只關注兩個數位錢包之間的餘額轉帳操作嗎?我們是否需要擔心其他功能? 面試官讓我們只關注餘額轉帳操作。 面試者該系統需要支援多少 TPS(每秒交易次數)? 面試官讓我們假設是 1,000,000 TPS (每秒 100 萬次交易)。 面試者數位錢包對正確性有嚴格的要求。我們可以假設事務保證 就足夠了嗎? 面試官聽起來不錯。 面試者我們需要證明正確性嗎? 面試官這是一個很好的問題。正確性(Correctness)通常只有在交...

Feb 2, 202610 min read190
System Design Interview Ch 12 Digital Wallet

Claude Code 利用 Event-Driven Hooks 打造自動化開發大腦

在現代 AI 輔助開發中,我們不僅需要 AI 寫程式,更需要它懂規則、記性好,並且能自動處理那些繁瑣的雜事。透過 Claude Code Hooks 機制,我們可以介入 AI 的思考與執行迴圈,實現真正的「人機協作自動化」。 一、 動機與痛點:為什麼你需要介入 AI 的生命週期? 在預設狀態下,Claude Code 雖然強大,但它是「被動」且「無狀態」的,這導致了開發者常遇到以下痛點: 記憶重置 (Session Amnesia): 痛點:每次重啟終端機,AI 就像失憶一樣。 解法:你...

Jan 24, 20266 min read457
Claude Code 利用 Event-Driven Hooks 打造自動化開發大腦
M

MicroFIRE

71 posts