Go 语言泛型编程之切片

简介: Go 现在都支持泛型了,我们该怎么利用泛型的特点。利用类型参数来写出真实世界的代码。泛型在实际中有什么用途呢?在没有泛型之前 Go 不能实现什么样的代码?

Go 切片 Slice

我们先来看一下切片,切片在 Go 中并不是简单的数组,而是一个结构体,其定义如下:

type slice struct {
  array unsafe.Pointer // 指向存放数据的数组指针
  len int // 长度
  cap int // 容量
}


用图示来看,一个空的 slice 的表现如下:


image.png

在切片中寻找元素

虽然我们通常可以编写特定程序所需的特定代码,但是在泛型之前,在任意容易类型(如切片)上编写函数并不容易。


例如,我们不能写一个函数,接收一个任意类型的切片,并确定该切片是否包含一个给定的元素。相反,我们需要写一个个函数,接收你需要的特定类型,如 []int[]string


但这会很无聊,因为无论切片元素的类型如何,其逻辑都是完全一样的。有了泛型之后,我们就可以通过只写一次这个函数,适用于所有类型。


现在来看一个例子:

func Contains[E comparable](s []E, v E) bool {
    for _, vs := range s {
        if v == vs {
            return true
        }
    }
    return false
}


对于如上的 Contains 函数:对于任意可比较的类型 E,Contains[E] 接收某个类型的切片 []E,和同一类型的元素值 v,并返回一个 bool 值,如果该元素在切片中,则返回 true, 否则返回 false。


类型参数 E 是受限制的,意味着我们不能从字面上接受任何类型,因为我们需要比较元素。如果我们不能确定 v == vs ,我们就无法判断这两个元素是否匹配。


而在 Go 中,并不是每一种数据类型都能用等号 == 的方式进行比较。例如,切片和 map 都不具有直接可比性,结构体如果包含切片或 map 等字段,也不具有可比性。


所以 Go 提供了一个预定义的类型约束,名为 comparable,它限制了 Contains  所允许的类型,使其只能与 == 操作符一起工作。


一个需要比较类型参数值的通用函数必须至少受到可比性的约束,就像本例中一样。

切片反转

我们完全可以编写泛型函数,对字面上任何类型的片子进行操作,在这种情况下,类型约束将只是任意的。例如,如果我们不需要比较元素,那么我们就不会被限制在只有可比较的类型。


例如,我们想反转一个切片,也就是说,从某个元素类型 E 中抽取一个切片,然后产生一个包含相同元素,但是元素顺序相反的切片。


可以尝试写如下的代码:

func Reverse[E any](s []E) []E {
    result := make([]E, 0, len(s))
    for i := len(s) - 1; i >= 0; i-- {
        result = append(result, s[i])
    }
    return result
}


这次,我们就不需要 comparable ,因为对切片进行反转并不需要做任何的比较。我们所需要做的就是在给定的切片基础上,向后循环,然后将每个元素添加到 result 切片上。


具体的实现并不重要,而且这个实现可能并不是最高效的,甚至也太好理解。关键的是,如果没有泛型,不为特定的元素类型重复整个 reverse 函数,根本就不能实现相同的效果。

切片排序

对切片进行排序是另一个例子,如果没有泛型,可能会有点复杂。


标准库的 sort.Slice API 要求用户传入自己的函数来确定两个元素的大小。

sort.Slice(s, func(i, j int) bool
{
    return s[i] < s[j]
})


但是,当我们对像 int 很容易比较大小的类型进行排序时,必须传入这样一个函数似乎很愚蠢,而这通常就是情况。


Go 完全知道如何用 < 来比较两个整数,那么为什么我们要写一个函数来做这个呢?好吧,你知道为什么,但这并不意味着它就不令人讨厌了。


现在我们可以写一个通用的 Sort  函数,它没有如此尴尬的 API 。

func Sort[E constraints.Ordered](s []E) []E {
    result := make([]E, len(s))
    copy(result, s)
    sort.Slice(result, func(i, j int) bool {
        return result[i] < result[j]
    })
    return result
}


就像前面的 Contains 例子一样,我们在这里需要一个类型约束。并非每一种 Go 类型都可以使用 < 操作符:例如,结构体就不可以。不过,其值可以明确排序的数据类型,被称为有序类型。所以我们使用标准库中的 constraints.Ordered 来实现这个限制。


同样,这肯定不是一个模型的实现--它背后使用了 sort.Slice ,这个函数很慢,因为它使用了反射。这在泛型中是不必要的,我们可以直接实现一些高效的排序算法,如快速排序 Quicksort


这个 Sort 函数的重要之处不是代码是否好--它并不好--而是我们可以为任何有序类型直接编写它。为了不让无序类型的人感到被遗弃,我们可以提供一个版本,让用户可以像以前一样传入自己的 Less 函数。


