CppGuide社区 CppGuide社区
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
  • Linux内核

    • 心中的内核 —— 在阅读内核代码之前先理解内核
    • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
    • TCP源码实现超详细注释版.pdf (opens new window)
GitHub (opens new window)
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
  • Linux内核

    • 心中的内核 —— 在阅读内核代码之前先理解内核
    • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
    • TCP源码实现超详细注释版.pdf (opens new window)
GitHub (opens new window)
  • Go开发实用指南 说明
  • 第1章 Go项目如何组织
  • 第2章 字符串处理
  • 第3章 处理日期和时间
  • 第4章 使用数组、切片和映射
  • 第5章 使用类型、结构体和接口
  • 第6章 使用泛型
    • 泛型函数
      • 编写一个用于数字相加的泛型函数
      • 操作方法……
      • 将约束声明为接口
      • 操作方法……
      • 将泛型函数用作访问器和适配器
      • 操作方法……
      • 从泛型函数返回零值
      • 操作方法……
      • 对泛型参数使用类型断言
      • 如何操作...
    • 泛型类型
      • 编写类型安全的集合
      • 如何操作...
      • 有序映射——使用多个类型参数
      • 如何操作...
  • 第7章 并发
  • 第8章 错误与恐慌(panic)
  • 第9章 context包
  • 第10章 处理大量数据
  • 第11章 处理JSON数据
  • 第12章 进程
  • 第13章 网络编程
  • 第14章 流式输入/输出
  • 第15章 数据库
  • 第16章 日志记录
  • 第17章 测试、基准测试和性能分析
目录

第6章 使用泛型

# 第6章 使用泛型

在编程过程中,经常会遇到这样的情况:你编写了一个函数,使用某种类型(比如整数)的值进行一些计算,但随着开发的推进,突然需要对另一种数据类型(比如float64)执行相同的操作。于是,你复制粘贴第一个函数,并修改其名称和数据类型。这种情况最明显、最常见的例子可能就是容器数据类型,比如映射(maps)和集合(sets)。你先为整数值构建一个容器类型,然后为字符串构建同样的容器类型,接着再为结构体构建,以此类推。

泛型(Generics)是一种在编译时通过代码模板来替代代码复制粘贴的方法。首先,你创建一个函数模板(泛型函数)或数据类型模板(泛型类型)。通过指定类型来实例化泛型函数或类型。编译器负责使用你提供的类型实例化模板,并检查实例化后的泛型类型或函数是否能与你提供的类型一起编译。

在本章中,你将学习如何在常见场景中使用泛型函数和泛型类型:

  • 泛型函数
    • 编写一个用于数字相加的泛型函数
    • 将约束声明为接口
    • 将泛型函数用作适配器和访问器
  • 泛型类型
    • 编写一个类型安全的集合
    • 有序映射——使用多个类型参数

# 泛型函数

泛型函数是一种将类型作为参数的函数模板。泛型函数必须能在其参数的所有可能类型赋值下编译通过。泛型函数能够接受的类型由 “类型约束(type constraints)” 定义。在本节中,我们将学习这些概念。

# 编写一个用于数字相加的泛型函数

一个用于说明泛型的很好的入门示例是数字相加函数。这些数字可以是各种类型的整数或浮点数。在这里,我们将研究几个具有不同功能的方法。

# 操作方法……

一个接受int和float64类型数字的泛型求和函数如下:

func Sum[T int | float64](values ...T) T {
    var result T
    for _, x := range values {
        result += x
    }
    
    return result
}
1
2
3
4
5
6
7
8

[T int | float64]结构为Sum函数定义了类型参数:

  • T是类型名称。例如,如果你为int类型实例化Sum函数,那么T就是int。
  • int | float64表达式是T的类型约束。在这种情况下,它表示 “T要么是int,要么是float64”。该约束告诉编译器,Sum函数只能为int或float64类型的值实例化。

正如我之前解释的,泛型函数只是一个模板。例如,你不能声明一个函数变量并将其赋值为Sum,因为Sum不是一个真正的函数。以下语句为int类型实例化Sum泛型函数:

fmt.Println(Sum[int](1, 2, 3))
1

在很多情况下,编译器可以推断出类型参数,所以以下代码也是有效的。由于所有参数都是int值,编译器会推断这里指的是Sum[int]:

fmt.Println(Sum(1, 2, 3))
1

但在以下情况中,实例化的函数是Sum[float64],参数会被解释为float64值:

fmt.Println(Sum[float64](1, 2, 3))
1

