博客 / 詳情

返回

每天學點 Go 規範 - 代碼不能寫太寬,那麼函數該怎麼換行呢?

公司內部的 Go 代碼規範中限制了每一行代碼的寬度。為了滿足這個規範,那些太寬的代碼行就不可避免地需要換行。換行不是普通的回車就行,如何在換行的同時,保持代碼優秀的可讀性,筆者根據日常 code review 中看到的各種模式,提出一些建議。

  • 上一篇文章:每天學點 Go 規範 - 函數傳參時,struct 應該傳值還是引用

規範和原因

公司的 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. 自動生成的代碼

參考資料

  • 函數簽名的概念
  • 每行80個字符在今天(2020年)依然合理!
  • Is this orm for go really a good idea?

本文章採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。

原作者: amc,原文發佈於騰訊雲開發者社區,也是本人的博客。歡迎轉載,但請註明出處。

原文標題:《每天學點 Go 規範 - 代碼不能寫太寬,那麼函數該怎麼換行呢?》

發佈日期:2023-12-06

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

CC BY-NC-SA 4.0 DEED.png

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.