南宁东凯做网站的公司,wordpress 企业 下载,手机个别网页打不开,下载小程序到微信✨✨ 欢迎大家来到景天科技苑✨✨
#x1f388;#x1f388; 养成好习惯#xff0c;先赞后看哦~#x1f388;#x1f388; #x1f3c6; 作者简介#xff1a;景天科技苑 #x1f3c6;《头衔》#xff1a;大厂架构师#xff0c;华为云开发者社区专家博主#xff0c;…
✨✨ 欢迎大家来到景天科技苑✨✨ 养成好习惯先赞后看哦~ 作者简介景天科技苑 《头衔》大厂架构师华为云开发者社区专家博主阿里云开发者社区专家博主CSDN全栈领域优质创作者掘金优秀博主51CTO博客专家等。 《博客》Python全栈Golang开发PyQt5和Tkinter桌面开发小程序开发人工智能js逆向App逆向网络系统安全数据分析Djangofastapiflask等框架云原生K8Slinuxshell脚本等实操经验网站搭建数据库等分享。 所属的专栏Go语言开发零基础到高阶实战 景天的主页景天科技苑 Go语言中的泛型
一、泛型介绍
Go语言自1.18版本开始引入了泛型支持这一特性极大地增强了代码的灵活性和重用性。 泛型通过引入类型参数使得函数、类型和方法能够处理多种数据类型从而减少了重复代码提高了开发效率。本文将详细介绍Go语言中泛型的用法并通过实际案例展示泛型在编程中的具体应用。
泛型多种不确定的类型
在泛型之前我们定义一个变量或函数参数类型类型都是写死的比如
var name string
func test(name string){}这样功能就比较受限。 如果我们想要编写一个反转切片的函数需要为每种类型分别编写一个函数。例如反转整型切片和反转浮点型切片的函数需要分别实现。但是从Go 1.18开始我们可以使用泛型来实现一个通用的反转函数。
我们先用空接口来测试多种类型的适配实现还是需要做类型判断
package mainimport fmtfunc 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 mainimport 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] []Tfunc 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 mainimport fmt// 泛型可以用在所有有类型的地方type MyStruct[T int | string] struct {Id TName string
}type IprintData[T int | float64 | string] interface {Print(data T)
}// MyChan 通道
type MyChan[T int | string | float64] chan Tfunc main() {//T 泛型的参数类型的属性可以远不止一个所有东西都可以泛型化。不一定非得用T 其他字母也可以// map(int)string// map[KEY]VALUE 类型形参参数是不确定 KEY 、VALUE// KEY int | string VALUE float32 | float64 约束// 类型的名字 MyMap[KEY,VALUE], 通过这一个类型来代表多个类型-- 泛型//map泛型就是多个参数 KEY VALUEtype MyMap[KEY int | string | float64, VALUE float32 | float64 | int | string] map[KEY]VALUE//定义了泛型后就可以实例化出不同类型的map// map [string]float64var score MyMap[string, float64] map[string]float64{go: 9.9,java: 8.0,}fmt.Println(score)// map [int]stringvar rank MyMap[int, string] map[int]string{1: 张三,2: 王五,}fmt.Println(rank)
}4. 特殊的泛型
package mainimport fmtfunc main() {// 特殊的泛型类型泛型的参数时多样的但是实际类型定义就是inttype AAA[T int | string] intvar a AAA[int] 123var b AAA[string] 123fmt.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 mainimport fmtfunc Swap[T any](a, b T) (T, T) {return b, a
}func main() {a, b : Swap[int](1, 2) // 显式指定类型fmt.Println(a, b) // 输出: 2 1c, d : Swap(hello, world) // 隐式类型推断,不用写类型fmt.Println(c, d) // 输出: world hello
}泛型可以增加代码的灵活性降低了可读性 Go的泛型语法糖自动推导 本质就是编译器帮我们加上去了在实际运行这里T还是加上去的 这种带了类型形参的函数就叫做泛型函数极大的提高代码的灵活心降低阅读性
package mainimport (fmt
)func main() {var a int 1var b int 2fmt.Println(Add[int](a, b))var c float32 1.1var d float32 2.2fmt.Println(Add[float32](c, d))// 每次都去写T的类型是很麻烦的支持自动推导// Go的泛型语法糖自动推导 本质就是编译器帮我们加上去了在实际运行这里T还是加上去的fmt.Println(Add(a, b)) // T : intfmt.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 mainimport (fmt
)// MySlice 定义一个泛型切片
type MySlice[T int | float32 | int64] []Tfunc 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 Tfor _, v : range s {sum v}return sum
}五、泛型约束的使用
1. 内置约束
Go语言提供了几个内置约束包括any表示任何类型和comparable表示可以比较的类型。 any经常用就是一个泛型表示了go所有的内置类型。interface{} 这里就不做太多赘述了。
comparable是一个接口所有可比较的类型都实现了这个接口。 可比较的类型包括布尔型、数值型、字符串、指针、通道channel、可比较类型的数组以及所有字段都是可比较类型的结构体等。 comparable仅能用于泛型中的类型限定type constraint不可作为变量的类型。 看下源码
在使用comparable作为类型约束时需要确保类型参数确实支持比较操作否则会导致编译错误。 comparable不支持大小比较操作如、、、仅支持相等性比较、!。 结构体类型可以作为comparable使用但前提是其所有字段都是comparable的。如果结构体包含不可比较的字段如切片、映射类型则整个结构体类型也不可比较。
搜索算法 泛型也可以用于实现通用的搜索算法。例如定义一个泛型函数来查找切片中的元素
package mainimport fmtfunc 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 mainimport fmt// MyInt 泛型的约束提取定义
type MyInt interface {int | float64 | int8 | int32 // 作用域泛型的而不是一个接口方法 这样泛型约束可以用着集中类型都可以
}// 自定义泛型
func main() {var a int 10var b int 20fmt.Println(GetMaxNum(a, b))
}// GetMaxNum 函数里面泛型T 用自己定义的约束MyInt
func GetMaxNum[T MyInt](a, b T) T {if a b {return a}return b
}3. 支持泛型衍生类型
新符号 ~和类型一起出现的表示支持该类型的衍生类型 衍生类型就是根据该类型常见的类型
package mainimport fmt// int8 衍生类型
type int8A int8
type int8B int8// NewInt ~ 表示可以匹配该类型的衍生类型
type NewInt interface {~int8
}// ~
func main() {var a int8A 8var b int8A 56fmt.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语言的泛型特性并在实际项目中灵活运用泛型来提高代码质量和开发效率。