做网站资质荣誉用的图片,重庆大足网站制作公司,株洲网站建设优化企业,测评网站怎么做从Go 1.18正式引入泛型#xff0c;再到Go 1.21大量泛型函数/类型进入标准库开始已经过去了三年。尽管有着不支持类型特化、不支持泛型方法、实现方式有少量运行时开销、使用指针类型时不够直观等限制#xff0c;泛型编程还是在golang社区和各种项目中遍地开花甚至硕果累累了。…从Go 1.18正式引入泛型再到Go 1.21大量泛型函数/类型进入标准库开始已经过去了三年。尽管有着不支持类型特化、不支持泛型方法、实现方式有少量运行时开销、使用指针类型时不够直观等限制泛型编程还是在golang社区和各种项目中遍地开花甚至硕果累累了。不过也因为泛型功能上的种种限制大多数代码中对其的应用仍然只停留在最基本的层面——仅仅减少重复代码上。但golang泛型的威力远不止如此即使不能进行复杂的类型编程泛型也可以让你的代码变得更安全、更健壮。这篇文章要说的是泛型在强化代码安全性和健壮性方面的应用。强化代码类型安全第一个应用是强化类型安全让类型错误尽可能在编译阶段就全部暴露出来。我手上正好有这样一个系统系统里有A、B、C三种不同类型的消息我们的系统只接收C类型的消息也只发送A或者B类型的消息。每种消息都实现了自己的序列化方法当然为了例子足够简洁这里我做了很大的简化type A struct {ID uint64Name string}func (a *A) Encode() string {return fmt.Sprintf(A: %#v, a)}type B struct {Name stringAge uint32CompanyID uint32}func (b *B) Encode() string {return fmt.Sprintf(B: %#v, b)}type C struct {RequestID stringName string}func (c *C) Encode() string {return fmt.Sprintf(C: %#v, c)}如果意外发送了C类型的消息其他的服务会出现错误。A和B类型的消息只是字段不太一样发送的逻辑是完全相同的所以很自然我们为了DRY原则会写出下面这样的代码type Encoder interface {Encode() string}func SendMessage(msg Encoder) {fmt.Println(msg.Encode())// 其他一些发送数据和校验的逻辑}这是最自然不过的既然逻辑都一样而且A和B的操作确实有一定关联性那么我们就没必要把发送代码写两遍定义一个能同时容纳A和B的接口再把接口作为SendMessage的参数类型即可。这样的代码其实是很不安全的因为C也实现了Encoder接口所以函数可以错误地发送C导致整个系统崩溃。作为泛型时代之前的解决办法我们只能在函数中加上类型断言或者type switch但这会带来不小的运行时开销同时也不能避免代码被误用根本原因在于我们不能控制接口被哪些类型实现因此无法避免一个我们不期望的类型被作为参数传入。有了泛型情况就不一样了我们现在可以在编译阶段就检查出所有误用并且几乎不需要支付运行时开销。然而想实现这个效果会很难你可能会写出这样的代码func SendMessage[T A | B](msg *T) {fmt.Println(msg.Encode())}遗憾的是这样的代码会收获编译错误msg.Encode undefined (type *T is pointer to type parameter, not type parameter)。这是个常见错误了直接取泛型变量的指针大多数时候都会报这种错我以前的博客里有解释过原因这里不再赘述。你也许会灵机一动直接让T本身是指针类型不就行了吗- func SendMessage[T A | B](msg *T) { func SendMessage[T *A | *B](msg T) {fmt.Println(msg.Encode())}这回确实有变化只不过是报错信息变了msg.Encode undefined (type T has no field or method Encode)。这是因为golang规定如果泛型的类型约束是具体的类型那么允许在泛型对象上执行的只有内置的那些加减乘除以及、a[123]这样的操作并且多个类型之间允许的操作会取交集。很遗憾方法调用并不在允许的范围内。对于写惯了其他语言中泛型代码的开发者来说go的这类限制多少有点自废武功的意味。好消息是稍微绕一条路我们也可以达成相同的效果type SendAble[T A | B] interface {*TEncoder}func SendMessage[T A | B, PT SendAble[T]](msg *T) {ptrMsg : PT(msg)fmt.Println(ptrMsg.Encode())}通过引入新的类型约束SendAble我们可以限制参数的类型了。SendAble中的*T表示被约束的类型只能是T的指针而我们限制了T只能是A或者B第二行则包含了Encoder的方法这要求这个指针类型也必须实现了这些方法。新代码中的ptrMsg : PT(msg)则把指针类型转换成了另一个类型参数PTPT拥有Encode方法因此可以正常调用而且编译器在类型推导中不会把*T当成类型参数的指针而是实际的类型T的指针这也避免了最初一版代码的报错。这个模式虽然有些绕但形式相当固定因此很容易掌握你可以当成一些golang的惯用法来看待。现在如果我们传递了C类型的变量到函数中编译器会报错C does not satisfy A | B (C missing in main.A | main.B)。错误描述还是多少有点不尽人意但总比运行时出问题要好得多。除了代码稍微复杂了一些这段代码本质上是调用了泛型的接口虽然编译器做了很多优化但难免还是会因为golang选择的泛型实现方式导致一点点的性能下降。不过比起类型断言来说这点下降影响往往没有前者那么大。这只是使用泛型保护类型安全的一个比较常见也比较简单的例子充分利用泛型特性可以在保证代码简洁的同时让代码更安全。保证常量安全golang中的常量很简单类型只能是整数、浮点、字符串或者以这些为底层类型的自定义类型。对于1、2、3、4、5这样的数字常量golang默认都是int类型。大多数时候这都是我们希望的然而有时候也会带来烦恼func handleOdd(n int)func handleEven(n int)假设我们有两个分别处理奇偶数的函数handleOdd和handleEven函数参数类型自然只能是int但int的取值实在是太宽泛了对于我们的函数来说里面有接近二分之一的值是不可接受的。然而除了运行时检查参数之外我们并没有其他的手段避免错误的值被传入函数尽管这些值很可能是常量人工检查一眼就能发现错误的那种。这和上一节提到的interface意外接受错误的类型一样属于如何从某个大集合中获取满足特定条件的元素的子集只不过讨论的对象从变量变成了常量。在前泛型时代我们只能靠运行时检查解决问题当然在泛型时代因为golang的限制我们也没法解决上面的奇偶数检查问题但对于更具体的实际场景来说泛型刚好能派上用场。例子同样选自生产环境中的系统这个系统里有一个请求发送组件它接受特定格式的数据对象和一个url通过http请求把数据发送至url然后再把返回结果存进特定格式的对象里伪代码如下type ARequest struct{}type AResponse struct{}func (a *ARequest) RequestData() string { return A Request }func (a *AResponse) ResponseData() string { return A Response }type BRequest struct{}type BResponse struct{}func (a *BRequest) RequestData() string { return B Request }func (a *BResponse) ResponseData() string { return B Response }type Requester interface {RequestData() string}type Responser interface {ResponseData() string}type Endpoint stringconst (AURL Endpoint https://a/apiBURL Endpoint https://b/api)func SendRequest(url Endpoint, req Requester) Resopnser {//...}是的发送逻辑是单一且固定的所以我们又使用接口来删除冗余代码只保留一个泛用的SendRequest函数。但问题在于url、request和reponse是严格配对的而我们的函数可以接受他们的任意组合比如我们只允许SendRequest(AURL, ARequest{})但即使写了SendRequest(AURL, BRequest{})代码也能正常通过编译这会导致系统在运行时崩溃或者更遭的遇到一些难以排查的脏数据问题。这就是常量安全问题最常见的一种体现。当然你还是可以在运行时通过字符串比较和类型断言来做校验但代码会很复杂而且有不低的性能开销。但所有参数我们其实在编译时就知道了url都是常量参数类型和返回值类型也是已知的只不过编译器不知道他们之间的配对关系。换句话说只要我们把常量和类型之间的配对关系以某种方式告诉编译器那么就有机会把这些参数校验放在编译时完成根本不需要付出运行时代价也不会让代码变得过于复杂。正好泛型编程中的Phantom Type可以解决这种区分常量以及类型配对的问题。所谓Phantom Type其实就是把一些简单的类型泛型化加上类型参数但这些类型参数只是简单占位和该泛型类型的值无关也不参与实际的计算和处理type PhantomString[T any] stringtype PhantomInt[T, U any] intPhantomString和PhantomInt仍然可以当做字符串和整形来使用但因为加上了类型参数所以即使他们底层的值相同也会因为类型不同而被视为不同的常量const (AP PhantomString[int] helloBP PhantomString[int] hello)// AP和BP因为有完全不同的类型所有即使值相同他们也是不同的const (A PhantomInt[int, uint] 1B PhantomInt[int, float64] 1C PhantomInt[[]rune, string] 1)// 同理ABC也是完全不同的可以看到类型参数本身和类型的值没有任何关联就像幻影一样所以得名Phantom Type。熟悉Haskell或者c模板元编程的开发者应该知道这种技巧通过赋予常量不同的类型我们可以靠类型系统来区分这些常量。而且泛型允许的类型参数可以有多个所以我们还能把类型之间的组合关系绑定到这些类型化的常量上。因此上面的例子可以使用Phantom Type改写// 将URL常量和请求/应答类型进行绑定type Endpoint[Req Requester, Resp Responser] stringconst (AURL Endpoint[*ARequest, *AResponse] https://a/apiBURL Endpoint[*BRequest, *BResponse] https://b/api)func SendMessage[Req Requester, Resp Responser](url Endpoint[Req, Resp], req Req) Resp {// 可以直接把url转回stringfmt.Printf(send request to: %s\n, string(url))var ret Respreturn ret}func main() {ret : SendMessage(AURL, ARequest{})fmt.Println(ret.ResponseData())// 编译报错// SendMessage(AURL, BRequest{})}现在我们为常量绑定了请求和应答的类型常量传入函数后编译器会自动推导出请求参数和返回值必须与常量绑定的类型一致任何不匹配都会报错。比如注释中的表达式in call to SendMessage, type *BRequest of BRequest{} does not match inferred type *ARequest for Req。这次的报错信息也相当清晰。利用Phantom Type的代码整体上也远比运行时检查清晰简洁而且这次我们不会付出任何运行时的性能代价所有检查都在编译代码时就完成了。不过有一点需要注意golang不会自动推导函数返回值的类型这里我们通过Endpoint绑定请求/应答类型能够让编译器推导出所有类型参数但其他场景下得注意这个限制有时候需要明确给出所有类型参数才行这时候代码可能就没那么简洁了。总结本文只是简单介绍了两种最常见的泛型增强代码安全性的用法实际上还有很多实用技巧等待大家去发现。