Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > Go [0]func(T)

Go 泛型中的 [0]func(T)的实现

作者:Kevin666

本文主要介绍了Go 泛型中的 [0]func(T)的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

很多 Go 泛型库会在一个看似“空”的结构体里塞一个很奇怪的字段:0 长度数组,元素类型是 函数且带类型参数。这不是炫技,而是在用编译器帮你“堵住误用”。

1. 先从一个真实需求出发:可插拔的“比较策略”

假设你在写一个泛型工具:对切片做去重、查找、或比较;你希望用户可以自定义“怎么判断两个元素相等”。

package main

type Eq[T any] interface {
    Equal(a, b T) bool
}

type Finder[T any, E Eq[T]] struct {
    eq E
}

func (f Finder[T, E]) IndexOf(xs []T, target T) int {
    for i, v := range xs {
        if f.eq.Equal(v, target) {
            return i
        }
    }
    return -1
}

你还想给一个默认实现:当 T 可比较时,直接用 ==

type DefaultEq[T comparable] struct{}

func (DefaultEq[T]) Equal(a, b T) bool { return a == b }

到这里看起来很完美,对吧?但实际上它埋了两个“类型安全”层面的坑。

2. 坑 A:不同T的默认策略“长得一样”,可能引发误用

DefaultEq[int]DefaultEq[string] 在内存布局上都是空结构体struct{}
空结构体最大的特点:没有任何字段,于是很多时候“看起来完全相同”。

你在项目里做大规模泛型封装时,很可能出现这样的情况:

直观理解:当一个泛型类型实例化后仍然是空的,类型系统可约束的东西就少了,误用空间就大。

我们想要的是:
DefaultEq[int]DefaultEq[string] 在“结构上就不一样”,从而尽量把错误挡在编译期。

3. 坑 B:策略对象被比较、被当成 map key —— 这通常是 bug

策略对象(比如“比较器”“哈希器”“排序规则”)一般只承载行为,不承载数据。
如果它是一个可比较的空结构体,那么下面这些“看起来合理但通常有坑”的写法就能通过编译:

// 例:把策略当成 key 来缓存某些结果
// map[DefaultEq[int]]something  // 这在“空结构体可比较”的情况下是可行的

更常见的是你写了一个容器/缓存结构,未来某个改动把策略对象塞进 struct,然后有人顺手就 == 比较整个 struct,结果“比较成功”但语义完全不对,bug 非常隐蔽。

我们希望:
策略对象最好不要支持 == 比较,这样一旦有人试图比较就立刻编译失败。

4. 解决方案:放一个“0 字节但类型强绑定”的字段

我们把默认策略改成这样:

type SaferDefaultEq[T comparable] struct {
    _ [0]func(T)
}

func (SaferDefaultEq[T]) Equal(a, b T) bool { return a == b }

这行字段同时完成两件事:

4.1[0]...:0 长度数组,不占内存(零运行时成本)

[0]X 的大小永远是 0,不管 X 是什么。
所以这个字段不会让 struct 变大,不会增加分配成本,也不会影响逃逸分析结果——它几乎纯粹是“给类型系统看的”。

4.2func(T):函数类型不可比较 → struct 也不可比较

在 Go 里:

于是:

var a, b SaferDefaultEq[int]
// _ = (a == b) // 编译错误:该类型不可比较

这就把“策略对象被拿去比较/当 key”这种误用直接扼杀在编译期。

4.3func(T)里带T:把类型参数“烙”进结构里

重点是 func(T) 这个字段类型包含了类型参数 T
T 不同,字段类型也不同:

它们在结构层面不再“长得一样”,从而更难在中间层被当成可互换的东西(尤其是当你有很多 wrapper、type alias、泛型适配器时,这种“强区分”很值钱)。

5. 用一组“能看懂就懂”的对照实验

5.1 对照:空结构体策略可以比较(常常不想要)

type PlainDefaultEq[T comparable] struct{}
func (PlainDefaultEq[T]) Equal(a, b T) bool { return a == b }

func compareStrategy() {
    var x, y PlainDefaultEq[int]
    _ = (x == y) // 可以编译:但比较它通常没有意义
}

5.2 加上_[0]func(T)后:比较直接被禁止

type StrictDefaultEq[T comparable] struct {
    _ [0]func(T)
}
func (StrictDefaultEq[T]) Equal(a, b T) bool { return a == b }

func compareStrategy2() {
    var x, y StrictDefaultEq[int]
    // _ = (x == y) // 编译失败:不可比较(更安全)
}

5.3 依然 0 成本:对象大小不变(仍然等于 0)

空结构体大小是 0;加了 [0]func(T) 仍然是 0。
(你可以用 unsafe.Sizeof 验证:两者一般都为 0;放进另一个 struct 时也不会额外占空间——0 长度数组不会贡献布局。)

6. 为什么不直接用其他“绑定 T 的办法”?

你可能会问:我也可以写这些啊:

6.1_ T—— 不行,会占空间且需要值

type Tag[T any] struct { _ T } // 会占用 T 的大小,完全不零成本

6.2_ *T—— 会占一个指针大小

type Tag[T any] struct { _ *T } // 通常 8 字节(64 位)

6.3struct{}—— 0 成本,但绑定不够“强”

type Tag[T any] struct { _ struct{} } // 0 成本,但没把 T 烙进字段类型

[0]func(T) 同时满足:

属于“一个字段,三个收益”。

7. 什么时候你应该用这种写法?

适用场景(很典型):

  1. 策略/配置/适配器对象:比如 Equal/Hash/Compare/Encode/Decode 策略
  2. 你不希望它被 == 比较:比较往往无意义且易隐藏 bug
  3. 你希望不同类型参数的实例化在类型层面强区分:避免在多层封装里被“当成一样的空壳”

不适用场景:

8. 一个更完整的“使用姿势”示例(仍然全新)

package main

type Eq[T any] interface {
    Equal(a, b T) bool
}

type StrictDefaultEq[T comparable] struct {
    _ [0]func(T)
}

func (StrictDefaultEq[T]) Equal(a, b T) bool { return a == b }

type Finder[T any, E Eq[T]] struct {
    eq E
}

func (f Finder[T, E]) Contains(xs []T, target T) bool {
    for _, v := range xs {
        if f.eq.Equal(v, target) {
            return true
        }
    }
    return false
}

func main() {
    f := Finder[int, StrictDefaultEq[int]]{eq: StrictDefaultEq[int]{}}
    _ = f.Contains([]int{1, 2, 3}, 2)
}

这个例子里:

9. 一句话总结

_ [0]func(T) 是一种 “零字节字段 + 强类型绑定 + 禁止比较” 的组合技巧。
它用极低的成本换来更强的编译期约束,尤其适合做泛型库里的“默认策略/类型标签/行为适配器”。

到此这篇关于Go 泛型中的 [0]func(T)的实现的文章就介绍到这了,更多相关Go [0]func(T)内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文