类型参数 (泛型)
泛型允许你编写不关心具体类型的函数或数据结构。就像C++的模板一样,你可以写一个 Index 函数,它既能在一个 []int 中查找 int,也能在一个 []string 中查找 string,而无需为每种类型都重写一遍函数。
Go 语言泛型函数:
func Index[T comparable](s []T, x T) int
约束 (Constraints):comparable 的作用
这是Go泛型与传统C++模板(C++20之前)一个非常重要的区别,也是Go泛型设计的核心优势。
- 什么是约束? 约束(Constraint)是对类型参数
T的一种“要求”或“规定”。它告诉编译器,任何用来替换T的具体类型,都必须满足某些条件。 - 为什么需要约束? 看
Index函数的内部实现:if v == x { ... }。 这行代码使用了==运算符。但并非所有Go的类型都支持==比较(比如切片、map、函数就不支持)。 如果没有约束,编译器就无法保证v == x这行代码对于所有可能的T都是合法的。 comparable约束:comparable是Go内置的一个约束。它规定:任何用来替换T的类型,都必须支持使用==和!=进行比较。- 满足
comparable的类型:int,string,bool,float64, 指针, 结构体(如果其所有字段也都是comparable的)等。 - 不满足
comparable的类型:[]int(切片),map[string]int(映射),func()(函数) 等。
- 满足
package main
import "fmt"
// Index 返回 x 在 s 中的第一个索引,若不存在则返回 -1。
// T 必须是可比较的类型。
func Index[T comparable](s []T, x T) int {
for i, v := range s {
// 因为有 comparable 约束,所以这里的 == 操作是类型安全的
if v == x {
return i
}
}
return -1
}
func main() {
// 1. 在 int 切片上使用 Index
intSlice := []int{10, 20, 15, 5}
fmt.Println(Index(intSlice, 15)) // 输出: 2
// 2. 在 string 切片上使用 Index
strSlice := []string{"foo", "bar", "baz"}
fmt.Println(Index(strSlice, "hello")) // 输出: -1
}
Index 函数可以无缝地工作在 []int 和 []string 上,因为 int 和 string 都满足 comparable 约束。
泛型类型
除了泛型函数,Go还支持泛型类型
// List 表示一个可以保存任何类型的值的单链表。
type List[T any] struct {
next *List[T]
val T
}
type List[T any] struct:List: 是我们定义的泛型类型的名字。[T any]: 这是类型参数列表。T是类型参数,它是一个占位符,代表了将来链表中要存储的具体数据类型(比如int,string或MyStruct)。any是一个内置的约束,意思是T可以是任何类型,没有任何限制。
- 结构体字段:
val T: 这个字段用来存储节点的值。它的类型是T,意味着如果我们创建一个List[int],val的类型就是int;如果我们创建一个List[string],val的类型就是string。next *List[T]: 这个字段是指向下一个节点的指针。注意,它的类型是*List[T],而不是*List。这保证了链表中的所有节点都必须存储相同类型的数据,从而确保了类型安全。
一个链表的具体实现
package main
import (
"fmt"
"strings"
)
// List 表示一个可以保存任何类型的值的单链表。
type List[T any] struct {
next *List[T]
val T
}
// Add 方法:在链表末尾添加一个新节点
// 接收者是指针,因为我们可能需要修改头节点(如果链表为空)
// 但为了简单,我们假设头节点总是已存在的。
func (l *List[T]) Add(value T) {
// 遍历链表直到找到最后一个节点
current := l
for current.next != nil {
current = current.next
}
// 在末尾添加新节点
current.next = &List[T]{val: value, next: nil}
}
// String 方法:实现 fmt.Stringer 接口,方便打印
func (l *List[T]) String() string {
var sb strings.Builder
sb.WriteString("[")
for current := l; current != nil; current = current.next {
// fmt.Sprint 会将任意类型的值转换为字符串
sb.WriteString(fmt.Sprint(current.val))
if current.next != nil {
sb.WriteString(" -> ")
}
}
sb.WriteString("]")
return sb.String()
}
// Get 方法:获取指定索引的节点值
func (l *List[T]) Get(index int) (T, bool) {
current := l
for i := 0; i < index; i++ {
if current.next == nil {
// 索引超出范围,返回 T 类型的零值和 false
var zero T
return zero, false
}
current = current.next
}
// 找到节点,返回其值和 true
return current.val, true
}
func main() {
// --- 演示 List[int] ---
fmt.Println("--- Integer List ---")
// 创建一个 int 类型的链表头节点
intList := List[int]{val: 1}
// 添加新元素
intList.Add(2)
intList.Add(3)
intList.Add(4)
// 打印整个链表 (会自动调用 String() 方法)
fmt.Println(intList) // 输出: [1 -> 2 -> 3 -> 4]
// 获取元素
val, ok := intList.Get(2)
if ok {
fmt.Printf("Value at index 2 is: %v\n", val) // 输出: Value at index 2 is: 3
}
val, ok = intList.Get(5) // 尝试获取一个不存在的索引
if !ok {
fmt.Println("Index 5 is out of bounds.") // 输出: Index 5 is out of bounds.
}
// --- 演示 List[string] ---
fmt.Println("\n--- String List ---")
// 创建一个 string 类型的链表,同样的代码,不同的类型
stringList := List[string]{val: "hello"}
stringList.Add("world")
stringList.Add("go")
fmt.Println(stringList) // 输出: [hello -> world -> go]
}