Go 1.21: 泛型函数的全面回顾

简介: Go 1.21: 泛型函数的全面回顾

在Go编程语言中,泛型一直是一个备受期待的特性。随着Go 1.21的发布。


本综合指南旨在提供Go 1.21中泛型的详细探索,阐明它们的优点、语法、实现和最佳实践。


Go 1.21中的泛型基本语法


要定义泛型函数或类型,可以使用类型 T关键字,后跟用方括号[]括起来的泛型形参的名称。例如,要创建一个接受任意类型的slice并返回其第一个元素的泛型函数,可以这样定义:

func First[T any](items []T) T {
    return items[0]
}


在上面的例子中,[T any]表示类型参数T,它表示任意类型。any关键字表示T类型可以是任何有效类型。


然后,可以使用任何切片类型调用First函数,该函数将返回该切片的第一个元素。例如:

func func1() {
  intSlice := []int{1, 2, 3, 4, 5}
  firstInt := First[int](intSlice) // returns 1
  println(firstInt)
  stringSlice := []string{"apple", "banana", "cherry"}
  firstString := First[string](stringSlice) // returns "apple"
  println(firstString)
}
func First[T any](items []T) T {
  return items[0]
}


注意,在调用泛型函数时,我们在方括号[]中指定类型参数。这允许编译器在编译期间为该类型生成特定的代码。


我们还可以向泛型类型参数添加约束,将其限制为特定类型。例如,如果我们想将类型T限制为仅实现Stringer接口的类型,可以使用如下约束:

func PrintString[T Stringer](value T) {
    fmt.Println(value.String())
}

Stringer约束确保类型T必须具有String()方法。这个约束允许我们在函数的value参数上安全地调用String()方法。


在Go 1.21中具有各种类型的泛型


在另一个示例中,让我们编写函数SumGenerics,它对各种数字类型(如intint16int32int64int8float32float64)执行加法操作。

func SumGenerics[T int | int16 | int32 | int64 | int8 | float32 | float64](a, b T) T {
    return a + b
}

让我们看看如何利用这个泛型函数:

func func2() {
  sumInt := SumGenerics[int](2, 3)
  sumFloat := SumGenerics[float32](2.5, 3.5)
  sumInt64 := SumGenerics[int64](10, 20)
  fmt.Println(sumInt)   // returns 5
  fmt.Println(sumFloat) // returns 6.0
  fmt.Println(sumInt64) // returns 30
}

在上面的代码中,我们可以看到,通过在调用泛型函数时在方括号[]中指定类型参数,我们可以对不同的数字类型执行加法操作。类型约束确保只有指定的类型[T int, int16, int32, int64, int8, float32,或float64]可以用作类型参数。


以这种方式使用泛型使我们能够在不牺牲类型安全的情况下编写简洁且可重用的代码。可以使用各种数字类型调用该函数,编译器将为每种类型生成特定的代码,以确保正确执行加法操作。


Go 1.21中具有任意数据类型的泛型


泛型可以用于任意数据类型的序列化和反序列化,使用提供的序列化和反序列化函数:


type Person struct {
 Name    string
 Age     int
 Address string
}
func Serialize[T any](data T) ([]byte, error) {
  buffer := bytes.Buffer{}
  encoder := gob.NewEncoder(&buffer)
  err := encoder.Encode(data)
  if err != nil {
    return nil, err
  }
  return buffer.Bytes(), nil
}
func Deserialize[T any](b []byte) (T, error) {
  buffer := bytes.Buffer{}
  buffer.Write(b)
  decoder := gob.NewDecoder(&buffer)
  var data T
  err := decoder.Decode(&data)
  if err != nil {
    return data, err
  }
  return data, nil
}


在本例中,我们有两个通用函数SerializeDeserialize,它们利用Go的gob包将任意数据类型转换为字节,反之亦然。

func DeserializeUsage() {
  person := Person{
  Name:    "John",
  Age:     30,
  Address: "123 Main St.",
  }
  serialized, err := Serialize(person)
  if err != nil {
    panic(err)
  }
  
  deserialized, err := Deserialize[Person](serialized)
  if err != nil {
    panic(err)
  }
  
  fmt.Printf("Name: %s, Age: %d, Address: %s", deserialized.Name, deserialized.Age, deserialized.Address)
}

