公司内部的 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 个,这往往会有两种情况:

  1. 如果出参多达 3 个,那么这给出的几个参数都是非常简单和直观的类型(否则在 CR 中会被挑战),这种情况也占不了多少宽度,不用换行
  2. 大部分情况是一到两个,两个的情况下往往第二个类型就是 err error,占不了多少宽度,而第一个参数加上类型基本上不可能超过 80 个字符

综上,出参都顺利放在同一行内,没有出现需要换行的情况。


笔者观点

不知道读者看了之后还有什么想法(欢迎在评论区告诉我)。诚然,每种流派都有自己的优缺点和道理。各团队可以根据各自的团队习惯制定一个指导。笔者个人使用的基本上是流派 3,但是针对入参应该如何换行的问题,笔者秉承以下原则:

  1. 如果所有入参拼在一起都没超过 80 个字符,那么各入参之间不换行。满足这一条的话,下面都不用管了
  2. ctx 可以单独成行,也可以与其他类型放在同一行,但前提是 ctx 必须是入参列表的第一个
  3. 如果两个变量是成对的,那么可以放在同一行,比如 reqrspminmax, xy 等等
  4. 可变长度参数 ... 单独放一行

按照我的这个原则,上面的函数可以写成:

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 语言语法的限制会导致换行后语法就不通过的情况,或者是不建议换行的情况:

  1. 结构体 struct 每个类型后面的 tag,特别是适配 gorm 的那一堆 tag(不喜欢 gorm 的理由 + 1)
  2. 字符串常量,为了保证完整性,不要为了换行而换行,特别是使用反引号括起来的字符串。
  3. import 行
  4. 自动生成的代码

参考资料


本文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。

原作者: amc,原文发布于腾讯云开发者社区,也是本人的博客。欢迎转载,但请注明出处。

原文标题:《每天学点 Go 规范 - 代码不能写太宽,那么函数该怎么换行呢?》

发布日期:2023-12-06

原文链接:https://cloud.tencent.com/developer/article/2368120

CC BY-NC-SA 4.0 DEED.png


amc
908 声望217 粉丝

电子和互联网深耕多年,拥有丰富的嵌入式和服务器开发经验。现负责腾讯心悦俱乐部后台开发