泛型函数必须能在所有可能的T类型下成功编译。在这个例子中,T可以是int或float64,所以函数体必须在T为int和T为float64时都有效。类型约束能让编译器生成有意义的编译时错误。例如,[T int | float64 | big.Int]约束无法编译,因为result += x对于big.Int类型无法编译通过。

Sum函数对从int或float64派生的类型不起作用,例如:

type ID int
1

即使ID本质是int类型,Sum[ID]也会导致编译错误,因为ID是一个新类型。要包含所有从int派生的类型,在约束中使用~int,例如:

func Sum[T ~int | ~float64](values ...T) T{...}
1

这个声明将处理所有从int和float64派生的类型。

# 将约束声明为接口

在声明新函数时不断重复约束并不实用。相反,你可以在接口中以类型列表或方法列表的形式定义它们。

# 操作方法……

Go语言的接口指定了一个方法集。Go语言泛型的实现扩展了这个定义,使得接口在用作约束时定义类型集。由于基本类型(如int)没有方法,这需要一些改变来适应基本类型。所以在将接口用作约束时,有两种语法:

  1. 类型列表指定可替代类型参数的类型列表。例如,以下UnsignedInteger约束接受所有无符号整数类型以及所有从无符号整数派生的类型:
type UnsignedInteger interface {
    ~uint8 | ~uint16 | ~uint32 | ~uint64
}
1
2
3
  1. 方法集指定可接受类型必须实现的方法。以下Stringer约束接受所有具有String() string方法的类型:
type Stringer interface {
    String() string
}
1
2
3

这些约束可以组合使用。例如,以下UnsignedIntegerStringer约束接受从无符号整数类型派生且具有String() string方法的类型:

type UnsignedIntegerString interface {
    UnsignedInteger
    Stringer
}
1
2
3
4

Stringer接口既可以用作约束,也可以用作普通接口。UnsignedInteger和UnsignedIntegerString接口只能用作约束。

# 将泛型函数用作访问器和适配器

泛型函数为类型安全的访问器和类型适配器提供了实用的解决方案。例如,用常量值初始化一个*int变量需要声明一个临时值,而这可以通过泛型函数简化。本方法包含几个这样的访问器和适配器。

# 操作方法……

这个泛型函数可以从任意值创建一个指针:

func ToPtr[T any](value T) *T {
    return &value
}
1
2
3

这可用于在不使用临时变量的情况下初始化指针:

type UpdateRequest struct {
    Name *string
    ...
}

...

request := UpdateRequest{Name: ToPtr("test"), }
1
2
3
4
5
6
7
8

类似地,这个泛型函数可以从任意值创建一个切片:

func ToSlice[T any](value T) []T {
    return []T{value}
}

func main() {
    fmt.Println(ToSlice(1))
    // 打印一个int切片: [1]
}
1
2
3
4
5
6
7
8

以下泛型函数返回切片的最后一个元素:

func Last[T any](slice []T) (T, bool) {
    if len(slice) == 0 {
        var zero T
        return zero, false
    }
    
    return slice[len(slice)-1], true
}
1
2
3
4
5
6
7
8

如果切片为空,它将返回false。

以下泛型函数可用于将返回一个值和一个错误的函数适配到只接受值的上下文中。如果有错误,该函数会引发恐慌(panic):

func Must[T any](value T, err error) T {
    if err != nil {
        panic(err)
    }
    
    return value
}
1
2
3
4
5
6
7

这将f() (T, error)函数适配为Must(f()) T。

# 从泛型函数返回零值

如前所述,泛型函数必须能在类型约束允许的所有可能类型下编译。在创建零值时,这可能会引发问题。

# 操作方法……

要创建一个参数化类型的零值,只需声明一个变量:

func Search[T []E, E comparable](slice T, value E) (E, bool) {
    for _, v := range slice {
        if v == value {
            return v, true
        }
    }
    
    // 像这样声明一个零值
    var zero E
    return zero, false
}
1
2
3
4
5
6
7
8
9
10
11

# 对泛型参数使用类型断言

有时,你需要在泛型函数中根据值的类型执行不同操作。这需要用到类型断言或类型开关,二者都适用于接口。然而,并不能保证函数会针对接口进行实例化。本方法展示如何实现上述需求。

# 如何操作...

假设你有一个泛型函数,它对整数的处理方式有所不同:

func Print[T any](value T) {
    // 以下代码无法工作,因为value不一定是interface{}类型。
    if intValue, ok:=value. (int); ok {
    	...
    } else {
    	...
    }
}
1
2
3
4
5
6
7
8

为了使其生效,你必须确保value是一个接口:

