搜索
首页后端开发GolangGo 泛型:深入探讨

Go Generics: A Deep Dive

1. 不使用泛型

在引入泛型之前,有几种方法来实现支持不同数据类型的泛型函数:

方法 1:为每种数据类型实现一个函数
这种方式会导致代码极度冗余和维护成本高昂。任何修改都需要对所有函数执行相同的操作。而且,由于Go语言不支持同名函数重载,因此暴露这些函数供外部模块调用也不方便。

方法二:使用范围最大的数据类型
为了避免代码冗余,另一种方法是使用范围最大的数据类型,即方法2。典型的例子是math.Max,它返回两个数字中较大的一个。为了能够比较各种数据类型的数据,math.Max使用了Go中数值类型中范围最大的float64数据类型作为输入和输出参数,从而避免了精度损失。虽然这在一定程度上解决了代码冗余问题,但是任何类型的数据都需要先转换为float64类型。例如,当比较 int 和 int 时,仍然需要进行类型转换,这不仅会降低性能,而且显得不自然。

方法 3:使用接口{}类型
使用interface{}类型有效解决了上述问题。然而,interface{}类型引入了一定的运行时开销,因为它需要在运行时进行类型断言或类型判断,这可能会导致一些性能下降。另外,当使用interface{}类型时,编译器无法进行静态类型检查,因此某些类型错误可能只能在运行时发现。

2. 泛型的优点

Go 1.18 引入了对泛型的支持,这是 Go 语言开源以来的一个重大变化。
泛型是编程语言的一个特性。它允许程序员在编程中使用泛型类型而不是实际类型。然后在实际调用时通过显式传递或自动推导,替换泛型类型,达到代码复用的目的。在使用泛型的过程中,将要操作的数据类型指定为参数。这样的参数类型在类、接口和方法中分别称为泛型类、泛型接口和泛型方法。
泛型的主要优点是提高代码的可重用性和类型安全性。与传统的形式参数相比,泛型使得编写通用代码更加简洁灵活,提供了处理不同类型数据的能力,进一步增强了Go语言的表达能力和复用性。同时,由于泛型的具体类型是在编译时确定的,因此可以提供类型检查,避免类型转换错误。

3. 泛型和接口的区别{}

在Go语言中,interface{}和泛型都是处理多种数据类型的工具。为了讨论它们的区别,我们先看一下interface{}和泛型的实现原理。

3.1 interface{}实现原理

interface{} 是一个空接口,接口类型中没有方法。由于所有类型都实现了interface{},因此它可用于创建可接受任何类型的函数、方法或数据结构。 interface{}在运行时的底层结构表示为eface,其结构如下所示,主要包含_type和data两个字段。

type eface struct {
    _type *_type
    data  unsafe.Pointer
}
type type struct {
    Size uintptr
    PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers
    Hash uint32 // hash of type; avoids computation in hash tables
    TFlag TFlag // extra type information flags
    Align_ uint8 // alignment of variable with this type
    FieldAlign_ uint8 // alignment of struct field with this type
    Kind_ uint8 // enumeration for C
    // function for comparing objects of this type
    // (ptr to object A, ptr to object B) -> ==?
    Equal func(unsafe.Pointer, unsafe.Pointer) bool
    // GCData stores the GC type data for the garbage collector.
    // If the KindGCProg bit is set in kind, GCData is a GC program.
    // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
    GCData *byte
    Str NameOff // string form
    PtrToThis TypeOff // type for pointer to this type, may be zero
}

_type 是指向 _type 结构的指针,其中包含实际值的大小、种类、哈希函数和字符串表示等信息。 data 是指向实际数据的指针。如果实际数据的大小小于或等于指针的大小,则将数据直接存储到数据字段中;否则,数据字段将存储指向实际数据的指针。
当特定类型的对象被赋值给interface{}类型的变量时,Go语言会隐式执行eface的装箱操作,将_type字段设置为值的类型,将data字段设置为值的数据。例如,当执行语句 var i interface{} = 123 时,Go 会创建一个 eface 结构体,其中 _type 字段代表 int 类型,data 字段代表值 123。
当从interface{}中检索存储的值时,会发生一个拆箱过程,即类型断言或类型判断。此过程需要显式指定预期类型。如果interface{}中存储的值的类型与预期类型匹配,则类型断言将成功,并且可以检索该值。否则,类型断言将会失败,这种情况需要进行额外的处理。

