Go 泛型:深入探讨
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. 最佳实践
为了用好泛型,在使用过程中应注意以下几点:
- 避免过度概括。 泛型并不适合所有场景,需要仔细考虑适合哪些场景。适当的时候可以使用反射:Go 有运行时反射。反射机制支持一定意义上的泛型编程。如果某些操作需要支持以下场景,可以考虑反射: (1) 对没有方法的类型进行操作,其中接口类型不适用。 (2) 当各个类型的操作逻辑不同时,泛型不适用。一个例子是encoding/json包的实现。由于不希望每个要编码的类型都实现 MarshalJson 方法,因此不能使用接口类型。并且由于不同类型的编码逻辑不同,所以不应该使用泛型。
- 明确使用 *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 }
概括
总的来说,仿制药的好处可以概括为三个方面:
- 类型在编译期间确定,保证类型安全。放进去的就是取出来的。
- 可读性得到提高。实际的数据类型在编码阶段就已经明确知道。
- 泛型合并了针对同一类型的处理代码,提高了代码复用率,增加了程序的通用灵活性。 然而,泛型并不是一般数据类型所必需的。还是需要根据实际使用情况慎重考虑是否使用泛型。
Leapcell:Go Web 托管、异步任务和 Redis 的高级平台
最后给大家介绍一下最适合部署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中文网其他相关文章!

热AI工具

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

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

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

Video Face Swap
使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热门文章

热工具

记事本++7.3.1
好用且免费的代码编辑器

SublimeText3汉化版
中文版,非常好用

禅工作室 13.0.1
功能强大的PHP集成开发环境

Dreamweaver CS6
视觉化网页开发工具

SublimeText3 Mac版
神级代码编辑软件(SublimeText3)

OpenSSL,作为广泛应用于安全通信的开源库,提供了加密算法、密钥和证书管理等功能。然而,其历史版本中存在一些已知安全漏洞,其中一些危害极大。本文将重点介绍Debian系统中OpenSSL的常见漏洞及应对措施。DebianOpenSSL已知漏洞:OpenSSL曾出现过多个严重漏洞,例如:心脏出血漏洞(CVE-2014-0160):该漏洞影响OpenSSL1.0.1至1.0.1f以及1.0.2至1.0.2beta版本。攻击者可利用此漏洞未经授权读取服务器上的敏感信息,包括加密密钥等。

在BeegoORM框架下,如何指定模型关联的数据库?许多Beego项目需要同时操作多个数据库。当使用Beego...

后端学习路径:从前端转型到后端的探索之旅作为一名从前端开发转型的后端初学者,你已经有了nodejs的基础,...

GoLand中自定义结构体标签不显示怎么办?在使用GoLand进行Go语言开发时,很多开发者会遇到自定义结构体标签在�...

Go语言中使用RedisStream实现消息队列时类型转换问题在使用Go语言与Redis...

Go语言中用于浮点数运算的库介绍在Go语言(也称为Golang)中,进行浮点数的加减乘除运算时,如何确保精度是�...

Go爬虫Colly中的Queue线程问题探讨在使用Go语言的Colly爬虫库时,开发者常常会遇到关于线程和请求队列的问题。�...

本文介绍如何在Debian系统上配置MongoDB实现自动扩容,主要步骤包括MongoDB副本集的设置和磁盘空间监控。一、MongoDB安装首先,确保已在Debian系统上安装MongoDB。使用以下命令安装:sudoaptupdatesudoaptinstall-ymongodb-org二、配置MongoDB副本集MongoDB副本集确保高可用性和数据冗余,是实现自动扩容的基础。启动MongoDB服务:sudosystemctlstartmongodsudosys
