公司內部的 Go 代碼規範中限制了每一行代碼的寬度。為了滿足這個規範,那些太寬的代碼行就不可避免地需要換行。換行不是普通的回車就行,如何在換行的同時,保持代碼優秀的可讀性,筆者根據日常 code review 中看到的各種模式,提出一些建議。
- 上一篇文章:每天學點 Go 規範 - 函數傳參時,struct 應該傳值還是引用
規範和原因
公司的 Go 規範統一要求每一行 Go 代碼不能超過 120 個可顯示字符的寬度。為什麼要限制呢?在 這篇文章 中的描述我是非常贊同的,這裏筆者就不再贅述了,讀者可以直接參閲。
至於 120 這個數字是怎麼來的?我就非常費解了。或許是覺得 80 是在太短,而 160 又太長,所以就取了一個折中值吧。
好,那麼既然換行是不可避免的,那麼接下來就是要如何換行了。下面筆者針對一些有爭議的代碼超寬換行的情況,具體説明如何優雅地換行。
函數簽名和調用
實際上,除了一些例外情況,那麼需要換行的地方,比較有爭議的主要都是集中在函數簽名 / 函數調用上。
問題提出
下面我舉一個例子,比如説我們要定義一個函數,包含以下信息:
- 函數功能: 向一個聊天羣裏發一個機器人消息, @ 其中的幾個人或者是 @all
- 函數入參: context, 羣 ID, 機器人 ID, @ 的用户 ID 列表 (空表示 @all), 消息正文
- 函數出參: 發出去的消息 ID, 錯誤信息
根據上述信息,我們設計一個接口,信息如下:
-
函數名:
SendRobotMessageToChatGroup
-
入參:
ctx context.Context-
req *SendRobotMessageToChatGroupRequestGroupID stringRobotID stringAtAll boolAtUserIDs []stringText string
-
出參:
rsp *SendRobotMessageToChatGroupResponseerr 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 行
- 自動生成的代碼
參考資料
- 函數簽名的概念
- 每行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。