【Golang】泛型與類型約束

文章目錄

  • 一、環境
  • 二、沒有泛型的Go
  • 三、泛型的優點
  • 四、理解泛型
    • (一)泛型函數(Generic function)
      • 1)定義
      • 2)調用
    • (二)類型約束(Type constraint)
      • 1)接口與約束
      • 2)結構體類型約束
      • 3)類型近似(Type approximations)
    • (三)泛型類型(Generic type)
      • 1)泛型切片
      • 2)泛型結構體
      • 3)泛型接口
    • (五)一些錯誤示例
      • 1)聯合約束中的類型元素限制
      • 2)一般接口只能用于泛型的類型約束
  • 五、參閱

一、環境

Go 1.20.2

二、沒有泛型的Go

假設現在我們需要寫一個函數,實現:
1)輸入一個切片參數,切片類型可以是[]int[]float64,然后將所有元素相加的“和”返回
2)如果是int切片,返回int類型;如果是float64切片,返回float64類型

當然,最簡單的方法是寫兩個函數SumSliceInt(s []int)SumSliceFloat64(s []float64)來分別支持不同類型的切片,但是這樣會導致大部分代碼重復冗余,不是很優雅。那么有沒有辦法只寫一個函數呢?

我們知道,在Go中所有的類型都實現了interface{}接口,所以如果想讓一個變量支持多種數據類型,我們可以將這個變量聲明為interface{}類型,例如var slice interface{},然后使用類型斷言(.(type))來判斷這個變量的類型。

interface{} + 類型斷言:

// any是inerface{}的別名,兩者是完全相同的:type any = interface{}
func SumSlice(slice any) (any, error) {switch s := slice.(type) {case []int:sum := 0for _, v := range s {sum += v}return sum, nilcase []float64:sum := float64(0)for _, v := range s {sum += v}return sum, nildefault:return nil, fmt.Errorf("unsupported slice type: %T", slice)}
}

從上述代碼可見,雖然使用interface{}類型可以實現在同一個函數內支持兩種不同切片類型,但是每個case塊內的代碼仍然是高度相似和重復的,代碼冗余的問題沒有得到根本的解決。

三、泛型的優點

幸運的是,在Go 1.18之后開始支持了泛型(Generics),我們可以使用泛型來解決這個問題:

func SumSlice[T interface{ int | float64 }](slice []T) T {var sum T = 0for _, v := range slice {sum += v}return sum
}

是不是簡潔了很多?而且,泛型相比interface{}還有以下優勢:

  • 可復用性:提高了代碼的可復用性,減少代碼冗余。
  • 類型安全性:泛型在編譯時就會進行類型安全檢查,可以確保編譯出來的代碼就是類型安全的;而interface{}是在運行時才進行類型判斷,如果編寫的代碼在類型判斷上有bug或缺漏,就會導致Go在運行過程中報錯。
  • 性能:不同類型的數據在賦值給interface{}變量時,會有一個隱式的裝箱操作,從interface{}取數據時也會有一個隱式的拆箱操作,而泛型就不存在裝箱拆箱過程,沒有額外的性能開銷。

四、理解泛型

(一)泛型函數(Generic function)

1)定義

編寫一個函數,輸入ab兩個泛型參數,返回它們的和:

// T的名字可以更改,改成K、V、MM之類的都可以,只是一般比較常用的是T
// 這是一個不完整的錯誤例子
func Sum(a, b T) T {return a + b
}

大寫字母T的名字叫類型形參(Type parameter),代表ab參數是泛型,可以接受多種類型,但具體可以接受哪些類型呢?在上面的定義中并沒有給出這部分信息,要知道,并不是所有的類型都可以相加的,因此這里就引出了約束的概念,我們需要對T可以接受的類型范圍作出約束:

// 正確例子
func Sum[T interface{ int | float64 }](a, b T) T {return a + b
}

中括號[]之間的空間用于定義類型形參,支持定義一個或多個

  • T:類型形參的名字
  • interface{ int | float64 }:對T的類型約束(Type Constraint),必須是一個接口,約束T只可以是intfloat64

為了簡化寫法,類型約束中的interface{}某些情況下是可以省略的,所以可以簡寫成:

func Sum[T int | float64](a, b T) T {return a + b
}

interface{}不能省略的一些情況:

// 當接口中包含方法時,不能省略
func Contains[T interface{ Equal() bool }](num T) {
}

可以定義多個類型形參:

func Add[T int, E float64](a T, b E) E {return E(a) + b
}

2)調用

以上面的Sum泛型函數為例,完整的調用寫法為:

Sum[int](1, 2)
Sum[float64](1.1, 2.2)

[]之間的內容稱為類型實參(Type argument),是函數定義中的類型形參T的實際值,例如傳int過去,那么T的實際值就是int

類型形參確定為具體類型的過程稱為實例化(Instantiations),可以簡單理解為將函數定義中的T替換為具體類型:
在這里插入圖片描述
泛型函數實例化后,就可以像普通函數那樣調用了。

大多數時候,編譯器都可以自動推導出該具體類型,無需我們主動告知,這個功能叫函數實參類型推導(Function argument type inference)。所以可以簡寫成:

// 簡寫,跟調用普通函數一樣的寫法
Sum(1, 2)
Sum(1.1, 2.2)

需要注意的是,在調用這個函數時,ab兩個參數的類型必須一致,要么兩個都是int,要么都是float64,不能一個是int一個是float64

Sum(1, 2.3) // 編譯會報錯

什么時候不能簡寫?

// 當類型形參T僅用在返回值,沒有用在函數參數列表時
func Foo[T int | float64]() T {return 1
}
Foo() // 報錯:cannot infer T
Foo[int]() // OK
Foo[float64]() // OK

(二)類型約束(Type constraint)

1)接口與約束

Go 使用interface定義類型約束。我們知道,在引入泛型之前,interface中只可以聲明一組未實現的方法,或者內嵌其它interface,例如:

// 普通接口
type Driver interface {SetName(name string) (int, error)GetName() string
}// 內嵌接口
type ReaderStringer interface {io.Readerfmt.Stringer
}

接口里的所有方法稱之為方法集(Method set)

引入泛型之后,interface里面可以聲明的元素豐富了很多,可以是任何 Go 類型,除了方法、接口以外,還可以是基本類型,甚至struct結構體都可以,接口里的這些元素稱為類型集(Type set)

// 基本類型約束
type MyInt interface {int
}// 結構體類型約束
type Point interface {struct{ X, Y int }
}// 內嵌其它約束
type MyNumber interface {MyInt
}// 聯合(Unions)類型約束,不同類型元素之間是“或”的關系
// 如果元素是一個接口,這個接口不能包含任何方法!
type MyFloat interface {float32 | float64
}

有了豐富的類型集支持,我們就可以更加方便的使用接口對類型形參T的類型作出約束,既可以約束為基本類型(intfloat32string…),也可以約束它必須實現一組方法,靈活性大大增加。

因此前面的Sum函數還可以改寫成:

// 原始例子:
// func Sum[T int | float64](a, b T) T {
//	 return a + b
// }type MyNumber interface {int | float64
}func Sum[T MyNumber](a, b T) T {return a + b
}

2)結構體類型約束

Go 還允許我們使用復合類型字面量來定義約束。例如,我們可以定義一個約束,類型元素是一個具有特定結構的struct

type Point interface {struct{ X, Y int }
}

然而,需要注意的是,雖然我們可以編寫受此類結構體類型約束的泛型函數,但在當前版本的 Go 中,函數無法訪問結構體的字段,例如:

func GetX[T Point](p T) int {return p.X  // p.X undefined (type T has no field or method X)
}

3)類型近似(Type approximations)

我們知道,在Go中可以創建新的類型,例如:

type MyString string

MyString是一個新的類型,底層類型是string

在類型約束中,有時候我們可能并不關心上層類型,只要底層類型符合要求就可以,這時候就可以使用類型近似符號:~

// 創建新類型
type MyString string// 定義類型約束
type AnyStr interface {~string
}// 定義泛型函數
func Foo[T AnyStr](param T) T {return param
}func main() {var p1 string = "aaa"var p2 MyString = "bbb"Foo(p1)Foo(p2) // 雖然p2是MyString類型,但也可以通過泛型函數的類型約束檢查
}

需要注意的是,類型近似中的類型,必須是底層類型,而且不能是接口類型:

type MyInt inttype I0 interface {~MyInt // 錯誤! MyInt不是底層類型, int才是~error // 錯誤! error是接口
}

