golang泛型初探

问题

假设有如下场景,我们需要给两个不同的struct实现Equal方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Data1 struct {
Id int
}

func (d *Data1) Equal(other *Data1) bool {
return d.Id == other.Id
}

type Data2 struct {
UniqueId string
}

func (d *Data2) Equal(other *Data2) bool {
return d.UniqueId == other.UniqueId
}

现在我们想定义一个通用接口Equaler,并让两个类型去实现它

1
2
3
4
5
6
type Equaler interface {
Equal(Equaler) bool
}

var _ Equaler = new(Data1) // does not satisfy Equaler
var _ Equaler = new(Data2) // does not satisfy Equaler

但这个代码不能通过编译

原因

对于*Data1Equal方法来说,参数类型为*Data1,并不是接口所要求的Equaler。在golang中参数中的 *Data1类型,并不会自动promote为Equaler接口类型。如果想要让*Data1 实现接口 Equaler,需要对它的Equal方法进行改写

1
2
3
4
5
6
7
8
9
type Data1 struct {
Id int
}

func (d *Data1) Equal(other Equaler) bool {
return d.Id == (other).(*Data1).Id
}

var _ Equaler = new(Data1) // satisfy Equaler

因为任意实现了Equaler接口的类型都能被当作参数传入Equal方法,所以需要在运行时Equaler接口进行强制转化,确保它是 *Data1类型

对于另一些语言来说,这一类型限制在编译期就能完成,以rust为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
trait Equaler {
fn equal(&self, other: &Self) -> bool;
}

struct Data1 {
id: i32,
}

impl Equaler for Data1 {
fn equal(&self, other: &Self) -> bool {
return self.id == b.id;
}
}

struct Data2 {
unique_id: String,
}

impl Equaler for Data2 {
fn equal(&self, other: &Self) -> bool {
return self.unique_id == b.unique_id;
}
}

在rust中,可以通过Self指代当前实现trait的具体类型,对于Data1equal方法来说,SelfData1,对于Data2equal方法来说,Self则是Data2
得益于Self这一概念,我们不需要在运行时去做类型转换也能实现和go代码相同的效果

泛型支持

在go1.18泛型的加持下,可以对上述代码进行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Data1 struct {
Id int
}

func (d *Data1) Equal(other *Data1) bool {
return d.Id == other.Id
}

type Data2 struct {
UniqueId string
}

func (d *Data2) Equal(other *Data2) bool {
return d.UniqueId == other.UniqueId
}

type Equaler[T any] interface {
Equal(T) bool
}

var _ Equaler[*Data1] = new(Data1) // satisfy Equaler[*Data1]
var _ Equaler[*Data2] = new(Data2) // satisfy Equaler[*Data2]

可以这么使用

1
2
3
4
5
6
7
8
9
10
11
func IsEqual[T Equaler[T]](a, b T) bool {
return a.Equal(b)
}

var d1 = &Data1{Id: 1}
var d2 = &Data1{Id: 1}

var d3 = &Data2{UniqueId: "1"}
var d4 = &Data2{UniqueId: "2"}
fmt.Println(IsEqual[*Data1](d1, d2)) //true
fmt.Println(IsEqual[*Data2](d3, d4)) //false

借助类型推导,使用时候也能不用显式指定泛型参数的类型

1
2
fmt.Println(IsEqual(d1, d2)) //true
fmt.Println(IsEqual(d3, d4)) //false

这样我们就得到了一个支持比较任意两个实现Equaler[T]接口的参数是否相等的 IsEqual函数,且一切对于类型的限制和检查都是在编译期完成的
所以虽说IsEqual代码实现看起来可读性较差,但至少它能实现一切我们想要的功能
这个issue中,有对golang是否需要引入Self的概念进行了讨论,但看起来官方还是倾向于使用泛型去达到这一效果•᷄ ᯅ •᷅

Reference

t and equal interface
associated items