golang泛型与类型约束

在 Go 语言中,泛型(Generics)是 Go 1.18 版本引入的一个强大特性,它允许你编写可以与多种类型一起工作的函数和类型。为了使用泛型,你需要定义一个或多个类型参数(type parameters),并为这些参数指定约束(constraints)。

泛型普通用法

定义类型参数

类型参数是在泛型函数或类型声明中使用的占位符。例如,你可以定义一个函数,它可以接受任何类型的切片:

func PrintSlice[T any](s []T) {
    for _, v := range s {
        fmt.Println(v)
    }
}

T 是一个类型参数。any 是 Go 中的内置类型,表示任何类型,但它实际上是 interface{} 的别名,从 Go 1.18 开始,你也可以使用 interface{}

现在如果要编写一个通用的乘法功能,自然类型不能是any,因为不是所有类型都是支持乘法。这个时候需要使用约束:

package main
 
import (
    "fmt"
    "constraints"
)
 
// 定义一个泛型乘法函数,接受满足constraints.Integer或constraints.Float的类型
func multiply[T constraints.Integer | constraints.Float](a, b T) T {
    return a * b
}
 
func main() {
    // 测试整数乘法
    fmt.Println(multiply(5, 3)) // 输出: 15
    // 测试浮点数乘法
    fmt.Println(multiply(2.5, 3.5)) // 输出: 8.75
}

自定义约束

除了内置类型外,还可以定义自定义约束,然后使用这个接口作为泛型函数的类型参数约束。如下:

package main
 
import (
    "fmt"
)
 
// 定义一个约束接口
type MyConstraint interface {
    int | float64 // 这里的 int 和 float64 是示例,你可以定义更复杂的约束条件
    SomeMethod() // 你可以定义需要实现的方法,例如 SomeMethod()
}
 
// 使用自定义约束的泛型函数
func PrintValue[T MyConstraint](value T) {
    fmt.Println(value)
}
 
// 实现约束接口的方法(如果你需要的话)
func (i int) SomeMethod() {}
func (f float64) SomeMethod() {}
 
func main() {
    PrintValue(42)        // int 类型满足 MyConstraint
    PrintValue(3.14)      // float64 类型满足 MyConstraint
    // PrintValue("hello") // 编译错误:string 不满足 MyConstraint
}

基于父类约束的泛型

在实际开发中,我们会广泛的使用type NewType BaseType为特定场景自定义数据类型。在golang中,别名和基本类型不是一个类型,所以泛型约束为NewType编写的不能应用于BaseType,反之亦然。

所以,此时要用~操作符。

type AnotherInt int
type AllInts interface {
  ~int
}


func AddElements[T AllInts](s []T) T {
  sum := T(0)
  for _, v := range s {
    sum = sum + v
  }
  return sum
}


func main() {
  s := []AnotherInt{0, 1, 2}
  fmt.Println(AddElements(s))
}

 使用双泛型类型参数的 Golang Slice 实现

在 Go 中,也可以为元素类型和切片类型分别指定不同的泛型参数,这在需要更灵活的类型组合时非常有用。以下是几种实现方式:

1. 基本双泛型参数 Slice

 
package main

import "fmt"

// FlexibleSlice 使用两个泛型参数:
// E - 元素类型
// S - 切片类型(通常是 []E 或类似的)
type FlexibleSlice[E any, S []E] struct {
    data S
}

func NewFlexibleSlice[E any, S []E](initial S) *FlexibleSlice[E, S] {
    return &FlexibleSlice[E, S]{data: initial}
}

func (fs *FlexibleSlice[E, S]) Append(elements ...E) {
    fs.data = append(fs.data, elements...)
}

func (fs *FlexibleSlice[E, S]) Get() S {
    return fs.data
}

func main() {
    // 标准用法:元素类型和切片类型一致
    intSlice := NewFlexibleSlice([]int{1, 2, 3})
    intSlice.Append(4, 5)
    fmt.Println(intSlice.Get()) // [1 2 3 4 5]
    
    // 更复杂的用法:元素类型和切片类型可以不同
    type MySpecialSlice []float64
    specialSlice := NewFlexibleSlice[float64, MySpecialSlice](MySpecialSlice{1.1, 2.2})
    specialSlice.Append(3.3)
    fmt.Println(specialSlice.Get()) // [1.1 2.2 3.3]
}

2. 带转换功能的双泛型 Slice

package main

import "fmt"

type Mapper[From any, To any] func(From) To

type TransformSlice[E any, F any, S []E, D []F] struct {
    source S
    mapper Mapper[E, F]
}

func NewTransformSlice[E any, F any, S []E, D []F](source S, mapper Mapper[E, F]) *TransformSlice[E, F, S, D] {
    return &TransformSlice[E, F, S, D]{
        source: source,
        mapper: mapper,
    }
}

func (ts *TransformSlice[E, F, S, D]) Transform() D {
    result := make(D, len(ts.source))
    for i, v := range ts.source {
        result[i] = ts.mapper(v)
    }
    return result
}