Output: Name: John, Age: 30, Address: 123 Main St.


在上面的代码中,我们用一些数据创建了一个Person实例。然后使用Serialize函数将person对象转换为字节数组。稍后,使用Deserialize函数,将字节数组转换回Person对象。


通过将序列化和反序列化函数定义为具有T any类型参数的泛型函数,我们可以序列化和反序列化任何支持使用gob包进行编码和解码的数据类型。


在Go中使用泛型和Validate函数自定义验证器


让我们用自定义验证器编写一个通用的Validate函数。

type Validator[T any] func(T) error
func Validate[T any](data T, validators ...Validator[T]) error {
 for _, validator := range validators {
  err := validator(data)
  if err != nil {
   return err
  }
 }
 return nil
}


在本例中,我们有一个通用的Validate函数,它使用自定义验证器执行数据验证。Validator类型表示一个函数,它接受任意类型T的值并返回一个错误。

func StringNotEmpty(s string) error {
 if len(strings.TrimSpace(s)) == 0 {
  return fmt.Errorf("string cannot be empty")
 }
 return nil
}
func IntInRange(num int, min, max int) error {
 if num < min || num > max {
  return fmt.Errorf("number must be between %d and %d", min, max)
 }
 return nil
}


此外,我们有两个自定义验证器示例:StringNotEmptyIntInRange


StringNotEmpty确保字符串不为空,IntInRange检查整数是否在指定范围内。

package main
func main() {
  person := Person{
    Name:    "John",
    Age:     30,
    Address: "123 Main St.",
  }
  
  err := Validate(person, func(p Person) error {
    return StringNotEmpty(p.Name)
  }, func(p Person) error {
    return IntInRange(p.Age, 0, 120)
  })
  
  if err != nil {
    println(err.Error())
    panic(err)
  }
  
  println("Person is valid")
}

在本例中,我们创建了一个Person实例,并将其传递给Validate函数。我们为Person结构定义了两个自定义验证器,检查Name字段是否为空,Age字段是否在有效范围内。如果任何验证器返回错误,验证过程将停止,并返回相应的错误。


通过使用泛型和自定义验证器,Validate函数允许跨不同数据类型进行灵活和可重用的数据验证,增强代码可重用性,并使添加或修改验证规则变得容易。


让我们再写一个使用validator函数的例子:

type LoginForm struct {
    Username string
    Password string
}
func (f *LoginForm) Validate() error {
    return Validate(f,
        func(l *LoginForm) error {
            return StringNotEmpty(l.Username)
        },
        func(l *LoginForm) error {
            return StringNotEmpty(l.Password)
        },
    )
}
func ValidateUsage2() {
    loginForm := LoginForm{
        Username: "John",
        Password: "123",
    }
    err := loginForm.Validate()
    if err != nil {
        println(err.Error())
        panic(err)
    }
    println("Login form is valid")
}


在本例中,LoginForm结构实现了一个Validate方法,该方法利用了我们之前定义的Validate泛型函数。


Validate方法调用通用的Validate函数,并为它提供两个特定于LoginForm类型的自定义验证器。验证器表示为闭包函数,使用StringNotEmpty验证器函数检查UsernamePassword字段是否为空。


要验证LoginForm实例,只需在实例本身上调用validate方法。

package main
func main() {
  loginForm := LoginForm{
    Username: "John",
    Password: "123",
  }
  
  err := loginForm.Validate()
  if err != nil {
    println(err.Error())
    panic(err)
  }
  
  println("Login form is valid")
}


如果任何验证器返回错误,验证过程将停止,并返回相应的错误。在这种情况下,我们相应地处理错误。


结尾


这些示例展示了Go 1.21中泛型的强大功能和多功能性。泛型使我们能够编写可重用和类型安全的代码,这些代码可以处理不同的数据类型和结构,而不会牺牲代码的清晰度和可维护性。


泛型为Go编程语言带来了显著的好处,增强了代码重用,减少了冗余,并改进了代码组织。有了泛型,开发人员就能够编写更有表现力、更简洁、更灵活的代码,以适应不同的数据类型和结构,从而为更具可扩展性和可维护性的软件开发铺平道路。