var i interface{} = "hello"
s, ok := i.(string)
if ok {
    fmt.Println(s) // Output "hello"
} else {
    fmt.Println("not a string")
}

可以看出,interface{}通过运行时的装箱和拆箱操作,支持对多种数据类型的操作。

3.2 泛型实现原理

Go核心团队在评估Go泛型的实现方案时非常谨慎。共提交了三个实施方案:

  • 模板方案
  • 词典计划
  • GC形状模板方案

Stenciling方案也是C、Rust等语言实现泛型所采用的实现方案。其实现原理是,在编译期间,根据调用泛型函数时的具体类型参数或约束中的类型元素,为每个类型参数生成泛型函数的单独实现,以保证类型安全和性能最优。然而,这种方法会减慢编译速度。因为当调用多种数据类型时,泛型函数需要为每种数据类型生成独立的函数,这可能会导致编译后的文件非常大。同时,由于CPU缓存未命中、指令分支预测等问题,生成的代码可能无法高效运行。

Dictionaries 方案只为泛型函数生成一个函数逻辑,但添加了一个参数 dict 作为函数的第一个参数。 dict 参数在调用泛型函数时存储类型参数的类型相关信息,并在函数调用期间使用 AX 寄存器(AMD)传递字典信息。这种方案的优点是减少了编译阶段的开销,并且不会增加二进制文件的大小。但增加了运行时开销,无法在编译阶段进行函数优化,并且存在字典递归等问题。

type eface struct {
    _type *_type
    data  unsafe.Pointer
}
type type struct {
    Size uintptr
    PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers
    Hash uint32 // hash of type; avoids computation in hash tables
    TFlag TFlag // extra type information flags
    Align_ uint8 // alignment of variable with this type
    FieldAlign_ uint8 // alignment of struct field with this type
    Kind_ uint8 // enumeration for C
    // function for comparing objects of this type
    // (ptr to object A, ptr to object B) -> ==?
    Equal func(unsafe.Pointer, unsafe.Pointer) bool
    // GCData stores the GC type data for the garbage collector.
    // If the KindGCProg bit is set in kind, GCData is a GC program.
    // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
    GCData *byte
    Str NameOff // string form
    PtrToThis TypeOff // type for pointer to this type, may be zero
}

Go 最终综合了上述两种方案,提出了 GC Shape Stenciling 方案进行通用实现。它以类型的 GC Shape 为单位生成函数代码。具有相同 GC Shape 的类型重用相同的代码(类型的 GC Shape 指的是它在 Go 内存分配器/垃圾收集器中的表示)。所有指针类型都重用 *uint8 类型。对于具有相同 GC Shape 的类型,使用共享的实例化函数代码。该方案还会自动为每个实例化的函数代码添加一个dict参数,以区分具有相同GC Shape的不同类型。

var i interface{} = "hello"
s, ok := i.(string)
if ok {
    fmt.Println(s) // Output "hello"
} else {
    fmt.Println("not a string")
}

3.3 差异

从interface{}和泛型的底层实现原理可以发现,它们的主要区别是interface{}支持在运行时处理不同的数据类型,而泛型支持在编译阶段静态处理不同的数据类型。实际使用中主要有以下区别:
(1) 性能差异:将不同类型的数据分配给接口{}或从接口{}检索不同类型的数据时执行的装箱和拆箱操作成本高昂,并会带来额外的开销。相比之下,泛型不需要装箱和拆箱操作,并且泛型生成的代码针对特定类型进行了优化,避免了运行时性能开销。
(2)类型安全:使用interface{}类型时,编译器无法进行静态类型检查,只能在运行时进行类型断言。因此,某些类型错误可能只能在运行时才能发现。相比之下,Go 的泛型代码是在编译时生成的,因此泛型代码可以在编译时获取类型信息,保证类型安全。

