公司内部的 Go 代码规范中限制了每一行代码的宽度。为了满足这个规范,那些太宽的代码行就不可避免地需要换行。换行不是普通的回车就行,如何在换行的同时,保持代码优秀的可读性,笔者根据日常 code review 中看到的各种模式,提出一些建议。
规范和原因
公司的 Go 规范统一要求每一行 Go 代码不能超过 120 个可显示字符的宽度。为什么要限制呢?在 这篇文章 中的描述我是非常赞同的,这里笔者就不再赘述了,读者可以直接参阅。
至于 120 这个数字是怎么来的?我就非常费解了。或许是觉得 80 是在太短,而 160 又太长,所以就取了一个折中值吧。
好,那么既然换行是不可避免的,那么接下来就是要如何换行了。下面笔者针对一些有争议的代码超宽换行的情况,具体说明如何优雅地换行。
函数签名和调用
实际上,除了一些例外情况,那么需要换行的地方,比较有争议的主要都是集中在函数签名 / 函数调用上。
问题提出
下面我举一个例子,比如说我们要定义一个函数,包含以下信息:
- 函数功能: 向一个聊天群里发一个机器人消息, @ 其中的几个人或者是 @all
- 函数入参: context, 群 ID, 机器人 ID, @ 的用户 ID 列表 (空表示 @all), 消息正文
- 函数出参: 发出去的消息 ID, 错误信息
根据上述信息,我们设计一个接口,信息如下:
函数名:
SendRobotMessageToChatGroup
入参:
ctx context.Context
req *SendRobotMessageToChatGroupRequest
GroupID string
RobotID string
AtAll bool
AtUserIDs []string
Text string
出参:
rsp *SendRobotMessageToChatGroupResponse
err error
不要吐槽命名太长, 这里是为了示例。此外,这也很可能是一个 protobuf 生成的 interface
,那么按照很多团队的 pb 命名习惯,确实入参和出参的命名也是非常的长。
OK,如果咱们不换行,这个函数就是这个样子的:
func SendRobotMessageToChatGroup(ctx context.Context, req *SendRobotMessageToChatGroupRequest, opts ...Option) (rsp *SendRobotMessageToChatGroupResponse, err error) {
// ... 函数具体实现 ...
}
上面的这个代码段,你的浏览器上出现了横滚动条了吗?
换行流派
OK,咱们要对上面的函数换行了。其实换行的方式呢,其实有很多流派。这里我列出几种我在 code review 中见过的几种流派(不同流派可以有交叉):
1、函数名与入参允许同行
func SendRobotMessageToChatGroup(ctx context.Context,
req *SendRobotMessageToChatGroupRequest, opts ...Option,
) (rsp *SendRobotMessageToChatGroupResponse, err error) {
// ... 函数具体实现 ...
}
这种模式中,就是按照逗号换行。允许部分入参和函数名放在同一行中。
其实单纯地允许部分入参换行,那感觉很明显地是为了满足代码规范而应试,这是会被诟病的地方,因此,这个流派中,往往会有一个限制,就是 “只有 context.Context
” 类型允许与函数放在同一行。
这么主张的同学,理由是认为 ctx 是许多函数 / 方法所需的默认参数,它也并不是一个关键的入参,因此把它和函数名凑在一起并不会影响整个函数的可读性。
2、入参与出参允许同行
func SendRobotMessageToChatGroup(
ctx context.Context, req *SendRobotMessageToChatGroupRequest,
opts ...Option) (rsp *SendRobotMessageToChatGroupResponse, err error) {
// ... 函数具体实现 ...
}
这种模式中,入参和出参是允许放在同一行的。
这种流派有一个问题,就是函数签名的部分和函数实现正文处于同一锁进,那么当代码密度很高的时候,一眼区分不出函数签名和正文的分水岭。
其实使用这种模式的同学,很多只是纯纯地不喜欢下面的流派 3 而已
3、入参与出参不允许同行
func SendRobotMessageToChatGroup(
ctx context.Context,
req *SendRobotMessageToChatGroupRequest, opts ...Option,
) (rsp *SendRobotMessageToChatGroupResponse, err error) {
// ... 函数具体实现 ...
}
这个流派的重点是:入参和出参不允许放在一行,但是入参的换行比较自由,或者说缺乏统一的指导规范,而这一缺乏规范就是为其他流派所诟病的点,认为这对可读性不佳。
此外前面不是提到流派 2 不喜欢流派 3 嘛,其中一个理由是不喜欢出入参换行以后出现的一个零锁进,认为这破坏了代码块的层级。
4、入参全部独立一行
func SendRobotMessageToChatGroup(
ctx context.Context,
req *SendRobotMessageToChatGroupRequest,
opts ...Option,
) (rsp *SendRobotMessageToChatGroupResponse, err error) {
// ... 函数具体实现 ...
}
这个流派的点呢,则是认为每一个入参都应该独立为一行。这主要是针对 3 的诟病点,认为既然参数如何换行缺乏规范,那么干脆我们就全部换行好了。
这个流派从规范角度,是足以满足的。大部分情况下,也不会出现函数签名过高的情况,以为我们还有另外一个规范:入参不得超过5个,因此这里入参最多盖 5 层楼。
不过呢这个流派被攻击的点也就是这个盖楼,特别是当入参类型名非常短的时候,就特别地难看。
出参?
可能有同学会提问:怎么上面的流派都是入参,没有出参?诚然,我们的规范是要求出参不得超过 3 个,这往往会有两种情况:
- 如果出参多达 3 个,那么这给出的几个参数都是非常简单和直观的类型(否则在 CR 中会被挑战),这种情况也占不了多少宽度,不用换行
- 大部分情况是一到两个,两个的情况下往往第二个类型就是
err error
,占不了多少宽度,而第一个参数加上类型基本上不可能超过 80 个字符
综上,出参都顺利放在同一行内,没有出现需要换行的情况。
笔者观点
不知道读者看了之后还有什么想法(欢迎在评论区告诉我)。诚然,每种流派都有自己的优缺点和道理。各团队可以根据各自的团队习惯制定一个指导。笔者个人使用的基本上是流派 3,但是针对入参应该如何换行的问题,笔者秉承以下原则:
- 如果所有入参拼在一起都没超过 80 个字符,那么各入参之间不换行。满足这一条的话,下面都不用管了
- ctx 可以单独成行,也可以与其他类型放在同一行,但前提是 ctx 必须是入参列表的第一个
- 如果两个变量是成对的,那么可以放在同一行,比如
req
和rsp
,min
和max
,x
和y
等等 - 可变长度参数
...
单独放一行
按照我的这个原则,上面的函数可以写成:
func SendRobotMessageToChatGroup(
ctx context.Context, req *SendRobotMessageToChatGroupRequest,
opts ...Option,
) (rsp *SendRobotMessageToChatGroupResponse, err error) {
// ... 函数具体实现 ...
}
函数调用
上述的流派是针对函数签名的,对于函数调用,换行流派也是类似的,不过还多了一个流派争议:
- 换行了最后一个参数之后,是否要再换行?
这里我举一个例子,日志:
log.ErrorContextf(ctx, "调用 xxxxxx.xxxxxxxx 服务发生错误, 用户 openid 为 %v, 请求参数 %v, 耗时 %v, 错误信息 %v", openID, log.ToJSON(req), time.Since(start), err)
// ... 后续逻辑 ...
最后一个参数不换行的话,就是这个样子的:
log.ErrorContextf(
ctx, "调用 xxxxxx.xxxxxxxx 服务发生错误, 用户 openid 为 %v, 请求参数 %v, 耗时 %v, 错误信息 %v",
openID, log.ToJSON(req), time.Since(start), err)
// ... 后续逻辑 ...
如果换行的话:
log.ErrorContextf(
ctx, "调用 xxxxxx.xxxxxxxx 服务发生错误, 用户 openid 为 %v, 请求参数 %v, 耗时 %v, 错误信息 %v",
openID, log.ToJSON(req), time.Since(start), err,
)
// ... 后续逻辑 ...
不换行派的拥趸认为换行是脱裤子放屁,而换行派的支持者则认为这完成了一个完整的代码块锁进,清晰地指明了一行代码的开始与结束。
笔者是换行派,函数调用中必然换行。因此,笔者不喜欢长长的链式调用,因为这种模式破坏了代码块的层级。(这也是笔者不喜欢 gorm 的原因之一)
例外情况
虽然规范中对代码宽度进行了限制,但是实际上在一些情况下,由于 Go 语言语法的限制会导致换行后语法就不通过的情况,或者是不建议换行的情况:
- 结构体
struct
每个类型后面的 tag,特别是适配 gorm 的那一堆 tag(不喜欢 gorm 的理由 + 1) - 字符串常量,为了保证完整性,不要为了换行而换行,特别是使用反引号括起来的字符串。
- import 行
- 自动生成的代码
参考资料
本文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
原作者: amc,原文发布于腾讯云开发者社区,也是本人的博客。欢迎转载,但请注明出处。
原文标题:《每天学点 Go 规范 - 代码不能写太宽,那么函数该怎么换行呢?》
发布日期:2023-12-06
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。