func Print[T any](value T) {
    // 将value转换为接口
    valueIntf := any{value)
    if intValue, ok:=valueIntf. (int); ok {
    	// value是一个整数
    } else {
    	// value不是一个整数
    }
}
1
2
3
4
5
6
7
8
9

同样的思路也适用于类型开关:

func Print[T any](value T) {
    switch v: = any(value).(type) {
    case int:
    	// value是一个整数
    
    default:
    	// value不是一个整数
    }
}
1
2
3
4
5
6
7
8
9

# 泛型类型

泛型函数的语法可以自然地扩展到泛型类型。泛型类型具有相同的类型参数和约束,并且该类型的每个方法也会隐式地拥有与类型本身相同的参数。

# 编写类型安全的集合

类型安全的集合可以使用map[T]struct{}来实现。需要注意的是,T不能是任意类型。只有可比较类型才能作为映射的键,Go语言中有一个预定义的约束来满足这一需求。

# 如何操作...

  1. 使用映射声明一个参数化的集合类型:
type Set[T comparable] map[T]struct{}
1
  1. 使用相同的类型参数声明该类型的方法。在声明方法时,只能通过名称引用类型参数:
// Has方法用于判断集合中是否包含给定的值
func (s Set[T]) Has(value T) bool {
    _, exists := s[value]
    return exists
}

// Add方法用于向集合s中添加值
func (s Set[T]) Add(values ...T) {
    for _, v := range values {
    	s[v] = struct{}{}
    }
}

// Remove方法用于从集合s中删除值
func (s Set[T]) Remove(values ...T) {
    for _, v := range values {
    	delete(s, v)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  1. 如果有必要,为新类型创建一个泛型构造函数:
// NewSet函数用于创建一个新的集合
func NewSet[T comparable]() Set[T] {
	return make(Set[T])
}
1
2
3
4
  1. 实例化该类型以使用它:
stringSet := NewSet[string]()
1

请注意,NewSet函数显式地使用字符串类型参数进行实例化。编译器无法推断你想要的类型,所以你必须明确写出NewSet[string]()。然后编译器会实例化Set[string]类型,这也会实例化该类型的所有方法 。

# 有序映射——使用多个类型参数

这个有序映射的实现允许你通过结合切片和映射来保持添加到映射中的元素的顺序。

# 如何操作...

  1. 定义一个包含两个类型参数的结构体:
type OrderedMap[Key comparable, Value any] struct {
    m     map[Key]Value
    slice []Key
}
1
2
3
4

由于Key将用作映射的键,所以它必须是可比较的。对值的类型没有限制。 2. 定义该类型的方法。现在,方法使用Key和Value进行声明:

// Add方法用于向映射中添加键值对
func (m *OrderedMap[Key, Value]) Add(key Key, value Value) {
    _, exists := m.m[key]
    if exists {
    	m.m[key] = value
    } else {
    	m.slice = append(m.slice, key)
    	m.m[key] = value
    }
}

// ValueAt方法返回给定索引处的值
func (m *OrderedMap[Key, Value]) ValueAt(index int) Value {
	return m.m[m.slice[index]]
}

// KeyAt方法返回给定索引处的键
func (m *OrderedMap[Key, Value]) KeyAt(index int) Key {
	return m.slice[index]
}

// Get方法返回与键对应的值,以及键是否存在
func (m *OrderedMap[Key, Value]) Get(key Key) (Value, bool) {
	v, bool := m.m[key]
	return v, bool
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
提示
接收器的类型参数是按位置匹配的,而不是按名称。换句话说,你可以如下定义一个方法:
go<br/>func (m *OrderedMap[K, V]) ValueAt(index int) V {<br/>return m.m[m.slice[index]]<br/>}<br/>
这里,K代表Key,V代表Value。
  1. 如果有必要,定义一个构造函数泛型函数:
func NewOrderedMap[Key comparable, Value any]() *OrderedMap[Key, Value] {
	return &OrderedMap[Key, Value]{
		m:     make(map[Key]Value),
		slice: make([]Key, 0),
	}
}
1
2
3
4
5
6
提示
在这种情况下,构造函数是必要的,因为我们需要初始化泛型结构体中的映射。每次向容器中添加内容时都检查映射是否为nil会很麻烦。你必须在拥有一个零值即可用的容器类型所带来的便利,与每次添加内容时检查nil映射所付出的性能代价之间做出选择。
上次更新: 2025/06/04, 17:59:49
第5章 使用类型、结构体和接口
第7章 并发

← 第5章 使用类型、结构体和接口 第7章 并发→

最近更新
01
第二章 关键字static及其不同用法
03-27
02
第一章 auto与类型推导
03-27
03
第四章 Lambda函数
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式