当前位置: 首页 > 科技观察

Go1.18泛型好、坏还是丑?

时间:2023-03-19 16:48:29 科技观察

大家好,我是程序员幽灵。Go泛型是固定的,什么是好的使用场景,什么是坏的使用场景,或者哪个用法看起来很难看?这篇文章讲的就是这个问题。1简介泛型很棒,而且Go比以往任何时候都更加方便。但是类似于非常有用的渠道,我们不应该仅仅因为它们存在就到处使用它们。泛型除了用于数据结构之外,还有其他很好的应用场景。当然,也有一些不好的用例,例如通用记录器。也有可行的解决方案,但相当丑陋,还有一些非常丑陋的东西。让我们分别看一个例子!2个好的用例我真正梦想在Go中做的,我认为我现在终于可以做的是CRUD端点的通用提供程序:typeModelinterface{ID()string}typeDataProvider[MODELModel]interface{FindByID(idstring)(MODEL,error)List()([]MODEL,error)Update(idstring,modelMODEL)errorInsert(modelMODEL)errorDelete(idstring)error}这是一个很大的接口,你可以根据具体用例的需要缩短它,不过,为了完整起见,我们现在将这样写。现在您可以定义一个使用DataProvider的HTTP处理程序:,err:=h.dataProvider.FindByID(id)iferr!=nil{//errorhandlingherereturn}err=json.NewEncoder(rw).Encode(model)iferr!=nil{//errorhandlingherereturn}}如你所见,我们可以为每个方法执行一次,然后我们就完成了。我们甚至可以在事物的另一端创建一个客户端,我们只需为基本方法实现一次。为什么我们在这里使用泛型而不是简单地使用我们定义的模型接口?在这里,泛型比使用模型类型本身有一些优势:使用泛型方法,DataProvider根本不需要了解模型,也不需要实现它。它可以简单地提供非常强大的具体类型(但对于简单的用例仍然是抽象的)我们可以扩展这个解决方案并使用具体类型进行操作。让我们看看插入或更新验证器会是什么样子。typeHTTPHandler[MODELany]struct{dataProviderDataProvider[MODEL]InsertValidatorfunc(newMODEL)errorUpdateValidatorfunc(oldMODEL,newMODEL)error}是该验证器中通用方法的真正优势所在。我们将解析HTTP请求,如果定义了自定义InsertValidator,那么我们可以使用它来验证模型检出,我们可以以类型安全的方式进行并使用具体模型:typeUserstruct{FirstNamestringLastNamestring}funcInsertValidator(uUser)error{ifu.FirstName==""{...}ifu.LastName==""{...}}所以我们有一个通用的处理程序,我们可以使用自定义回调进行调整,从而获得有效的负载。没有类型转换。没有地图。只有结构本身!3糟糕的应用场景让我们看一个通用记录器的例子:typeGenericLogger[Tany]interface{WithField(string,string)TInfo(string)}这本身不是很有用。有更简单的方法可以将键值字符串对添加到记录器,并且没有记录器(据我所知)实际实现此接口。我们也不需要新的日志记录标准。如果我们想使用logrus[1],我们必须这样做:typeGenericLogger[Tany,FIELDmap[string]interface{}]interface{WithFields(M)TInfo(string)}由logrusLogger实现来完成。但是,让我们考虑在实际结构中使用它,例如某种处理程序:typeMessageHandler[TGenericLogger[T],FIELDmap[string]interface{}]struct{loggerGenericLogger[T,FIELD]}以便在结构中使用它记录器,我们需要使我们的结构通用,这仅适用于记录器。如果MessageHandler本身正在处理一条通用消息,那将成为第三种类型参数!到目前为止,甚至没有办法将它分配给具有泛型的变量。所以,虽然我们可以用一个接口来表示这个记录器很好,但我实际上不建议这样做。而我最喜欢的日志库(zap[2]),由于其字段的性质,甚至不能用它来表示。4丑陋的场景当我使用泛型时,我发现缺乏对在方法中引入新的泛型参数的支持。虽然这可能有充分的理由,但它确实需要一些解决方法。假设我们想要将一个映射缩减为一个整数。理想情况下,我们将通过使用返回新通用参数的方法来执行此操作,然后我们可以简单地提供一个mapreduce函数。那么当我们仍然想一般地缩小该地图时我们该怎么办?由于没有方法,让我们创建一个:for_,v:=range{values=append(values,v)}returnvalues}funcReduce[KEYcomparable,VALUEany,RETURNany](gGenericMap[KEY,VALUE],callbackfunc(RETURN,KEY,VALUE"KEYcomparable,VALUEany,RETURNany")返回)RETURN{varrRETURNfork,v:=rangeg{r=callback(r,k,v)}returnr}GenericMap成为第一个参数或者我们的Reduce函数。在这种情况下,您可以使用任何类型的地图作为第一个参数,而不是GenericMap。但是,我想指出的是,如果该方法本身是GenericMap的一部分,那就太好了。即使不是,我们仍然可以模仿这种行为。总的来说,我可能仍然会在某些用例中使用这种模式,即使它实际上很丑陋。5真的很丑有时你可能想使用工厂模式,它为你提供了诸如DataProviders之类的东西。您可能希望在动态注册的端点上获取提供程序。所以你可以这样做:name,typ:reflect.TypeOf(m)}]if!has{returnnil,false}returnprov.(DataProvider[MODEL]),true}funcRegisterProvider[MODELModel](factory*DataProviderFactory,namestring,pDataProvider[MODEL]“MODELModel”){varmMODELfactory.dataProviders[providerKey{name:name,typ:reflect.TypeOf(m)}]=p}虽然这行得通,而且可能行得通,但它很丑陋。它结合了丑陋的(反射)和更丑陋的东西(泛型)。虽然从技术上讲这应该是类型安全的,但它仍然很难看,因为我们的复合键有一个名称和一个反映的类型。我很纠结是否应该把它放在生产代码的任何地方。6总结虽然我喜欢泛型,但我发现很难取得平衡,尤其是在开始的时候。所以我们需要确保记住它们为什么存在,在什么情况下我们应该使用它们,以及我们应该避免它们!原文链接:https://itnext.io/golang-1-18-generics-the-good-the-bad-the-ugly-5e9fa2520e76参考文献[1]logrus:https://github.com/sirupsen/logrus[2]zap:https://github.com/uber-go/zap本文转载自微信“鬼”,可通过以下二维码关注。转载本文请联系有鬼公众号。