【魅力golang】之-泛型

发布于:2025-02-11 ⋅ 阅读:(33) ⋅ 点赞:(0)

早期的golang版本是不支持泛型的,这对于从其它语言转型做go开发的程序员来说,非常不友好,自 1.18开始golang正式支持泛型,解决了开发者在编写通用代码时的需求。泛型通过类型参数允许函数和数据结构支持多种类型,从而提升代码的可复用性和灵活性。

1泛型的意义

1.1 什么是泛型

泛型就是广泛的数据类型,或者叫扩展的类型、增加的类型,其本质是参数化类型,指在定义函数或数据结构时使用类型参数,以便可以在调用时指定具体的类型。它允许开发者编写通用代码,而无需为每种数据类型编写单独的实现。

1.2 为什么需要泛型

在 Go 的早期版本中,处理不同类型的通用逻辑需要使用接口或类型断言。这种方式虽然灵活,但牺牲了类型安全性和性能。泛型的引入解决了以下问题:

  1. 代码复用性:避免为不同类型编写重复逻辑。
  2. 类型安全性:在编译时确保类型一致性,减少运行时错误。
  3. 性能优化:避免接口和类型断言带来的额外开销。

示例:没有泛型的通用函数

package main

import "fmt"

func addInts(a, b int) int {
	return a + b
}

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

func main() {
	fmt.Println(addInts(1, 2)) // 输出: 3

	fmt.Println(addFloats(1.1, 2.2)) // 输出: 3.3000000000000003
}

输出:

3
3.3000000000000003(这里请大家先忽略精度丢失问题,后面风云会单独发文详细阐述,今天主要讲泛型,下同)

上述代码需要为每种数据类型分别实现函数。使用泛型后,可以实现统一的逻辑。

2Go 泛型的设计思路

Go 的泛型设计注重简单性和实用性。其关键点如下:

  1. 类型参数:通过类型参数化函数和结构体,实现多种类型的支持。
  2. 类型约束:使用接口定义类型参数的限制,明确泛型函数可用的操作。
  3. 简单与一致:Go 泛型设计避免了过度复杂性,保持语言的简洁性。

3泛型的基本用法

3.1 泛型函数

在 Golang 中,使用方括号 [] 定义类型参数。

示例:泛型函数

package main

import "fmt"

// 泛型函数
func add[T int | float64](a, b T) T {  // T 表示泛型类型
	return a + b
}

func main() {
	fmt.Println(add(1, 2)) // 输出: 3

	fmt.Println(add(1.1, 2.2)) // 输出: 3.3000000000000003
}
  • T:表示类型参数。
  • int | float64:类型约束,表示 T 可以是 int 或 float64。

3.2 泛型结构体

泛型可以用于结构体,使其支持多种类型。

示例:泛型结构体

package main

import "fmt"

// 泛型结构体
type Pair[T any] struct {  // T 表示泛型类型
	First T  // First 和 Second 分别表示泛型类型的两个字段

	Second T
}

func main() {
	p1 := Pair[int]{First: 1, Second: 2}  // 创建一个 Pair[int] 类型的实例

	p2 := Pair[string]{First: "Hello", Second: "World"} // 创建一个 Pair[string] 类型的实例

	fmt.Println(p1) // 输出: {1 2}
	fmt.Println(p2) // 输出: {Hello World}
}
  • any类型实际上是interface{}的别名,两者在所有方面都是等价的‌‌1any类型的引入主要是为了在泛型中使用时简化语法,减少括号的使用‌。

3.3 类型约束

Go 使用接口定义类型约束,指定类型参数的范围。

示例:使用类型约束

package main

import "fmt"

// 定义一个约束
type Number interface {
	int | float64
}

// 使用约束的泛型函数
func multiply[T Number](a, b T) T {
	return a * b
}

func main() {
	fmt.Println(multiply(2, 3)) // 输出: 6

	fmt.Println(multiply(1.5, 2.0)) // 输出: 3
}