(三)泛型類型(Generic type)

1)泛型切片

假設現在有一個IntSlice類型:

type IntSlice []intvar s1 IntSlice = []int{1, 2, 3} // 正常
var s2 IntSlice = []string{"a", "b", "c"} // 報錯,因為IntSlice底層類型是[]int,字符串無法賦值

很顯然,因為類型不一致,s2是無法賦值的,如果想要支持其它類型,需要定義新類型:

type StringSlice []string
type Float32Slice []float32
type Float64Slice []float64
// ...

但是這樣做的問題也顯而易見,它們結構都是一樣的,只是元素類型不同就需要重新定義這么多新類型,導致代碼復雜度增加。

這時候就可以用泛型類型來解決這個問題:

// 只需定義一種新類型,就可以同時支持[]int/[]string/[]float32多種切片類型
// 新類型的名字叫 MySlice[T]
type MySlice[T int|string|float32] []T

類型定義中帶 類型形參 的類型,稱之為泛型類型(Generic type)

泛型切片的初始化:

var s1 MySlice[int] = MySlice[int]{1, 2, 3}
var s2 MySlice[string] = MySlice[string]{"a", "b", "c"}
s3 := MySlice[string]{"a", "b", "c"} // 簡寫

其它一些例子:

// 泛型Map
type MyMap[K int | string, V any] map[K]Vvar m1 MyMap[string, int] = MyMap[string, int]{"a": 1, "b": 2} // 完整寫法
m2 := MyMap[int, string]{1: "a", 2: "b"} // 簡寫// 泛型通道
type MyChan[T int | float32] chan Tvar c1 MyChan[int] = make(MyChan[int]) // 完整寫法
c2 := make(MyChan[float32]) // 簡寫

2)泛型結構體

假設現在要創建一個struct結構體,里面含有一個data泛型屬性,類型是一個intfloat64的切片:

type List[T int | float64] struct {data []T
}

給這個結構體增加一個Sum方法,用于對切片求和:

func (l *List[T]) Sum() T {var sum Tfor _, v := range l.data {sum += v}return sum
}

實例化結構體,并調用Sum方法:

// var list *List[int] = &List[int]{data: []int{1, 2, 3}} // 完整寫法
list := &List[int]{data: []int{1, 2, 3}}
sum := list.Sum()
fmt.Println(sum) // 輸出:6

3)泛型接口

泛型也可以用在接口上:

type Human[T float32] interface {GetWeight() T
}

假設現在有兩個結構體,它們都有GetWeight()方法,哪個結構體實現了上面Human[T]接口?

// 結構體1
type Person1 struct {Name string
}
func (p Person1) GetWeight() float32 {return 66.6
}// 結構體2
type Person2 struct {Name string
}
func (p Person2) GetWeight() int {return 66
}

注意觀察兩個GetWeight()方法的返回值類型,因為我們在Human[T]接口中約束了T的類型只能是float32,而只有Person1結構體的返回值類型符合約束,所以實際上只有Person1結構體實現了Human[T]接口。

p1 := Person1{Name: "Tim"}
var iface1 Human[float32] = p1 // 正常,因為Person1實現了接口,所以可以賦值成功p2 := Person2{Name: "Tim"}
var iface2 Human[float32] = p2 // 報錯,因為Person2沒有實現接口

(五)一些錯誤示例

下面列出一些錯誤使用泛型的例子。

1)聯合約束中的類型元素限制

聯合約束中的類型元素不能是包含方法的接口:

// 錯誤
type ReaderStringer interface {io.Reader | fmt.Stringer // 錯誤,io.Reader和fmt.Stringer是包含方法的接口
}// 正確
type MyInt interface {int
}
type MyFloat interface {float32
}
type MyNumber interface {MyInt | MyFloat // 正確,MyInt和MyFloat接口里面沒有包含方法
}

聯合約束中的類型元素不能含有comparable接口:

type Number interface {comparable | int // 含有comparable,報錯
}

2)一般接口只能用于泛型的類型約束

先解釋下相關概念,引入泛型后,Go的接口分為兩種類型:

  • 基本接口(Basic interface)
    只包含方法的接口,稱為基本接口,其實就是引入泛型之前的那種傳統接口。
  • 一般接口(General interface)
    由于引入泛型后,接口可以定義的元素大大豐富,如果一個接口里含有除了方法以外的元素,那么這個接口就稱為一般接口

