Go泛型中如何定义适配值与指针混合接收器的类型约束?
背景
本文探讨为同时包含值接收器与指针接收器的类型定义类型约束的规范、恰当或最佳实践。先明确几个术语:
- 类型约束(Constraint):作为类型实参传入的接口类型,限制类型参数必须满足接口的所有元素要求。
- 类型集合(Type set):接口内所有元素类型集合的交集;非接口类型的类型集合仅包含自身。
- 近似元素(Approximation element):Go中用
~表示,意为“所有底层类型为X的类型”,前提是X的底层类型为其自身。
问题描述
通过示例说明问题。先看以下代码:
// 类型A与指针接收器方法 type A struct { s string } func (a *A) String() string { return a.s } // 类型约束C type C interface { A String() string } func show [T C] (x T) { fmt.Println(x.String()) } func main() { a := A{s:"a"} show(a) }
运行代码会报错:
A does not satisfy C (method String has no pointer receiver)
显然,String()方法的接收器是*A而非A本身。将约束C改为基于*A,并传入指针即可解决:
// 类型约束C type C interface { *A String() string } // 调整main函数 func main() { // ... show(&a) }
但当类型A同时拥有值接收器和指针接收器方法时,问题会变得复杂:
// 类型A,值接收器与指针接收器方法 type A struct { s string } func (a A) Show() { fmt.Println(a.s) } func (a *A) String() string { return a.s } // 类型约束C:??? type C interface { A Show() String() string } func show1 [T C] (x T) { fmt.Println(x.String()) } func main() { a := A{s:"a"} show(a) }
约束C要求所有实现C的类型必须同时拥有Show()和String()方法,但Show()定义在A上,String()定义在*A上,二者无法通过单一约束直接调和。
尝试的解决方案
我尝试了多种方案,分类如下:
- 使用近似元素
- 使用类型联合
- 调整类型参数
近似元素方案
第一种方案是使用近似元素,但它仅描述所有底层类型为T的类型集合,T的指针类型并不属于该集合:
// 类型约束C type C interface { ~struct{s string} // 这里不能用A,因为A的底层类型就是自身 :) String() string Show() }
类型联合方案
另一种方案是尝试使用类型联合,但Go泛型要求类型集合中的每个成员都必须实现约束的所有方法,因此无法自动将String()关联到*A、Show()关联到A:
// 类型约束C type C interface { A | *A // A does not satisfy C (method String has pointer receiver) String() string Show() }
类型参数调整方案
最后一种方案是放弃单一约束,但尝试定义两个不同约束时也遇到了问题:
func show [P interface{ String() string}, T interface{Show()}] (x T) { x.Show() p := P(&x) // 无法将&T类型的值转换为P类型 fmt.Println(p.String()) }
尽管*T满足P的约束,但编译器无法认可&x可以转换为P类型。
核心问题:在Go中如何为包含多种接收器的类型定义约束?是否必须定义不同的约束,还是可以用单一约束实现?是否存在遗漏的语法技巧来关联类型与其指针?
补充场景说明
为了更清晰地说明问题,补充一个真实场景:
- 假设要编写一个数据库接口,包含多张表。
- 每张表的列对应结构体的字段。
- 为了接口灵活,编写泛型接口,调用时返回行类型
T。 - 同时希望行类型
T能提供查询字符串,因此在*T上定义了Query() string方法。
选择指针接收器是遵循Go的推荐实践:
如果接收器是大型结构体或数组,指针接收器效率更高。多大算“大型”?可以类比为将所有元素作为参数传入方法的开销,如果这个开销过大,那么作为接收器也过大。
(以上内容来自Go代码评审建议)
注意:在非泛型代码中,无论传入值还是指针,都可以调用值接收器和指针接收器方法,但泛型代码默认不支持这一特性,这也是提出该问题的原因之一。
内容的提问来源于stack exchange,提问作者Micrified

