Go 代碼規(guī)范錯誤處理示例經(jīng)驗總結(jié)
引言
編寫代碼應(yīng)該要有極客追求,不要一味的只為了完成功能不加思索而噼里啪啦一頓操作,我認(rèn)為應(yīng)該要像一位設(shè)計者一樣去設(shè)計代碼完成功能,因為好的代碼設(shè)計清晰可讀易擴(kuò)展、好修復(fù)、更少的學(xué)習(xí)成本。
因此我們應(yīng)該學(xué)習(xí)并制定一些代碼規(guī)范,如下是我學(xué)習(xí)Go語言中實戰(zhàn)總結(jié)的一些經(jīng)驗,僅代表個人觀點,大家可以互相討論一下
一、相關(guān)聯(lián)的聲明放到一起
1、導(dǎo)包規(guī)范
// Bad import "logics/user_logic" import "logics/admin_logic" import "logics/goods_logic" // good import ( "logics/user_logic" "logics/admin_logic" "logics/goods_logic" )
分組導(dǎo)包
內(nèi)置庫
其他庫
相關(guān)聯(lián)庫放在一起
// Bad import ( "fmt" "logics/user_logic" "logics/admin_logic" "strings" ) // Good import ( "fmt" "strings" // 邏輯處理 "logics/user_logic" "logics/admin_logic" // 數(shù)據(jù)庫相關(guān)操作 "db/managers" "db/models" )
2、常量、變量、類型聲明
在定義一些常量、變量與類型聲明的時候,也是一樣可以把相關(guān)聯(lián)放到一起
常量
// Bad const YearMonthDay = "2006-01-02" const YearMonthDayHourMinSec = "2006-01-02 15:04:05" const DefaultTimeFmt = YearMonthDayHourMinSec // Good // TimeFormat 時間格式化 type TimeFormat string const ( YearMonthDay TimeFormat = "2006-01-02" // 年月日 yyyy-mm-dd YearMonthDayHourMinSec TimeFormat = "2006-01-02 15:04:05" // 年月年時分秒 yyyy-mm-dd HH:MM:SS DefaultTimeFmt TimeFormat = YearMonthDayHourMinSec // 默認(rèn)時間格式化 )
變量
// Bad
var querySQL string
var queryParams []interface{}
// Good
var (
querySQL string
queryParams []interface{}
)
類型聲明
// Bad type Area float64 type Volume float64 type Perimeter float64 // Good type ( Area float64 // 面積 Volume float64 // 體積 Perimeter float64 // 周長 )
枚舉常量
// TaskAuditState 任務(wù)審核狀態(tài) type TaskAuditState int8 // Bad const TaskWaitHandle TaskAuditState = 0 // 待審核 const TaskSecondReview TaskAuditState = 1 // 復(fù)審 const TaskPass TaskAuditState = 2 // 通過 const TaskRefuse TaskAuditState = 3 // 拒絕 // Good const ( TaskWaitHandle TaskAuditState = iota // 待審核 TaskSecondReview // 復(fù)審 TaskPass // 通過 TaskRefuse // 拒絕 )
在進(jìn)行Go開發(fā)時,指定一些非必選的入?yún)r,不好區(qū)別空值是否有意義
如下 AuditState 是非必選參數(shù),而 AuditState 在后端定義是 0 審核中、1復(fù)審、2通過、3拒絕,這都沒什么問題,但框架解析參數(shù)時會把入?yún)⒛P徒Y(jié)構(gòu)沒有傳值的參數(shù)設(shè)置成默認(rèn)值,字符串類型是空串、數(shù)字類型是0等, 這樣就會有問題如果前端傳遞參數(shù)的值是0、空值或者沒有傳遞時,則無法判斷是前端傳遞過來的還是框架默認(rèn)設(shè)置的,導(dǎo)致后續(xù)邏輯不好寫。
// QueryAuditTaskIn 查詢?nèi)蝿?wù)入?yún)?
type QueryAuditTaskIn struct {
TeamCode string `query:"team_code" validate:"required"` // 團(tuán)隊編碼
TaskType enums.RiskType `query:"task_type" validate:"required"` // 任務(wù)類型
TagId int `query:"tag_id" validate:"required"` // 標(biāo)簽id
AuditState constants.TaskAuditState `query:"audit_state"` // 審核狀態(tài)
}
解決辦法就是設(shè)計時讓前端不要傳遞一些空值,整型枚舉常量設(shè)置成 從1開始,這樣更好的處理后續(xù)邏輯。
// TaskAuditState 定義審核狀態(tài)類型 type TaskAuditState int8 const ( TaskWaitHandle TaskAuditState = iota + 1 // 待審核 TaskSecondReview // 復(fù)審 TaskPass // 通過 TaskRefuse // 拒絕 )

