简介
自从Go 1.18
版本开始,Go开始支持泛型。然而,本文并不是关于什么是泛型(已经有很多文章了),而是关于何时使用它们的问题。显而易见的TLDR是:当你需要为多个类型编写相同的逻辑时,使用泛型。这听起来很容易理解,但当我们面对实际情况时,我们还有另一个选择:使用接口。那么问题来了:何时使用泛型而不是接口?我们将在本文中使用示例来讨论这个问题。
从一个简单的例子开始
sort.Sort
提供了一种对Interface
类型的切片进行排序的方法。
func Sort(data Interface)
而Interface
被定义为:
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
因此,如果一个array
有Len
,Less
和Swap
方法,它就可以通过sort.Sort
进行排序。
在许多情况下,泛型可以实现相同的目标。而且在大多数情况下,它们具有类似的性能。但是,泛型有一个真正的优势:**你可以直接知道具体类型。**这为什么很有用呢?
为什么我们需要知道具体类型?
让我们看另一个例子。假设你需要编写一个存储控制器,可以从给定的文件中读取/写入数据。你只需要对象能够将其序列化/反序列化为/自JSON。你可以像这样编写代码:
type Object interface {
...
}
type FileStorage struct {
fileName string
}
func (s *FileStorage) Load(ctx context.Context, ch chan<- Object) error {
...
}
func (s *FileStorage) Save(ctx context.Context, ch <-chan Object) error {
...
}
对于Save
,没有问题。由于Object
是一个interface
,因此可以传递任何实现Object
接口的对象。实际类型在Marshal
的运行时已知。然而,对于Load
,问题是在Unmarshall
之前你不知道对象的实际类型。解决这个问题的一种冗长的方法是在你Marshal
和Unmarshall
它时使用一个辅助结构来包装对象和类型信息。这很烦人。
但是让我们想一想,为什么我们需要存储这些信息呢?答案是:**我们不知道对象的具体类型,因为它是一个接口。使用泛型,你可以以一种更加优雅的方式解决这个问题:
type FileStorage[OBJ any] struct {
fileName string
}
func (s *FileStorage[OBJ]) Load(ctx context.Context, ch chan<- OBJ) error {
orig, err := os.ReadFile(s.fileName)
if err != nil {
return err
}
var data []OBJ
if err = json.Unmarshal(orig, &data); err != nil {
return err
}
for _, obj := range data {
select {
case <-ctx.Done():
return ctx.Err()
case ch <- obj:
}
}
return nil
}
因为OBJ
是一个具体类型,所以你可以直接在Load
函数中使用它。你不需要使用包装器结构来存储类型信息。你可能唯一遇到的问题是需要使用具体类型来实例化FileStorage
。但是在大多数情况下,这应该是可行的,因为你的程序中只有几种类型。
结论
在本文中,我们讨论了何时使用泛型而不是接口。一个更具体的最佳实践是:
- 对于控制器对象,使用接口使它们更加灵活。
- 对于数据对象,使用泛型使你的代码更加简洁和优雅。
评论(0)