4. 泛型的场景

4.1 适用场景

  • 实现通用数据结构时:通过使用泛型,您可以编写一次代码并在不同的数据类型上重用它。这减少了代码重复并提高了代码的可维护性和可扩展性。
  • 在 Go 中操作原生容器类型时:如果函数使用 Go 内置容器类型(例如切片、映射或通道)的参数,并且函数代码没有对容器中的元素类型做出任何特定假设,使用泛型可以将容器算法与容器中的元素类型完全解耦。在泛型语法出现之前,通常会使用反射来实现,但是反射使得代码可读性较差,无法进行静态类型检查,大大增加了程序的运行时开销。
  • 当不同数据类型的方法实现的逻辑相同时:当不同数据类型的方法功能逻辑相同,唯一区别是输入参数的数据类型时,可以使用泛型来减少代码冗余。

4.2 不适用场景

  • 不要用类型参数替换接口类型:接口支持某种意义上的泛型编程。如果对某些类型的变量的操作只调用该类型的方法,则直接使用接口类型即可,无需使用泛型。例如,io.Reader 使用接口从文件和随机数生成器中读取各种类型的数据。 io.Reader 从代码角度看很容易阅读,效率很高,函数执行效率几乎没有差别,所以不需要使用类型参数。
  • 当不同数据类型的方法实现细节不同时:如果每种类型的方法实现不同,则应使用接口类型而不是泛型。
  • 运行时动态性较强的场景:例如使用switch进行类型判断的场景,直接使用interface{}会有更好的效果。

5. 泛型中的陷阱

5.1 无比较

在Go语言中,类型参数是不允许与nil直接比较的,因为类型参数是在编译时进行类型检查的,而nil是运行时的一个特殊值。由于类型参数的底层类型在编译时是未知的,因此编译器无法确定类型参数的底层类型是否支持与 nil 进行比较。因此,为了维护类型安全并避免潜在的运行时错误,Go语言不允许类型参数与nil直接比较。

type eface struct {
    _type *_type
    data  unsafe.Pointer
}
type type struct {
    Size uintptr
    PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers
    Hash uint32 // hash of type; avoids computation in hash tables
    TFlag TFlag // extra type information flags
    Align_ uint8 // alignment of variable with this type
    FieldAlign_ uint8 // alignment of struct field with this type
    Kind_ uint8 // enumeration for C
    // function for comparing objects of this type
    // (ptr to object A, ptr to object B) -> ==?
    Equal func(unsafe.Pointer, unsafe.Pointer) bool
    // GCData stores the GC type data for the garbage collector.
    // If the KindGCProg bit is set in kind, GCData is a GC program.
    // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
    GCData *byte
    Str NameOff // string form
    PtrToThis TypeOff // type for pointer to this type, may be zero
}

5.2 无效的底层元素

底层元素的类型T必须是基类型,不能是接口类型。

type eface struct {
    _type *_type
    data  unsafe.Pointer
}
type type struct {
    Size uintptr
    PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers
    Hash uint32 // hash of type; avoids computation in hash tables
    TFlag TFlag // extra type information flags
    Align_ uint8 // alignment of variable with this type
    FieldAlign_ uint8 // alignment of struct field with this type
    Kind_ uint8 // enumeration for C
    // function for comparing objects of this type
    // (ptr to object A, ptr to object B) -> ==?
    Equal func(unsafe.Pointer, unsafe.Pointer) bool
    // GCData stores the GC type data for the garbage collector.
    // If the KindGCProg bit is set in kind, GCData is a GC program.
    // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
    GCData *byte
    Str NameOff // string form
    PtrToThis TypeOff // type for pointer to this type, may be zero
}

5.3 无效的联合类型元素

联合类型元素不能是类型参数,非接口元素必须成对不相交。如果有多个元素,则不能包含具有非空方法的接口类型,也不能进行比较或嵌入比较。