通过类型约束,确保泛型函数支持的操作是合法的。

4泛型的应用场景

4.1 通用容器

泛型适合实现通用容器,如列表、队列、栈等。

示例:通用列表

package main

import "fmt"

// 定义一个泛型切片
type List[T any] struct {
	items []T
}

// 添加一个元素到切片中
func (l *List[T]) Add(item T) { // 泛型方法
	l.items = append(l.items, item) // 添加一个元素到切片中
}

func (l *List[T]) Get(index int) T { // 泛型方法
	return l.items[index] // 返回指定索引的元素
}

func main() {
	intList := List[int]{} // 定义一个整数泛型切片

	intList.Add(1) // 添加元素
	intList.Add(2) // 添加元素

	fmt.Println(intList.Get(0)) // 输出: 1

	stringList := List[string]{} // 定义一个字符串泛型切片

	stringList.Add("Hello") // 添加元素
	stringList.Add("World") // 添加元素

	fmt.Println(stringList.Get(1)) // 输出: World
}

4.2 数据操作

使用泛型实现通用的数据操作函数,如查找、过滤、映射等。

示例:通用查找函数

package main

import "fmt"

// 查找函数
func find[T comparable](arr []T, target T) int { // 泛型函数
	for i, v := range arr { //遍历

		if v == target { // 使用泛型类型 T
			return i
		}
	}

	return -1
}

func main() {
	nums := []int{1, 2, 3, 4}  // 使用泛型类型 int
	fmt.Println(find(nums, 3)) // 输出: 2

	words := []string{"Go", "Python", "Java"} // 使用泛型类型 string
	fmt.Println(find(words, "Python"))        // 输出: 1
}
  • comparable:预定义约束,表示类型支持 == 和 !=。

4.3 自定义算法

泛型适合实现通用算法,如排序、归并等。

示例:通用排序

package main

import (
	"fmt"

	"sort"
)

// 泛型排序函数
func sortSlice[T int | float64](arr []T) {
	sort.Slice(arr, func(i, j int) bool { // 使用泛型类型作为比较函数
		return arr[i] < arr[j] // 使用泛型类型进行比较
	})
}

func main() {
	nums := []int{4, 2, 3, 1} // 使用int类型
	sortSlice(nums)           // 使用泛型排序函数
	fmt.Println(nums)         // 输出: [1 2 3 4]

	floats := []float64{3.1, 1.2, 2.3} // 使用float64类型
	sortSlice(floats)                  // 使用泛型排序函数
	fmt.Println(floats)                // 输出: [1.2 2.3 3.1]
}

5泛型的特点与注意事项

5.1 特点

  1. 类型安全:在编译时进行类型检查,减少运行时错误。
  2. 代码复用:通过泛型减少代码重复。
  3. 灵活性:支持任意类型的动态组合。

5.2 注意事项

  1. 性能问题:泛型代码可能在编译时生成多种类型的实现,可能增加二进制文件大小。
  2. 约束复杂性:复杂的类型约束可能降低代码的可读性。
  3. 接口 vs 泛型:在某些场景下,使用接口比泛型更简单。

示例:接口更适合的场景

对于需要操作不同类型的值但无需类型安全的场景,接口更为简洁。

package main

import "fmt"

func printValues(values []interface{}) { // 使用 interface{} 类型作为参数
	// 遍历 values 切片,打印每个元素的值。使用 range 语句来迭代每个元素,无需关心切片长度。
	for _, v := range values {
		fmt.Println(v)
	}
}

func main() {
	printValues([]interface{}{1, "Hello", 3.14}) // 输出: 1, Hello, 3.14
}

6总结

Go 中的泛型通过类型参数和类型约束,使开发者能够编写更加灵活、高效的代码。尽管泛型强大,但应根据具体场景选择最合适的工具。在简单场景中,接口可能更为直观,而泛型则适合需要类型安全的复杂逻辑。合理使用泛型可以显著提升代码的可维护性和复用性。


网站公告

今日签到

点亮在社区的每一天
去签到