相关文章
|
1天前
|
安全 Java Go
探索Go语言在高并发环境中的优势
在当今的技术环境中,高并发处理能力成为评估编程语言性能的关键因素之一。Go语言(Golang),作为Google开发的一种编程语言,以其独特的并发处理模型和高效的性能赢得了广泛关注。本文将深入探讨Go语言在高并发环境中的优势,尤其是其goroutine和channel机制如何简化并发编程,提升系统的响应速度和稳定性。通过具体的案例分析和性能对比,本文揭示了Go语言在实际应用中的高效性,并为开发者在选择合适技术栈时提供参考。
|
5天前
|
运维 Kubernetes Go
"解锁K8s二开新姿势!client-go:你不可不知的Go语言神器,让Kubernetes集群管理如虎添翼,秒变运维大神!"
【8月更文挑战第14天】随着云原生技术的发展,Kubernetes (K8s) 成为容器编排的首选。client-go作为K8s的官方Go语言客户端库,通过封装RESTful API,使开发者能便捷地管理集群资源,如Pods和服务。本文介绍client-go基本概念、使用方法及自定义操作。涵盖ClientSet、DynamicClient等客户端实现,以及lister、informer等组件,通过示例展示如何列出集群中的所有Pods。client-go的强大功能助力高效开发和运维。
24 1
|
1天前
|
监控 NoSQL Go
Go语言中高效使用Redis的Pipeline
Redis 是构建高性能应用时常用的内存数据库,通过其 Pipeline 和 Watch 机制可批量执行命令并确保数据安全性。Pipeline 类似于超市购物一次性结账,减少网络交互时间,提升效率。Go 语言示例展示了如何使用 Pipeline 和 Pipelined 方法简化代码,并通过 TxPipeline 保证操作原子性。Watch 机制则通过监控键变化实现乐观锁,防止并发问题导致的数据不一致。这些机制简化了开发流程,提高了应用程序的性能和可靠性。
5 0
|
3天前
|
NoSQL Go Redis
Go语言中如何扫描Redis中大量的key
在Redis中,遍历大量键时直接使用`KEYS`命令会导致性能瓶颈,因为它会一次性返回所有匹配的键,可能阻塞Redis并影响服务稳定性。为解决此问题,Redis提供了`SCAN`命令来分批迭代键,避免一次性加载过多数据。本文通过两个Go语言示例演示如何使用`SCAN`命令:第一个示例展示了基本的手动迭代方式;第二个示例则利用`Iterator`简化迭代过程。这两种方法均有效地避免了`KEYS`命令的性能问题,并提高了遍历Redis键的效率。
11 0
|
4天前
|
监控 Serverless Go
Golang 开发函数计算问题之Go 语言中切片扩容时需要拷贝原数组中的数据如何解决
Golang 开发函数计算问题之Go 语言中切片扩容时需要拷贝原数组中的数据如何解决
|
5天前
|
关系型数据库 MySQL 数据库连接
Go语言中使用sqlx来操作事务
在应用中,数据库事务保证操作的ACID特性至关重要。`github.com/jmoiron/sqlx`简化了数据库操作。首先安装SQLX和MySQL驱动:`go get github.com/jmoiron/sqlx`和`go get github.com/go-sql-driver/mysql`。导入所需的包后,创建数据库连接并使用`Beginx()`方法开始事务。通过`tx.Commit()`提交或`tx.Rollback()`回滚事务以确保数据一致性和完整性。
8 0
|
3月前
|
开发框架 安全 中间件
Go语言开发小技巧&易错点100例(十二)
Go语言开发小技巧&易错点100例(十二)
50 1
|
8天前
|
JSON 中间件 Go
go语言后端开发学习(四) —— 在go项目中使用Zap日志库
本文详细介绍了如何在Go项目中集成并配置Zap日志库。首先通过`go get -u go.uber.org/zap`命令安装Zap,接着展示了`Logger`与`Sugared Logger`两种日志记录器的基本用法。随后深入探讨了Zap的高级配置,包括如何将日志输出至文件、调整时间格式、记录调用者信息以及日志分割等。最后,文章演示了如何在gin框架中集成Zap,通过自定义中间件实现了日志记录和异常恢复功能。通过这些步骤,读者可以掌握Zap在实际项目中的应用与定制方法
go语言后端开发学习(四) —— 在go项目中使用Zap日志库
|
5天前
|
算法 NoSQL 中间件
go语言后端开发学习(六) ——基于雪花算法生成用户ID
本文介绍了分布式ID生成中的Snowflake(雪花)算法。为解决用户ID安全性与唯一性问题,Snowflake算法生成的ID具备全局唯一性、递增性、高可用性和高性能性等特点。64位ID由符号位(固定为0)、41位时间戳、10位标识位(含数据中心与机器ID)及12位序列号组成。面对ID重复风险,可通过预分配、动态或统一分配标识位解决。Go语言实现示例展示了如何使用第三方包`sonyflake`生成ID,确保不同节点产生的ID始终唯一。
go语言后端开发学习(六) ——基于雪花算法生成用户ID
|
7天前
|
JSON 缓存 监控
go语言后端开发学习(五)——如何在项目中使用Viper来配置环境
Viper 是一个强大的 Go 语言配置管理库,适用于各类应用,包括 Twelve-Factor Apps。相比仅支持 `.ini` 格式的 `go-ini`,Viper 支持更多配置格式如 JSON、TOML、YAML
go语言后端开发学习(五)——如何在项目中使用Viper来配置环境