var i interface{} = "hello"
s, ok := i.(string)
if ok {
    fmt.Println(s) // Output "hello"
} else {
    fmt.Println("not a string")
}

5.4 接口类型不能递归嵌入

type Op interface{
       int|float 
}
func Add[T Op](m, n T) T { 
       return m + n
} 
// After generation =>
const dict = map[type] typeInfo{
       int : intInfo{
             newFunc,
             lessFucn,
             //......
        },
        float : floatInfo
} 
func Add(dict[T], m, n T) T{}

6. 最佳实践

为了用好泛型,在使用过程中应注意以下几点:

  1. 避免过度概括。 泛型并不适合所有场景,需要仔细考虑适合哪些场景。适当的时候可以使用反射:Go 有运行时反射。反射机制支持一定意义上的泛型编程。如果某些操作需要支持以下场景,可以考虑反射: (1) 对没有方法的类型进行操作,其中接口类型不适用。 (2) 当各个类型的操作逻辑不同时,泛型不适用。一个例子是encoding/json包的实现。由于不希望每个要编码的类型都实现 MarshalJson 方法,因此不能使用接口类型。并且由于不同类型的编码逻辑不同,所以不应该使用泛型。
  2. 明确使用 *T、[]T 和 map[T1]T2,而不是让 T 代表指针类型、切片或映射。 与 C 中的类型参数是占位符并会替换为实际类型不同,Go 中的类型参数 T 的类型是类型参数本身。因此,将其表示为指针、切片、映射等数据类型,在使用过程中会导致很多意想不到的情况,如下所示:
type V interface{
        int|float|*int|*float
} 
func F[T V](m, n T) {}
// 1. Generate templates for regular types int/float
func F[go.shape.int_0](m, n int){} 
func F[go.shape.float_0](m, n int){}
// 2. Pointer types reuse the same template
func F[go.shape.*uint8_0](m, n int){}
// 3. Add dictionary passing during the call
const dict = map[type] typeInfo{
        int : intInfo{},
        float : floatInfo{}
} 
func F[go.shape.int_0](dict[int],m, n int){}

上面的代码会报错:无效操作:ptr(受 *int | *uint 约束的 T 类型变量)的指针必须具有相同的基类型。出现这个错误的原因是T是类型参数,而类型参数不是指针,不支持解引用操作。这可以通过将定义更改为以下内容来解决:

// Wrong example
func ZeroValue0[T any](v T) bool {
    return v == nil  
}
// Correct example 1
func Zero1[T any]() T {
    return *new(T) 
}
// Correct example 2
func Zero2[T any]() T {
    var t T
    return t 
}
// Correct example 3
func Zero3[T any]() (t T) {
    return 
}

概括

总的来说,仿制药的好处可以概括为三个方面:

  1. 类型在编译期间确定,保证类型安全。放进去的就是取出来的。
  2. 可读性得到提高。实际的数据类型在编码阶段就已经明确知道。
  3. 泛型合并了针对同一类型的处理代码,提高了代码复用率,增加了程序的通用灵活性。 然而,泛型并不是一般数据类型所必需的。还是需要根据实际使用情况慎重考虑是否使用泛型。

Leapcell:Go Web 托管、异步任务和 Redis 的高级平台

Go Generics: A Deep Dive

最后给大家介绍一下最适合部署Go服务的平台Leapcell。

1. 多语言支持

  • 使用 JavaScript、Python、Go 或 Rust 进行开发。

2.免费部署无限个项目

  • 只需支付使用费用——无请求,不收费。

3. 无与伦比的成本效益

  • 即用即付,无闲置费用。
  • 示例:25 美元支持 694 万个请求,平均响应时间为 60 毫秒。

4.简化的开发者体验

  • 直观的用户界面,轻松设置。
  • 完全自动化的 CI/CD 管道和 GitOps 集成。
  • 实时指标和日志记录以获取可操作的见解。

