Golang - 泛型

类型参数 (泛型)

泛型允许你编写不关心具体类型的函数或数据结构。就像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 的类型intstringboolfloat64, 指针, 结构体(如果其所有字段也都是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 是类型参数,它是一个占位符,代表了将来链表中要存储的具体数据类型(比如intstringMyStruct)。
      • 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]
}