一般接口只能用于泛型的類型約束,不能用于變量、函數參數、返回值的類型聲明,而基本接口則沒有此限制:

type NoMethods interface {int
}// 錯誤,不能用于函數參數列表、返回值
func Foo(param NoMethods) NoMethods {return param
}// 錯誤,不能用來聲明變量的類型
var param NoMethods// 正確
func Foo[T NoMethods](param T) T {return param
}

五、參閱

  • Go泛型全面講解:一篇講清泛型的全部
  • Golang泛型
  • An Introduction To Generics

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/74319.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/74319.shtml
英文地址,請注明出處:http://en.pswp.cn/web/74319.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

k8s常用總結

1. Kubernetes 架構概覽 主節點(Master): 負責集群管理,包括 API Server、Controller Manager、Scheduler 和 etcd 存儲。 工作節點(Node): 運行 Pod 和容器,包含 kubelet、kube-pr…

Android 單例模式全解析:從基礎實現到最佳實踐

單例模式(Singleton Pattern)是軟件開發中常用的設計模式,其核心是確保一個類在全局范圍內只有一個實例,并提供全局訪問點。在 Android 開發中,單例模式常用于管理全局資源(如網絡管理器、數據庫助手、配置…

ffmpeg濾鏡使用

ffmpeg實現畫中畫效果 FFmpeg中,可以通過overlay將多個視頻流、多個多媒體采集設備、多個視頻文件合并到一個界面中,生成畫中畫的效果 FFmpeg 濾鏡 overlay 基本參數 x和y x坐標和Y坐標 eof action 遇到 eof表示時的處理方式,默認為重復。…

OpenAI即將開源!DeepSeek“逼宮”下,AI爭奪戰將走向何方?

OpenAI 終于要 Open 了。 北京時間 4 月 1 日凌晨,OpenAI 正式宣布:將在未來幾個月內開源一款具備推理能力的語言模型,并開放訓練權重參數。這是自 2019 年 GPT-2 部分開源以來,OpenAI 首次向公眾開放核心模型技術。 【圖片來源于…

貪心算法,其優缺點是什么?

什么是貪心算法? 貪心算法(Greedy Algorithm)是一種在每一步選擇中都采取在當前狀態下最優(局部最優)的選擇,從而希望導致全局最優解的算法策略。 它不像動態規劃那樣考慮所有可能的子問題,而是做出局部最優選擇,依賴這些選擇來…

python string 類型字符拼接 +=的缺點,以及取代方法

在Python中,使用進行字符串拼接雖然語法簡單,但在性能和代碼維護方面存在明顯缺陷。以下是詳細分析及替代方案: 一、的缺點 性能低下 內存分配問題:字符串在Python中不可變,每次操作會創建新字符串對象,導…

web前端開發-JS

web前端開發-JS 什么是JavaScript Web標準也稱網頁標準,由一系列的標準組成,大部分由W3C(World Wide Web Consortium,萬維網聯盟)負責制定。三個組成部分: HTML:負責網頁的結構(頁面元素和內容)。CSS:負責網頁的表現(頁面元素的外觀、位置等頁面樣式,如:顏色、大小等)。JavaS…

Turtle綜合案例實戰(繪制復雜圖形、小游戲)

在學習了 Turtle 基本的繪圖技巧后,我們可以通過結合多個概念和技巧,繪制復雜的圖形或實現簡單的小游戲。本章將介紹兩個實戰案例: 繪制復雜圖形:結合前面所學的知識,繪制一個精美的多層次復雜圖案。簡單的游戲:利用 Turtle 實現一個簡單的小游戲——蛇形游戲,這是一個經…

Python設計模式:克隆模式

1. 什么是克隆模式 克隆模式的核心思想是通過復制一個已有的對象(原型)來創建一個新的對象(克隆)。這種方式可以避免重復的初始化過程,從而提高效率。克隆模式通常涉及以下幾個方面: 原型對象&#xff1a…

邏輯漏洞之越權訪問總結

什么是越權訪問漏洞? “越權訪問漏洞” 是 “邏輯漏洞” 的一種,是由于網站系統的權限校驗的邏輯不夠嚴謹,沒有對用戶權限進行嚴格的身份鑒別,導致普通權限的用戶做到了其它普通用戶或管理員才能完成的操作,稱之為“越…

超短波通信模擬設備:增強通信能力的關鍵工具

在全球信息化戰爭的背景下,通信系統扮演著至關重要的角色。為確保通信系統的穩定性和抗干擾能力,超短波通信模擬設備應運而生,為軍事訓練和通信干擾任務提供強大的支持。 設備特點及優勢 便攜性:設備體積小、重量輕,…

C++STL——容器-vector(含部分模擬實現,即地層實現原理)(含迭代器失效問題)

目錄 容器——vector 1.構造 模擬實現 2.迭代器 模擬實現: ?編輯 3.容量 模擬實現: 4.元素的訪問 模擬實現 5.元素的增刪查改 迭代器失效問題: 思考問題 【注】:這里的模擬實現所寫的參數以及返回值,都是…

Ubuntu交叉編譯器工具鏈安裝

聲明 本博客所記錄的關于正點原子i.MX6ULL開發板的學習筆記,(內容參照正點原子I.MX6U嵌入式linux驅動開發指南,可在正點原子官方獲取正點原子Linux開發板 — 正點原子資料下載中心 1.0.0 文檔),旨在如實記錄我在學校學…

Tomcat 部署 Jenkins.war 詳細教程(含常見問題解決)

在Tomcat中部署Jenkins.war文件是一個相對簡單的過程,以下是詳細步驟: 1. 準備工作 確保已安裝JDK:Jenkins需要Java環境,建議安裝JDK 8或更高版本。 下載Jenkins.war:https://pan.quark.cn/s/c4fd7711a1b3 下載Tomc…

DAY46 動態規劃Ⅸ 股票問題Ⅱ

188. 買賣股票的最佳時機 IV - 力扣&#xff08;LeetCode&#xff09; class Solution { public:int maxProfit(int k, vector<int>& prices) {if(prices.size()0) return 0;vector<vector<int>>dp(prices.size(),vector<int>(2*k1,0));for(int i…

4月2日工作日志

一個樸實無華的目錄 今日學習內容&#xff1a;1.UIAbility生命周期2.默認啟動頁面設置3.同模塊喚起ability 今日實操內容&#xff1a; 今日學習內容&#xff1a; 1.UIAbility生命周期 2.默認啟動頁面設置 3.同模塊喚起ability 今日實操內容&#xff1a; 通過分組件文件&#…

鴻蒙學習筆記(4)-Radio組件、彈框組件、組件內部狀態、工具類

一、Radio組件 &#xff08;1&#xff09;簡述 創建單選框組件。接收一個RadioOptions類型對象參數。 名稱類型必填說明valuestring是 當前單選框的值。 groupstring是 當前單選框的所屬群組名稱&#xff0c;相同group的Radio只能有一個被選中。 indicatorType12RadioIndica…

111.在 Vue 3 中使用 OpenLayers 實現動態曲線流動圖(類似 ECharts 遷徙狀態)

在數據可視化領域&#xff0c;ECharts 提供的 遷徙圖&#xff08;流動圖&#xff09; 是一種直觀展示數據流動的方式&#xff0c;如人口遷徙、物流流向等。我們可以使用 OpenLayers 結合 Vue 3 來實現類似的 動態曲線流動圖&#xff0c;從而在 Web GIS 項目中提供更生動的可視化…

全棧開發項目實戰——AI智能聊天機器人

文章目錄 一&#xff1a;項目技術棧和代碼分析1.前端技術棧&#xff08;1&#xff09;HTML&#xff08;index.html&#xff09;&#xff1a;&#xff08;2&#xff09;CSS&#xff08;styles.css&#xff09;&#xff1a;&#xff08;3&#xff09;JavaScript&#xff08;scrip…

無人機機體結構設計要點與難點!

一、無人機機體結構設計要點 1. 類型與應用場景匹配 固定翼無人機&#xff1a;需優化機翼升阻比&#xff0c;采用流線型機身降低氣動阻力&#xff08;如大展弦比機翼設計&#xff09;。 多旋翼無人機&#xff1a;注重輕量化框架和對稱布局&#xff08;如四軸/六軸碳纖維機…