5. 轻松的可扩展性和高性能

  • 自动扩展以轻松处理高并发。
  • 零运营开销——只需专注于构建。

在文档中探索更多内容!

Leapcell Twitter:https://x.com/LeapcellHQ

以上是Go 泛型:深入探讨的详细内容。更多信息请关注PHP中文网其他相关文章!

声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
Golang vs. Python:并发和多线程Golang vs. Python:并发和多线程Apr 17, 2025 am 12:20 AM

Golang更适合高并发任务,而Python在灵活性上更有优势。1.Golang通过goroutine和channel高效处理并发。2.Python依赖threading和asyncio,受GIL影响,但提供多种并发方式。选择应基于具体需求。

Golang和C:性能的权衡Golang和C:性能的权衡Apr 17, 2025 am 12:18 AM

Golang和C 在性能上的差异主要体现在内存管理、编译优化和运行时效率等方面。1)Golang的垃圾回收机制方便但可能影响性能,2)C 的手动内存管理和编译器优化在递归计算中表现更为高效。

Golang vs. Python:申请和用例Golang vs. Python:申请和用例Apr 17, 2025 am 12:17 AM

selectgolangforhighpperformanceandcorrency,ifealforBackendServicesSandNetwork程序; selectpypypythonforrapiddevelopment,dataScience和machinelearningDuetoitsverserverserverserversator versator anderticality andextility andextentensivelibraries。

Golang vs. Python:主要差异和相似之处Golang vs. Python:主要差异和相似之处Apr 17, 2025 am 12:15 AM

Golang和Python各有优势:Golang适合高性能和并发编程,Python适用于数据科学和Web开发。 Golang以其并发模型和高效性能着称,Python则以简洁语法和丰富库生态系统着称。

Golang vs. Python:易于使用和学习曲线Golang vs. Python:易于使用和学习曲线Apr 17, 2025 am 12:12 AM

Golang和Python分别在哪些方面更易用和学习曲线更平缓?Golang更适合高并发和高性能需求,学习曲线对有C语言背景的开发者较平缓。Python更适合数据科学和快速原型设计,学习曲线对初学者非常平缓。

表演竞赛:Golang vs.C表演竞赛:Golang vs.CApr 16, 2025 am 12:07 AM

Golang和C 在性能竞赛中的表现各有优势:1)Golang适合高并发和快速开发,2)C 提供更高性能和细粒度控制。选择应基于项目需求和团队技术栈。

Golang vs.C:代码示例和绩效分析Golang vs.C:代码示例和绩效分析Apr 15, 2025 am 12:03 AM

Golang适合快速开发和并发编程,而C 更适合需要极致性能和底层控制的项目。1)Golang的并发模型通过goroutine和channel简化并发编程。2)C 的模板编程提供泛型代码和性能优化。3)Golang的垃圾回收方便但可能影响性能,C 的内存管理复杂但控制精细。

Golang的影响:速度,效率和简单性Golang的影响:速度,效率和简单性Apr 14, 2025 am 12:11 AM

GoimpactsdevelopmentPositationalityThroughSpeed,效率和模拟性。1)速度:gocompilesquicklyandrunseff,ifealforlargeprojects.2)效率:效率:ITScomprehenSevestAndArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdArdEcceSteral Depentencies,增强开发的简单性:3)SimpleflovelmentIcties:3)简单性。

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
1 个月前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳图形设置
1 个月前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您听不到任何人,如何修复音频
1 个月前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.聊天命令以及如何使用它们
1 个月前By尊渡假赌尊渡假赌尊渡假赌

热工具

ZendStudio 13.5.1 Mac

ZendStudio 13.5.1 Mac

功能强大的PHP集成开发环境

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

EditPlus 中文破解版

EditPlus 中文破解版

体积小,语法高亮,不支持代码提示功能

安全考试浏览器

安全考试浏览器

Safe Exam Browser是一个安全的浏览器环境,用于安全地进行在线考试。该软件将任何计算机变成一个安全的工作站。它控制对任何实用工具的访问,并防止学生使用未经授权的资源。

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具