【Golang】合理运用泛型,简化开发流程
✨✨ 欢迎大家来到景天科技苑✨✨
🎈🎈 养成好习惯,先赞后看哦~🎈🎈
🏆 作者简介:景天科技苑
🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。
🏆《博客》:Python全栈,Golang开发,PyQt5和Tkinter桌面开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi,flask等框架,云原生K8S,linux,shell脚本等实操经验,网站搭建,数据库等分享。所属的专栏:Go语言开发零基础到高阶实战
景天的主页:景天科技苑
Go语言中的泛型
一、泛型介绍
Go语言自1.18版本开始引入了泛型支持,这一特性极大地增强了代码的灵活性和重用性。
泛型通过引入类型参数,使得函数、类型和方法能够处理多种数据类型,从而减少了重复代码,提高了开发效率。本文将详细介绍Go语言中泛型的用法,并通过实际案例展示泛型在编程中的具体应用。
泛型:多种不确定的类型
在泛型之前,我们定义一个变量或函数参数类型,类型都是写死的,比如:
var name string
func test(name string){}
这样,功能就比较受限。
如果我们想要编写一个反转切片的函数,需要为每种类型分别编写一个函数。例如,反转整型切片和反转浮点型切片的函数需要分别实现。但是,从Go 1.18开始,我们可以使用泛型来实现一个通用的反转函数。
我们先用空接口来测试多种类型的适配实现,还是需要做类型判断
package main
import "fmt"
func main() {
str := []string{"1", "2", "3", "4", "5"}
printSlice(str)
str2 := []int{1, 2, 3, 4, 5}
printSlice(str2)
}
func printSlice(i interface{}) {
// 断言 x.(T), 如果x实现了T,那么就将 x转换为T类型
//这样的话,也是需要做好多种判断
switch i.(type) {
case []string:
for _, i3 := range i.([]string) {
fmt.Println(i3)
}
fmt.Println()
case []int:
for _, i3 := range i.([]int) {
fmt.Println(i3)
}
}
// 其他很多种类型都要做判断,所以通过这种方式不现实.........
}
这种情况,传递的参数进来,需要自己做N种判断来进行适配。
也可以通过反射来实现,在go1.18之后,我们最常用的还是泛型
泛型的作用:
1、减少重复性的代码,提高安全性
- 针对不同的类型,写了相同逻辑的代码,我们就可以通过泛型来简化代码!
2、在1.18版本之前 反射 来实现。 泛型并不能完全取代反射!
二、泛型的基本概念
不限定参数的类型,让调用的人自己去定义类型。
类型参数:类型参数是在函数或类型定义时,紧随名称后的方括号中定义的。例如,func T any {…}中的[T any]表示类型参数T可以是任何类型。
约束:约束是指类型参数必须满足的接口。例如,any是一个内置约束,表示任何类型。通过约束,可以限制类型参数的范围,确保它们具备某些特定的行为或属性。
三、泛型类型的声明和使用
1. 声明泛型类型:使用[]括起方括号来声明泛型类型。例如:
type List[T any] []T
这里,List[T any]表示一个泛型类型,其中T是类型参数,可以是任何类型。any是约束, 是Go语言中的空接口,表示 T 可以是任何类型。
T 说白了就是一个占位符,类型的形式参数,T是不确定的,需要在使用的时候进行传递。也可以随便用个其他字母代替
2. 使用泛型类型:在使用泛型类型时,需要指定类型参数。
T是占位符,在使用的时候,必须要实例化为具体的类型。
声明泛型类型后,可以像普通类型一样使用它。例如:
myIntList := List[int]{1, 2, 3}
这里,myIntList是一个整型列表。
3. 泛型类型的应用:泛型类型可以应用于结构体、接口、函数和方法中。
例如,定义一个泛型结构体:
type Box[T any] struct {
content T
}
func (b Box[T]) Content() T {
return b.content
}
使用:
box := Box[int]{content: 123}
fmt.Println(box.Content()) // 输出: 123
泛型使用案例1:
普通的定义类型,这个类型只能代表本身一个,泛型类型,我们可以实现,参数类型传递
package main
import "fmt"
// 我们定义的结构都是一样的,只是它的类型不同,就需要重新定义这么多的类型。
// 思考:是否有一种机制,只定义一个类型就可以代表上面的所有类型?
// 泛型:类型 参数化了! 参数:人为传递的
/*
1、T 说白了就是一个占位符,类型的形式参数,T是不确定的,需要在使用的时候进行传递。
2、由于T类型是不确定的,我们需要加一些约束 int|float64|float32 。告诉编译器我这个T,只接受
int、float64、float32 类型
3、我们这里定义的类型是什么?Slice[T]
*/
// Slice 这种类型的定义方式,带了类型形参,和普通定义类型就完全不同的。
// 普通的定义类型,这个类型只能代表本身一个,泛型类型,我们可以实现,参数类型传递。
// 我们可以在使用的时候来定义类型。
// 语法糖:简化开发
// T后面是约束,约束用户只能用哪些类型
// 创建个不固定类型的切片类型Slice
type Slice[T int | float64 | float32] []T
func main() {
//使用的时候,必须要实例化为具体的类型
// Slice是我们上面自己定义的类型,中括号里面是约束,在使用时要用具体的类型
var a Slice[int] = []int{1, 2, 3}
fmt.Printf("%T\n", a) // Slice[int]
var b Slice[float64] = []float64{1.0, 2.0, 3.0}
fmt.Printf("%T\n", b) // Slice[float64]
// 不能够赋值(string 不在T的约束当中,不能实例化的)
//var c Slice[string] = []string{"jigntian","xxx"}
// T是占位符,在使用的时候,必须要实例化为具体的类型。
//var d Slice[T] = []int{1,2,3}
}
泛型使用案例2:
泛型可以用在所有有类型的地方
package main
import "fmt"
// 泛型可以用在所有有类型的地方
type MyStruct[T int | string] struct {
Id T
Name string
}
type IprintData[T int | float64 | string] interface {
Print(data T)
}
// MyChan 通道
type MyChan[T int | string | float64] chan T
func main() {
//T 泛型的参数类型的属性可以远不止一个,所有东西都可以泛型化。不一定非得用T 其他字母也可以
// map(int)string
// map[KEY]VALUE 类型形参(参数是不确定) KEY 、VALUE
// KEY int | string , VALUE float32 | float64 约束
// 类型的名字 MyMap[KEY,VALUE], 通过这一个类型,来代表多个类型!--> 泛型
//map泛型就是多个参数 KEY VALUE
type MyMap[KEY int | string | float64, VALUE float32 | float64 | int | string] map[KEY]VALUE
//定义了泛型后,就可以实例化出不同类型的map
// map [string]float64
var score MyMap[string, float64] = map[string]float64{
"go": 9.9,
"java": 8.0,
}
fmt.Println(score)
// map [int]string
var rank MyMap[int, string] = map[int]string{
1: "张三",
2: "王五",
}
fmt.Println(rank)
}
4. 特殊的泛型
package main
import "fmt"
func main() {
// 特殊的泛型类型,泛型的参数时多样的,但是实际类型定义就是int
type AAA[T int | string] int
var a AAA[int] = 123
var b AAA[string] = 123
fmt.Println(a)
fmt.Println(b)
//查看类型
fmt.Printf("%T\n", a)
fmt.Printf("%T\n", b)
//var c AAA[string] = "hello" //不能这样赋值,因为AAA的值约束的类型是int
}
这里虽然使用了泛型。但是底层类型就是int,所以传int和string都可以的,但是赋值,只能是int
四、泛型函数和方法的定义和使用
1. 定义泛型函数:
单纯的泛型没啥意义。和函数结合使用, 可以使用调用者(调用者的类型可以自定义,就可以实现泛型。)
带了类型形参的函数就叫做泛型函数,极大的提高代码的灵活心,降低阅读性!
泛型函数可以处理多种类型的数据。例如,定义一个交换两个值的泛型函数:
func Swap[T any](a, b T) (T, T) {
return b, a
}
使用:
package main
import "fmt"
func Swap[T any](a, b T) (T, T) {
return b, a
}
func main() {
a, b := Swap[int](1, 2) // 显式指定类型
fmt.Println(a, b) // 输出: 2 1
c, d := Swap("hello", "world") // 隐式类型推断,不用写类型
fmt.Println(c, d) // 输出: world hello
}
泛型可以增加代码的灵活性,降低了可读性!
Go的泛型语法糖:自动推导 (本质,就是编译器帮我们加上去了,在实际运行,这里T还是加上去的)
这种带了类型形参的函数就叫做泛型函数,极大的提高代码的灵活心,降低阅读性!
package main
import (
"fmt"
)
func main() {
var a int = 1
var b int = 2
fmt.Println(Add[int](a, b))
var c float32 = 1.1
var d float32 = 2.2
fmt.Println(Add[float32](c, d))
// 每次都去写T的类型是很麻烦的,支持自动推导!
// Go的泛型语法糖:自动推导 (本质,就是编译器帮我们加上去了,在实际运行,这里T还是加上去的)
fmt.Println(Add(a, b)) // T : int
fmt.Println(Add(c, d)) // T : float32
}
// Add 真正的Add实现,传递不同的参数都是可以适配的! Add[T] T在调用的时候需要实例化
// 这种带了类型形参的函数就叫做泛型函数,极大的提高代码的灵活心,降低阅读性!
func Add[T int | float32 | float64](a T, b T) T {
return a + b
}
2. 定义泛型方法:
泛型方法可以应用于泛型类型上。
针对不同类型的切片做累加和,使用泛型比较简便
package main
import (
"fmt"
)
// MySlice 定义一个泛型切片
type MySlice[T int | float32 | int64] []T
func main() {
//针对不同类型的切片都可以计算
var s MySlice[int] = []int{1, 2, 3, 4}
fmt.Println(s.sum())
var s1 MySlice[float32] = []float32{1.0, 2.0, 3.0, 4.4}
fmt.Println(s1.sum())
}
// 调用者,类型是不确定的,用户传什么,她就实例化什么。 类型参数化了 , 泛型
// 没有泛型之前, 反射: reflect.ValueOf().Kind() , 也需要很多if,本质是逻辑相同的,只是类型不同!
func (s MySlice[T]) sum() T {
var sum T
for _, v := range s {
sum += v
}
return sum
}
五、泛型约束的使用
1. 内置约束:
Go语言提供了几个内置约束,包括any(表示任何类型)和comparable(表示可以比较的类型)。
any经常用,就是一个泛型,表示了go所有的内置类型。interface{} 这里就不做太多赘述了。
comparable是一个接口,所有可比较的类型都实现了这个接口。
可比较的类型包括布尔型、数值型、字符串、指针、通道(channel)、可比较类型的数组以及所有字段都是可比较类型的结构体等。
comparable仅能用于泛型中的类型限定(type constraint),不可作为变量的类型。
看下源码
在使用comparable作为类型约束时,需要确保类型参数确实支持比较操作,否则会导致编译错误。
comparable不支持大小比较操作(如<、<=、>、>=),仅支持相等性比较(==、!=)。
结构体类型可以作为comparable使用,但前提是其所有字段都是comparable的。如果结构体包含不可比较的字段(如切片、映射类型),则整个结构体类型也不可比较。
搜索算法:
泛型也可以用于实现通用的搜索算法。例如,定义一个泛型函数来查找切片中的元素
package main
import "fmt"
func Find[T comparable](slice []T, value T) (int, bool) {
for i, v := range slice {
if v == value {
return i, true
}
}
return -1, false
}
func main() {
numbers := []int{1, 2, 3, 4, 5}
index, found := Find(numbers, 3)
if found {
fmt.Println("Element found at index:", index) // 输出: Element found at index: 2
} else {
fmt.Println("Element not found")
}
}
2. 自定义约束
由于约束有时候很多,我们可以定义一些自己的泛型约束(本质是一个接口)
除了内置约束外,还可以使用自定义接口作为约束条件。例如,定义一个支持加法操作的泛型函数:
package main
import "fmt"
// MyInt 泛型的约束提取定义
type MyInt interface {
int | float64 | int8 | int32 // 作用域泛型的,而不是一个接口方法 这样泛型约束可以用着集中类型都可以
}
// 自定义泛型
func main() {
var a int = 10
var b int = 20
fmt.Println(GetMaxNum(a, b))
}
// GetMaxNum 函数里面泛型T 用自己定义的约束MyInt
func GetMaxNum[T MyInt](a, b T) T {
if a > b {
return a
}
return b
}
3. 支持泛型衍生类型
新符号 ~,和类型一起出现的,表示支持该类型的衍生类型!
衍生类型,就是根据该类型常见的类型
package main
import "fmt"
// int8 衍生类型
type int8A int8
type int8B = int8
// NewInt ~ 表示可以匹配该类型的衍生类型
type NewInt interface {
~int8
}
// ~
func main() {
var a int8A = 8
var b int8A = 56
fmt.Println(GetMax(a, b))
}
func GetMax[T NewInt](a, b T) T {
if a > b {
return a
}
return b
}
六、泛型在实际项目中的应用案例
-
日志库:在日志库中,可以使用泛型来创建通用的日志记录器,支持多种日志级别和数据类型。例如,定义一个泛型日志记录器接口,并为不同类型的日志消息实现该接口。
-
数据库ORM:在数据库ORM框架中,泛型可以用于实现通用的查询和更新操作,支持多种数据类型和数据库表结构。通过定义泛型接口和方法,可以简化数据库操作的代码,提高开发效率。
-
网络框架:在网络框架中,泛型可以用于实现通用的请求处理函数和中间件,支持多种请求和响应类型。通过定义泛型类型和接口,可以方便地扩展和定制网络框架的功能。
七、泛型使用的注意事项
-
性能问题:虽然泛型提高了代码的灵活性和重用性,但在某些情况下可能会引入性能开销。因此,在使用泛型时需要权衡性能与代码可读性之间的关系。
-
类型推断:Go语言的类型推断机制在大多数情况下能够正确地推断出泛型类型参数的类型,但在某些复杂情况下可能会出现推断失败的情况。因此,在编写泛型代码时需要注意类型推断的限制和规则。
-
约束条件:在使用泛型时,需要为类型参数指定适当的约束条件,以确保它们具备必要的属性和行为。然而,由于Go语言的接口机制相对较为简单,某些复杂的约束条件可能无法通过接口来实现。因此,在定义和使用泛型时需要仔细考虑约束条件的合理性和可行性。
八、总结
Go语言的泛型特性为开发者提供了更加灵活和强大的编程工具,使得代码更加简洁、可读和可重用。通过掌握泛型的基本概念、用法和注意事项,开发者可以更好地利用泛型来优化代码结构和提高开发效率。同时,也需要注意泛型可能带来的性能开销和类型推断限制等问题,以确保代码的正确性和稳定性。
以上是关于Go语言中泛型用法的详细介绍和教程。希望本文能够帮助读者更好地理解和掌握Go语言的泛型特性,并在实际项目中灵活运用泛型来提高代码质量和开发效率。