二、Go錯誤處理
在Go開發(fā)中會出現(xiàn)好多if err != nil 的判斷
尤其我在使用 manager 操作數(shù)據(jù)庫時一調(diào)用方法就要處理錯誤,還要向上層依次傳遞
manager(數(shù)據(jù)庫操作層) -> logic(邏輯層) -> api(接口層),每一層都要處理錯誤從而導(dǎo)致
一大堆的 if err != nil
// DelSensitive 刪除內(nèi)部敏感詞
func (sl SensitiveLogic) DelSensitive(banWordId uint32) error {
banWordManager, err := managers.NewBanWordsManager()
if err != nil {
return err
}
banWords, err := banWordManager.GetById(banWordId)
if err != nil{
return err
}
if banWords == nil {
return exceptions.NewBizError("屏蔽詞不存在")
}
_, err = banWordManager.DeleteById(banWordId)
if err != nil {
return err
}
// 刪除對應(yīng)的敏感詞前綴樹,下一次文本審核任務(wù)進(jìn)來的時候會重新構(gòu)造敏感詞前綴樹
banWordsModel := banWords.(*models.BanWordsModel)
sensitive.DelTrie(banWordsModel.Scene, banWordsModel.TeamCode)
return nil
}
這樣代碼太不美觀了如果改成如下看看
// DelSensitive 刪除內(nèi)部敏感詞
func (sl SensitiveLogic) DelSensitive(banWordId uint32) {
banWordManager := managers.NewBanWordsManager()
banWords := banWordManager.GetById(banWordId)
if banWords == nil {
return
}
banWordManager.DeleteById(banWordId)
// 刪除對應(yīng)的敏感詞前綴樹,下一次文本審核任務(wù)進(jìn)來的時候會重新構(gòu)造敏感詞前綴樹
banWordsModel := banWords.(*models.BanWordsModel)
sensitive.DelTrie(banWordsModel.Scene, banWordsModel.TeamCode)
}
是不是美觀多了,但這樣出現(xiàn)出錯誤不能很好的定位到錯誤的位置以及日志記錄,還會 panic 拋錯誤出來,導(dǎo)致協(xié)程終止執(zhí)行,要等到 recover 恢復(fù)協(xié)程來中止 panic 造成的程序崩潰,從而影響性能。處理與不處理各有好處,我個人認(rèn)為錯誤應(yīng)該要處理但不要無腦的 if err != nil , 從而
可以在設(shè)計與規(guī)范上面來解決,對于一些嚴(yán)重的一定會導(dǎo)致程序奔潰的錯誤,可以自己統(tǒng)一設(shè)計錯誤類型,例如 數(shù)據(jù)庫error 和 網(wǎng)絡(luò)error 等,這種是很難避免的,即是避免了,系統(tǒng)也不能正常處理邏輯,因此對于這些 嚴(yán)重的錯誤可以手動 panic 然后在全局錯誤處理中記錄日志信息,從而減少代碼中的 if err != nil 的次數(shù)。如下

這樣就不用一層一層傳遞 error ,但缺乏日志信息,雖然可以在上面的代碼中打印日志信息,這樣不太好,因此可以到全局錯誤那統(tǒng)一處理

