前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >每天学点 Go 规范 - 代码不能写太宽,那么函数该怎么换行呢?

每天学点 Go 规范 - 代码不能写太宽,那么函数该怎么换行呢?

原创
作者头像
amc
修改2023-12-06 17:48:58
9820
修改2023-12-06 17:48:58
举报
文章被收录于专栏:后台全栈之路后台全栈之路

公司内部的 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,如果咱们不换行,这个函数就是这个样子的:

代码语言:go
复制
func SendRobotMessageToChatGroup(ctx context.Context, req *SendRobotMessageToChatGroupRequest, opts ...Option) (rsp *SendRobotMessageToChatGroupResponse, err error) {
    // ... 函数具体实现 ...
}

上面的这个代码段,你的浏览器上出现了横滚动条了吗?

换行流派

OK,咱们要对上面的函数换行了。其实换行的方式呢,其实有很多流派。这里我列出几种我在 code review 中见过的几种流派(不同流派可以有交叉):

1、函数名与入参允许同行
代码语言:go
复制
func SendRobotMessageToChatGroup(ctx context.Context,
    req *SendRobotMessageToChatGroupRequest, opts ...Option,
) (rsp *SendRobotMessageToChatGroupResponse, err error) {
    // ... 函数具体实现 ...
}

这种模式中,就是按照逗号换行。允许部分入参和函数名放在同一行中。

其实单纯地允许部分入参换行,那感觉很明显地是为了满足代码规范而应试,这是会被诟病的地方,因此,这个流派中,往往会有一个限制,就是 “只有 context.Context” 类型允许与函数放在同一行。

这么主张的同学,理由是认为 ctx 是许多函数 / 方法所需的默认参数,它也并不是一个关键的入参,因此把它和函数名凑在一起并不会影响整个函数的可读性。

2、入参与出参允许同行
代码语言:go
复制
func SendRobotMessageToChatGroup(
    ctx context.Context, req *SendRobotMessageToChatGroupRequest,
    opts ...Option) (rsp *SendRobotMessageToChatGroupResponse, err error) {
    // ... 函数具体实现 ...
}

这种模式中,入参和出参是允许放在同一行的。

这种流派有一个问题,就是函数签名的部分和函数实现正文处于同一锁进,那么当代码密度很高的时候,一眼区分不出函数签名和正文的分水岭。

其实使用这种模式的同学,很多只是纯纯地不喜欢下面的流派 3 而已

3、入参与出参不允许同行
代码语言:go
复制
func SendRobotMessageToChatGroup(
    ctx context.Context,
    req *SendRobotMessageToChatGroupRequest, opts ...Option,
) (rsp *SendRobotMessageToChatGroupResponse, err error) {
    // ... 函数具体实现 ...
}

这个流派的重点是:入参和出参不允许放在一行,但是入参的换行比较自由,或者说缺乏统一的指导规范,而这一缺乏规范就是为其他流派所诟病的点,认为这对可读性不佳。

此外前面不是提到流派 2 不喜欢流派 3 嘛,其中一个理由是不喜欢出入参换行以后出现的一个零锁进,认为这破坏了代码块的层级。

4、入参全部独立一行
代码语言:go
复制
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. 可变长度参数 ... 单独放一行

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

代码语言:go
复制
func SendRobotMessageToChatGroup(
    ctx context.Context, req *SendRobotMessageToChatGroupRequest,
    opts ...Option,
) (rsp *SendRobotMessageToChatGroupResponse, err error) {
    // ... 函数具体实现 ...
}

函数调用

上述的流派是针对函数签名的,对于函数调用,换行流派也是类似的,不过还多了一个流派争议:

  • 换行了最后一个参数之后,是否要再换行?

这里我举一个例子,日志:

代码语言:go
复制
    log.ErrorContextf(ctx, "调用 xxxxxx.xxxxxxxx 服务发生错误, 用户 openid 为 %v, 请求参数 %v, 耗时 %v, 错误信息 %v", openID, log.ToJSON(req), time.Since(start), err)
    // ... 后续逻辑 ...

最后一个参数不换行的话,就是这个样子的:

代码语言:go
复制
    log.ErrorContextf(
        ctx, "调用 xxxxxx.xxxxxxxx 服务发生错误, 用户 openid 为 %v, 请求参数 %v, 耗时 %v, 错误信息 %v",
        openID, log.ToJSON(req), time.Since(start), err)
    // ... 后续逻辑 ...

如果换行的话:

代码语言:go
复制
    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
CC BY-NC-SA 4.0 DEED.png

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 规范和原因
  • 函数签名和调用
    • 问题提出
      • 换行流派
        • 1、函数名与入参允许同行
        • 2、入参与出参允许同行
        • 3、入参与出参不允许同行
        • 4、入参全部独立一行
        • 出参?
    • 笔者观点
    • 函数调用
    • 例外情况
    • 参考资料
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档