# 聊聊 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
有疑问加站长微信联系(非本文作者))