這里是之前的想法可以考慮下,但像一些業(yè)務(wù)異常太多了就會頻繁 panic,導(dǎo)致性能不佳以及后續(xù)的一些協(xié)程問題,所以我上文提到自己設(shè)計錯誤以及規(guī)范,什么錯誤、異??梢?panic 什么不可以,從而來減少 if err != nil。
其次就是在設(shè)計函數(shù)的來避免錯誤的出現(xiàn)
1、失敗的原因只有一個時,不使用 error
我們看一個案例:
func (self *AgentContext) CheckHostType(host_type string) error {
switch host_type {
case "virtual_machine":
return nil
case "bare_metal":
return nil
}
return errors.New("CheckHostType ERROR:" + host_type)
}
我們可以看出,該函數(shù)失敗的原因只有一個,所以返回值的類型應(yīng)該為 bool,而不是 error,重構(gòu)一下代碼:
func (self *AgentContext) IsValidHostType(hostType string) bool {
return hostType == "virtual_machine" || hostType == "bare_metal"
}
說明:大多數(shù)情況,導(dǎo)致失敗的原因不止一種,尤其是對 I/O 操作而言,用戶需要了解更多的錯誤信息,這時的返回值類型不再是簡單的 bool,而是 error。
2、沒有失敗時,不使用 error
error 在 Golang 中是如此的流行,以至于很多人設(shè)計函數(shù)時不管三七二十一都使用 error,即使沒有一個失敗原因。我們看一下示例代碼:
func (self *CniParam) setTenantId() error {
self.TenantId = self.PodNs
return nil
}
對于上面的函數(shù)設(shè)計,就會有下面的調(diào)用代碼:
err := self.setTenantId()
if err != nil {
// log
// free resource return errors.New(...)
}
根據(jù)我們的正確姿勢,重構(gòu)一下代碼:
func (self *CniParam) setTenantId() {
self.TenantId = self.PodNs
}
于是調(diào)用代碼變?yōu)椋?/p>
self.setTenantId()
3、錯誤值統(tǒng)一定義
很多人寫代碼時,到處 return errors.New(value),而錯誤 value 在表達(dá)同一個含義時也可能形式不同,比如“記錄不存在”的錯誤 value 可能為:
errors.New("record is not existed.")
errors.New("record is not exist!")
errors.New("訂單不存在")
這使得相同的錯誤 value 撒在一大片代碼里,當(dāng)上層函數(shù)要對特定錯誤 value 進(jìn)行統(tǒng)一處理時,需要漫游所有下層代碼,以保證錯誤 value 統(tǒng)一,不幸的是有時會有漏網(wǎng)之魚,而且這種方式嚴(yán)重阻礙了錯誤 value 的重構(gòu)。
在每個業(yè)務(wù)系統(tǒng)中維護(hù)一個錯誤對象定義文件,一些公用的錯誤則封裝到Go的公用庫中
業(yè)務(wù)系統(tǒng)錯誤封裝:
package exceptions
// err_struct.go
// OrderBizError 訂單系統(tǒng)業(yè)務(wù)錯誤結(jié)構(gòu)體
type OrderBizError struct {
message string // 錯誤信息
code ErrorCode // 響應(yīng)碼
sysName string // 系統(tǒng)名稱
}
func NewOrderBizError(message string, errorCode ...ErrorCode) *OrderBizError {
code := FailCode
if len(errorCode) > 0 {
code = errorCode[0]
}
return &OrderBizError{
code: code,
message: message,
sysName: "HuiYiMall—OrderSystem", // 可抽到微服務(wù)公用庫中
}
}
// Code 狀態(tài)碼
func (b OrderBizError) Code() ErrorCode {
return b.code
}
// Message 錯誤信息
func (b OrderBizError) Message() string {
return b.message
}
// err_const.go
// ErrorCode 定義錯誤code類型
type ErrorCode string
const (
OrderTimeoutErrCode ErrorCode = "4000" // 訂單超時
OrderPayFailErrCode ErrorCode = "4001" // 訂單支付失敗
)
var (
OrderTimeoutErr = NewOrderBizError("order timeout", OrderTimeoutErrCode)
OrderPayFailErr = NewOrderBizError("order pay fail", OrderPayFailErrCode)
)
返回錯誤信息給前端則返回狀態(tài)碼和信息,日志則記錄全部的錯誤信息
Go公用庫錯誤封裝:
// err_struct.go
// BizError 業(yè)務(wù)錯誤結(jié)構(gòu)體
type BizError struct {
message string // 錯誤信息
code ErrorCode // 響應(yīng)碼
}
// Code 狀態(tài)碼
func (b BizError) Code() ErrorCode {
return b.code
}
// Message 錯誤信息
func (b BizError) Message() string {
return b.message
}
func NewBizError(message string, errorCode ...ErrorCode) *BizError {
code := FailCode
if len(errorCode) > 0 {
code = errorCode[0]
}
return &BizError{
code: code,
message: message,
}
}
// err_const.go
const (
SuccessCode ErrorCode = "0000" // 成功
FailCode ErrorCode = "0403" // 失敗
AuthorizationCode ErrorCode = "0403" // 認(rèn)證錯誤
// ...
)
var (
Success = NewOrderBizError("Success", SuccessCode)
FailErr = NewOrderBizError("Fail", FailCode)
AuthorizationErr = NewOrderBizError("Authorization Error", AuthorizationCode)
// ...
)
其實每個業(yè)務(wù)系統(tǒng)的結(jié)構(gòu)體可以繼承公用的
// BizError 業(yè)務(wù)錯誤結(jié)構(gòu)體
type BizError struct {
message string // 錯誤信息
code ErrorCode // 響應(yīng)碼
}
// OrderBizError 訂單系統(tǒng)業(yè)務(wù)錯誤結(jié)構(gòu)體
type OrderBizError struct {
BizError
sysName string // 系統(tǒng)名稱
}
然后使用的時候就可以不要每次都自己單獨的定義錯誤碼和信息
三、代碼規(guī)范與實踐
1、良好的命名與注釋
生成Swaager接口文檔注釋盡量對齊
// QueryAuditTask 查詢已領(lǐng)取的審核任務(wù)
// @Summary 查詢已領(lǐng)取的審核任務(wù)
// @Tags 審核管理接口
// @Accept json
// @Produce json
// @Param team_code query string true "團(tuán)隊編碼"
// @Param task_type query string true "風(fēng)控類型"
// @Param tag_id query string true "審核任務(wù)類型標(biāo)簽ID"
// @Param audit_state query int false "任務(wù)審核狀態(tài) 1待審核 2復(fù)審 3通過 4拒絕"
// @Success 200 {object} rsp.ResponseData
// @Router /task.audit.list_get [get]
func QueryAuditTask(ctx *fiber.Ctx) error {
路由注釋少不

入?yún)⒊鰠⒔Y(jié)構(gòu)體注釋少不了
// QueryAuditTaskIn 領(lǐng)取任務(wù)入?yún)?
type QueryAuditTaskIn struct {
TeamCode string `query:"team_code" validate:"required"` // 團(tuán)隊編碼
TaskType enums.RiskType `query:"task_type" validate:"required"` // 任務(wù)類型
TagId int `query:"tag_id" validate:"required"` // 標(biāo)簽id
AuditState constants.TaskResultType `query:"audit_state"` // 審核狀態(tài)
}
// TaskListItem 任務(wù)列表項
type TaskListItem struct {
Id uint32 `json:"id"` // 主鍵id
TeamCode string `json:"team_code"` // 團(tuán)隊編碼
ObjectType enums.ObjectType `json:"object_type"` // 對象類型
ObjectId string `json:"object_id"` // 對象ID
TaskType enums.RiskType `json:"task_type"` // 任務(wù)類型
Content datatypes.JSON `json:"content"` // 任務(wù)內(nèi)容
TagId uint32 `json:"tag_id"` // 標(biāo)簽ID
TaskResult constants.TaskResultType `json:"task_result"` // 任務(wù)審核結(jié)果
ReviewerId uint32 `json:"reviewer_id"` // 領(lǐng)取任務(wù)人ID
AuditReason string `json:"audit_reason"` // 審核理由
SourceList []interface{} `json:"source_list"` // 溯源列表
CreateTs int64 `json:"create_ts"` // 任務(wù)創(chuàng)建的時間戳
}
一些復(fù)雜的嵌套結(jié)構(gòu)最好寫上樣例
// 獲取全部的團(tuán)隊列表
teamSlice := rmc.getAllTeam()
// 統(tǒng)計各審核任務(wù)未領(lǐng)取數(shù)量
tagTaskCountMap := rmc.getTagTaskCount()
// 獲取所有task_group標(biāo)簽與其二級標(biāo)簽
tagMenuSlice := rmc.getTagMenu()
// 將風(fēng)控審核菜單信息組裝到各個團(tuán)隊中并填充統(tǒng)計數(shù)量
// eg: [
// {
// "team_code": "lihua",
// "team_name": "梨花"
// "task_count": 1
// "tag_menus": [{"tag_id": 1, "tag_name": "文本", "tag_type": "task_group", "task_count":1, "child_tags": []}, ...]
// },
// ...
//]
riskMenuSlice := make([]*TeamMenuItem, 0)
for _, team := range *teamSlice {
// 填充各審核類型未領(lǐng)取任務(wù)數(shù)量
newTagMenuSlice := rmc.FillTagTaskCount(tagTaskCountMap, team, tagMenuSlice)
// 填充各團(tuán)隊未領(lǐng)取任務(wù)總數(shù)
teamTaskCount := uint32(0)
if tagCountMap, ok := tagTaskCountMap[team.TeamCode]; ok {
for _, tagTaskCount := range tagCountMap {
teamTaskCount += tagTaskCount
}
}
teamMenuItem := TeamMenuItem{
TeamCode: team.TeamCode,
TeamName: team.TeamName,
TaskCount: teamTaskCount,
TagMenus: newTagMenuSlice,
}
riskMenuSlice = append(riskMenuSlice, &teamMenuItem)
}
riskMenuMap := map[string]interface{}{
"work_menu": riskMenuSlice,
}
2、美化SQL語句,避免 Select
一些長的SQL語句不要寫到一行里面去,可以使用 `` 原生字符串 達(dá)到在字符串中換行的效果從而美化SQL語句,然后就是盡量需要什么業(yè)務(wù)數(shù)據(jù)就查什么,避免Select * 后再邏輯處理去篩選
queryField := `
task.id AS task_id,
tag_name,
staff.real_name AS staff_real_name,
staff_tar.audit_reason AS staff_audit_reason,
staff_tar.review_result AS staff_review_result,
staff_tar.review_ts AS staff_review_ts,
chief.real_name AS chief_real_name,
chief_tar.audit_reason AS chief_audit_reason,
chief_tar.review_result AS chief_review_result,
chief_tar.review_ts AS chief_review_ts,
task.content AS task_content,
task.json_extend AS task_json_ext,
tar.json_extend AS audit_record_json_ext`
querySQL := `
SELECT
%s
FROM
task_audit_log AS tar
JOIN task ON tar.task_id = task.id
JOIN tag ON task.tag_id = tag.id
LEFT JOIN task_audit_log AS staff_tar ON task.id = staff_tar.task_id AND staff_tar.reviewer_role = "staff"
LEFT JOIN reviewer AS staff ON staff.account_id = staff_tar.reviewer_id
LEFT JOIN task_audit_log AS chief_tar ON task.id = chief_tar.task_id AND chief_tar.reviewer_role = "chief"
LEFT JOIN reviewer AS chief ON chief.account_id = chief_tar.reviewer_id
WHERE
team_code = ?`
queryParams := []interface{}{taskAuditLogIn.TeamCode}
3、避免階梯縮進(jìn)與代碼緊湊
階梯縮進(jìn)、代碼緊湊會導(dǎo)致代碼不易閱讀,理解更難,可以通過一些反向判斷來拒絕一些操作,從而減少階梯縮進(jìn),代碼緊湊則可以把一些相關(guān)的邏輯放到一起,不同的處理步驟適當(dāng)換行。
// Bad
// 校驗參數(shù)
VerifyParams(requestIn)
// 獲取信息
orderSlice := GetDBInfo(params)
// 邏輯處理
// ...
// 組織返參
for _, order := range(orderSlice){
...
}
// Good
// 校驗參數(shù)
VerifyParams(requestIn)
// 獲取信息
orderSlice := GetDBInfo(params)
// 邏輯處理
// ...
// 組織返參
for _, order := range(orderSlice){
...
}
同一步驟的邏輯太長可以封裝成函數(shù)、方法。
// Bad
for _, v := range data {
if v.F1 == 1 {
v = process(v)
if err := v.Call(); err == nil {
v.Send()
} else {
return err
}
} else {
log.Printf("Invalid v: %v", v)
}
}
// Good
for _, v := range data {
if v.F1 != 1 {
log.Printf("Invalid v: %v", v)
continue
}
v = process(v)
if err := v.Call(); err != nil {
return err
}
v.Send()
}
不必要的else
// Bad
var a int
if b {
a = 100
} else {
a = 10
}
// Good
a := 10
if b {
a = 100
}
4、避免循環(huán)IO、上下文無關(guān)聯(lián)的耗時動作采用Go協(xié)程
避免循環(huán)IO,可以用批量就改用批量。
func (itm InspectionTaskManager) BatchCreateInspectionTask(taskIdList []uint32) error {
inspectionTaskList := make([]models.InspectionTaskModel, 0)
// 組裝好批量創(chuàng)建的抽查任務(wù)
for _, id := range taskIdList {
inspectionTaskList = append(inspectionTaskList, models.InspectionTaskModel{
TaskId: id,
})
}
// 批量創(chuàng)建
_, err := itm.BulkCreate(inspectionTaskList)
return err
}
有些數(shù)據(jù)庫表結(jié)構(gòu)可以使用自關(guān)聯(lián)的方式簡化查詢從而避免循環(huán)IO、減少查詢次數(shù)。
// GetTagMenu 獲取所有task_group標(biāo)簽與其二級標(biāo)簽
func (tm *TagManager) GetTagMenu() []*TagMenuResult {
querySql := `
SELECT
t1.id, t1.tag_name, t1.tag_type,
t2.id as two_tag_id, t2.tag_name as two_tag_name,
t2.tag_type as two_tag_type,
t2.pid
FROM
tag AS t1
INNER JOIN tag AS t2 ON t2.pid = t1.id
WHERE
t1.tag_type = "task_group"`
tagMenuSlice := make([]*TagMenuResult, 0)
tm.Conn.Raw(querySql).Scan(&tagMenuSlice)
return tagMenuSlice
}
然后就是上下文無關(guān)聯(lián)的可以并行執(zhí)行,提高性能。
// GetPageWithTotal 獲取分頁并返回總數(shù)
func (bm BaseManager) GetPageWithTotal(condition *Condition) (*PageResult, error) {
errChan := make(chan error)
resultChan := make(chan PageResult)
defer close(errChan)
defer close(resultChan)
var pageResult PageResult
pageResult.Total = -1 // 設(shè)置默認(rèn)值為-1, 用于判斷沒有獲取到數(shù)據(jù)的時候
go func() {
// 獲取總數(shù)
total, err := bm.GetCount(condition)
if err != nil {
errChan <- err
return
}
pageResult.Total = total
resultChan <- pageResult
}()
go func() {
// 獲取分頁數(shù)據(jù)
result, err := bm.GetPage(condition)
if err != nil {
errChan <- err
return
}
pageResult.ResultList = result
resultChan <- pageResult
}()
for {
select {
case err := <-errChan:
return nil, err
case result := <-resultChan:
if result.Total != -1 && result.ResultList != nil {
return &result, nil
}
case <-time.After(time.Second * 5):
return nil, exceptions.NewInterError(fmt.Sprintf("超時,分頁查詢失敗"))
}
}
}
以上是借鑒網(wǎng)上一些處理方法和自己的一些想法與實踐經(jīng)驗,可以互相探討與學(xué)習(xí),更多關(guān)于Go 代碼規(guī)范錯誤處理的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
go語言區(qū)塊鏈學(xué)習(xí)調(diào)用智能合約
這篇文章主要為大家介紹了go語言區(qū)塊鏈學(xué)習(xí)中如何調(diào)用智能合約的實現(xiàn)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2021-10-10
golang 輸出重定向:fmt Log,子進(jìn)程Log,第三方庫logrus的詳解
這篇文章主要介紹了golang 輸出重定向:fmt Log,子進(jìn)程Log,第三方庫logrus的詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12
初學(xué)Go必備的vscode插件及最常用快捷鍵和代碼自動補(bǔ)全
這篇文章主要給大家介紹了關(guān)于初學(xué)vscode寫Go必備的vscode插件及最常用快捷鍵和代碼自動補(bǔ)全的相關(guān)資料,由于vscode是開源免費(fèi)的,而且開發(fā)支持vscode的插件相對比較容易,更新速度也很快,需要的朋友可以參考下2023-07-07