func main() {
    // 将 int 切片转换为 string 切片
    ints := []int{1, 2, 3, 4, 5}
    toString := NewTransformSlice[int, string, []int, []string](
        ints,
        func(i int) string { return fmt.Sprintf("Number-%d", i) },
    )
    
    strs := toString.Transform()
    fmt.Println(strs) // [Number-1 Number-2 Number-3 Number-4 Number-5]
    
    // 将 float64 切片转换为 int 切片
    floats := []float64{1.1, 2.2, 3.3}
    toInt := NewTransformSlice[float64, int, []float64, []int](
        floats,
        func(f float64) int { return int(f) },
    )
    
    intsFromFloats := toInt.Transform()
    fmt.Println(intsFromFloats) // [1 2 3]
}

3. 带约束的双泛型参数

package main

import (
    "fmt"
    "golang.org/x/exp/constraints"
)

type Number interface {
    constraints.Integer | constraints.Float
}

type NumericSlice[E Number, S []E] struct {
    data S
}

func NewNumericSlice[E Number, S []E](initial S) *NumericSlice[E, S] {
    return &NumericSlice[E, S]{data: initial}
}

func (ns *NumericSlice[E, S]) Sum() E {
    var sum E
    for _, v := range ns.data {
        sum += v
    }
    return sum
}

func (ns *NumericSlice[E, S]) ConvertToFloat() []float64 {
    result := make([]float64, len(ns.data))
    for i, v := range ns.data {
        result[i] = float64(v)
    }
    return result
}

func main() {
    // 使用基本 int 切片
    intSlice := NewNumericSlice([]int{1, 2, 3})
    fmt.Println("Sum:", intSlice.Sum()) // Sum: 6
    
    // 使用自定义 float32 切片类型
    type MyFloatSlice []float32
    floatSlice := NewNumericSlice[float32, MyFloatSlice](MyFloatSlice{1.1, 2.2, 3.3})
    fmt.Println("Sum:", floatSlice.Sum()) // Sum: 6.6
    fmt.Println("As float64:", floatSlice.ConvertToFloat()) // [1.1 2.2 3.3]
}

4. 实际应用示例:键值对存储

 
package main

import "fmt"

type Pair[K any, V any] struct {
    Key   K
    Value V
}

type PairSlice[K any, V any, S []Pair[K, V]] struct {
    data S
}

func NewPairSlice[K any, V any, S []Pair[K, V]]() *PairSlice[K, V, S] {
    return &PairSlice[K, V, S]{data: make(S, 0)}
}

func (ps *PairSlice[K, V, S]) Add(key K, value V) {
    ps.data = append(ps.data, Pair[K, V]{Key: key, Value: value})
}

func (ps *PairSlice[K, V, S]) GetKeys() []K {
    keys := make([]K, len(ps.data))
    for i, pair := range ps.data {
        keys[i] = pair.Key
    }
    return keys
}

func (ps *PairSlice[K, V, S]) GetValues() []V {
    values := make([]V, len(ps.data))
    for i, pair := range ps.data {
        values[i] = pair.Value
    }
    return values
}

func main() {
    // 使用标准 []Pair 切片
    stringIntPairs := NewPairSlice[string, int, []Pair[string, int]]()
    stringIntPairs.Add("one", 1)
    stringIntPairs.Add("two", 2)
    
    fmt.Println("Keys:", stringIntPairs.GetKeys())    // [one two]
    fmt.Println("Values:", stringIntPairs.GetValues()) // [1 2]
    
    // 使用自定义 Pair 切片类型
    type MyPair struct {
        K string
        V float64
    }
    type MyPairSlice []MyPair
    
    customPairs := NewPairSlice[string, float64, MyPairSlice]()
    customPairs.Add("pi", 3.14)
    customPairs.Add("e", 2.718)
    
    fmt.Println("Custom Keys:", customPairs.GetKeys())      // [pi e]
    fmt.Println("Custom Values:", customPairs.GetValues()) // [3.14 2.718]
}

cmp包

Go 1.21开始成为标准包的一部分,用于比较有序数据。和slices/maps一样(也都是1.21引入,有点像stl里面的算法库),他们底层都用泛型和约束。

关键点总结

  1. 双泛型参数:可以分别指定元素类型和切片类型,提供更大的灵活性

  2. 类型安全:Go 编译器会确保类型约束得到满足

  3. 实际应用:这种模式特别适合需要处理多种切片类型或需要类型转换的场景

  4. 性能考虑:泛型在编译时会生成具体类型的代码,不会带来运行时性能损失

这种双泛型参数的设计模式在需要处理多种相关类型或需要定义更复杂的类型关系时特别有用。

其它说明

Go官方团队的技术负责人Russ Cox在2022.01.25提议[1]将constraints包从Go标准库里移除,放到x/exp项目下。Russ Cox给出的理由是:

  • constraints名字太长,代码写起来比较繁琐。
  • 大多数泛型的代码只用到了anycomparable这2个类型约束。constaints包里只有constraints.Ordered使用比较广泛,其它很少用。所以完全可以把Ordered设计成和any以及comparable一样,都作为Go的预声明标识符,不用单独弄一个constraints包。

备注:

  • golang.org/x下所有package的源码独立于Go源码的主干分支,也不在Go的二进制安装包里。如果需要使用golang.org/x下的package,可以使用go get来安装。
  • golang.org/x/exp下的所有package都属于实验性质或者被废弃的package,不建议使用。

golang预定义的约束可参见:https://pkg.go.dev/golang.org/x/exp/constraints

posted @ 2025-04-12 16:51  zhjh256  阅读(45)  评论(0)    收藏  举报