聊聊 Go 中没排面的元组

symphony09 · · 2968 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

# 聊聊 Go 中没排面的元组 ## 元组是什么 元组(tuple)和列表等一样,也是一种数据类型。 它和列表不同点在于,列表是元素类型固定,而长度不固定。元组则恰恰相反,长度固定,而元素类型不固定。 对于有 python 或 js 编程经验的人或许对元组比较熟悉,但是对于纯 gophers 来说可能就比较陌生了。与在日常 go 编程中,切片(slice),映射(map)随处可见,但元组却无迹可寻。 其实这也难怪,因为 go 根本没有像内置 slice 和 map 一样内置元组,实在是没有排面。 这篇文章的主旨呢,就是来聊聊这个没排面的元组。 与其他文章不同的是,这篇文章里除了介绍元组的几种实现方式,还会聊聊元组如何工作,可以解决什么问题。在我看来后者是更有意义的。 ## 天生我材必有用 如果有人对你说,公司离了你照样转。也许 ta 说的对,但也不必灰心丧气,有句话说的是天生我材必有用。 只要能在某些领域,某些场景,甚至某些小事上做的更好,那就是价值所在。 不小心扯远了,还是来聊聊元组。 显然,go 离了元组照样好好的转,或许这也是没有内置元组的原因,毕竟大道至简嘛。 但是元组也有其一技之长,在熟悉元组后,某些场景下我们能更好地实现逻辑。 ### 元组的一技之长 以我的理解来说,元组的一技之长就是简化组合。 可以从一个坐标点的实现来看看元组是怎么简化组合的。 一般实现: ```go type Point struct{ X, Y int } func PrintPoint1() { point := Point{1, 2} x, y := point.X, point.Y fmt.Printf("point at { x: %d, y: %d }\n", x, y) } // output: point at { x: 1, y: 2 } ``` 以上已经是比较简略的实现,但是如果用元组来表达呢 元组伪代码: ``` func PrintPoint2() { point := (1, 2) x, y := point fmt.Printf("point at { x: %d, y: %d }\n", x, y) } ``` 最大的不同就是不用再声明 Point 类型,整整少写了一行代码(笑) ### So What 如果单单是表达一个 x 和 y 坐标的 point,似乎元组相比结构体并没有多大优势。 但是,如果还有带 z 坐标的 point,还有带名字的 point 呢,还有既带 z 坐标又带名字的 point 呢,还有坐标值类型为浮点数的 point 呢? 当然可以把这些字段都放在一个结构体中,但这对只处理整形数 x, y 坐标值的代码来说数据是冗余的,零值可也占空间。 或者声明不同结构体,但就要费脑筋来取名区分不同结构体。时间一长还可能会产生很多只在某几段代码使用的结构体。 但是从元组角度来看,不是 point 表达了组合元素,而是组合元素表达了 point, 只要元素确定,叫 Point 还是 PointWithXY 并无所谓。 ### 实际场景 突然想起聊聊元组倒也并不是空穴来风,最近我用 go 实现了一个事件总线处理库[eventd](https://github.com/symphony09/eventd),大致使用方法如下: ```go bus := new(EventBus[int]) cancel, err := bus.Subscribe(func(event string, i int) bool { // do something return true }, On("put")) // 订阅 put 事件 //... bus.Emit("put", 0) // 触发事件 ``` 可以看到,这里回调方法的签名可以通过事件对象类型推导出来。 但是在使用时有一个问题: 在传递事件消息时,事件对象可能不止一个。举个例子,用户编辑了一篇文章,那么主体就是用户,此外还有客体文章。主体和客体的类型可能会变,但是主体和客体的有序对关系不会变化。 我不想为各种主客体组合声明结构体,也不想实现多个不同数量泛型参数的 event bus。 就在这时,我想起了元组。如果用元组实现会是怎么样呢? ```go bus := new(EventBus[Tuple[User, Blog]]) ``` 这样一来,对于 event bus 来说,永远只有一个事件对象,同时事件对象又可以是不同对象的灵活组合。 但是另一个问题来了,元组从哪里来? ## 元组的3种实现 ### 函数多返回值 其实 go 并不是全无元组的影子。前面伪代码中 `x, y := point`,只要在 point 后加个圆括号不就是函数嘛。 按这个思路实现一下伪代码,差不多是下面这样子: ```go func PrintPoint2() { point := func() (int, int) { return 1, 2 } x, y := point() fmt.Printf("point at { x: %d, y: %d }\n", x, y) } ``` 好像差点意思,用泛型加强下表达能力。 ### 泛型函数 ```go type Pair[L, R any] func() (L, R) func TwoTuple[T1, T2 any](v1 T1, v2 T2) Pair[T1, T2] { return func() (T1, T2) { return v1, v2 } } func PrintPoint2() { point := TwoTuple(1, 2) x, y := point() fmt.Printf("point at { x: %d, y: %d }\n", x, y) } ``` 还可以为 Pair 实现方法: ```go // ... func (p Pair[L, R]) Right() R { _, r := p() return r } // ... func PrintPoint2() { point := TwoTuple(1, 2) x, y := point() fmt.Printf("point at { x: %d, y: %d }\n", x, y) point2 := TwoTuple(1, 2.33) fmt.Printf("point at { y: %f }\n", point2.Right()) } ``` 从上面这些代码可以看到,只要声明一次二元组,那么就能表达任意两种元素的组合。 ### 泛型结构体 像上面通过泛型函数来实现元组还是有点花里胡哨了,其实有了泛型之后,直接用结构体表达元组更加简单。 目前比较多 star 的元组实现也这样实现的。 ```go type Tuple2[T1, T2 any] struct { V1 T1 V2 T2 } ``` 这样直接通过访问成员属性就可以读写元素。 与上面泛型函数实现相比,这种实现需要增加一个类似 Values 的方法来一下来实现`x,y := ...`的效果。但可以避免函数带来的一些副作用,比如说内存逃逸。 以上三种方法都可以模拟元组,但是也有共同的缺点,对于每种长度的元组都需要声明一下。好在一般情况下不会使用 9 个元素以上的元组,这个缺点也算可以接受。 ## 总结 在 go 中没有排面的元组也能在特定场景发光发热,泛型的引入也使得第三方元组实现真正实用起来。 最后为我的 github 项目做个宣传:https://github.com/symphony09/eventd

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2968 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传