相关文章
|
3月前
|
算法 Go
Go 语言泛型 — 泛型语法与示例
本文详解 Go 语言泛型语法与使用示例,涵盖泛型函数、类型声明、类型约束及实战应用,适合入门学习与开发实践。
|
8天前
|
Java 编译器 Go
【Golang】(5)Go基础的进阶知识!带你认识迭代器与类型以及声明并使用接口与泛型!
好烦好烦好烦!你是否还在为弄不懂Go中的泛型和接口而烦恼?是否还在苦恼思考迭代器的运行方式和意义?本篇文章将带你了解Go的接口与泛型,还有迭代器的使用,附送类型断言的解释
53 3
|
8天前
|
存储 安全 Java
【Golang】(4)Go里面的指针如何?函数与方法怎么不一样?带你了解Go不同于其他高级语言的语法
结构体可以存储一组不同类型的数据,是一种符合类型。Go抛弃了类与继承,同时也抛弃了构造方法,刻意弱化了面向对象的功能,Go并非是一个传统OOP的语言,但是Go依旧有着OOP的影子,通过结构体和方法也可以模拟出一个类。
56 1
|
1月前
|
存储 Java Go
对比Java学习Go——函数、集合和OOP
Go语言的函数支持声明与调用,具备多返回值、命名返回值等特性,结合`func`关键字与类型后置语法,使函数定义简洁直观。函数可作为一等公民传递、赋值或作为参数,支持匿名函数与闭包。Go通过组合与接口实现面向对象编程,结构体定义数据,方法定义行为,接口实现多态,体现了Go语言的简洁与高效设计。
|
3月前
|
存储 安全 算法
Go语言泛型-泛型对代码结构的优化
Go语言自1.18版本引入泛型,极大提升了代码的通用性与可维护性。通过泛型,开发者可以减少重复代码、提高类型安全性,并增强程序的复用性和可读性。本文详细介绍了泛型在数据结构、算法及映射功能中的应用,展示了其在优化代码结构方面的优势。同时,Go编译器对泛型代码进行类型推导,确保运行时性能不受影响。合理使用泛型,有助于构建更加灵活高效的程序。
|
3月前
|
人工智能 Go
GO语言之泛型应用
本文介绍了Go语言中泛型的使用,包括为何引入泛型、泛型语法详解以及如何自定义约束。通过实例展示了泛型在简化代码、提高复用性方面的优势,并演示了泛型在slice、指针、map等数据类型中的应用。
104 1
|
3月前
|
分布式计算 算法 安全
Go语言泛型-泛型约束与实践
Go语言中的泛型约束用于限制类型参数的范围,提升类型安全性。通过接口定义约束,可实现对数值类型、排序与比较等操作的支持。开发者既可使用标准库提供的预定义约束,如constraints.Ordered和constraints.Comparable,也可自定义约束以满足特定需求。泛型广泛应用于通用数据结构(如栈、队列)、算法实现(如排序、查找)及构建高效可复用的工具库,使代码更简洁灵活。
|
4月前
|
人工智能 Dart Go
Go语言中的make和new函数的区别及使用场景
本文详细解析了Go语言中`make`和`new`函数的使用方法及区别。`make`用于创建切片、映射和通道等引用类型,返回初始化后的值;`new`用于创建任意类型的零值对象,返回指向该对象的指针。文章通过多个示例说明两者的应用场景,并总结了面试中可能遇到的相关问题,如底层实现、使用场景及优缺点等,帮助读者更好地理解和区分这两个函数。
133 1
|
3月前
|
Linux Go 开发者
Go语言泛型-泛型约束与实践
《Go语言实战指南》介绍了如何使用Go进行交叉编译,即在一个操作系统上编译出适用于不同系统和架构的二进制文件。通过设置GOOS和GOARCH环境变量,开发者可轻松构建跨平台程序,无需在每个平台上单独编译。Go从1.5版本起原生支持此功能,极大提升了多平台部署效率。
|
5月前
|
Go 调度
GO语言函数的内部运行机制分析
以上就是Go语言中函数的内部运行机制的概述,展示了函数在Go语言编程中如何发挥作用,以及Go如何使用简洁高效的设计,使得代码更简单,更有逻辑性,更易于理解和维护。尽管这些内容深入了一些底层的概念,但我希望通过这种方式,将这些理论知识更生动、更形象地带给你,让你在理解的同时找到编程的乐趣。
81 5

热门文章

最新文章