<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
編寫程式碼應該要有極客追求,不要一味的只為了完成功能不加思索而噼裡啪啦一頓操作,我認為應該要像一位設計者一樣去設計程式碼完成功能,因為好的程式碼設計清晰可讀易擴充套件、好修復、更少的學習成本。
因此我們應該學習並制定一些程式碼規範,如下是我學習Go語言中實戰總結的一些經驗,僅代表個人觀點,大家可以互相討論一下
// Bad import "logics/user_logic" import "logics/admin_logic" import "logics/goods_logic" // good import ( "logics/user_logic" "logics/admin_logic" "logics/goods_logic" )
分組導包
內建庫
其他庫
相關聯庫放在一起
// Bad import ( "fmt" "logics/user_logic" "logics/admin_logic" "strings" ) // Good import ( "fmt" "strings" // 邏輯處理 "logics/user_logic" "logics/admin_logic" // 資料庫相關操作 "db/managers" "db/models" )
在定義一些常數、變數與型別宣告的時候,也是一樣可以把相關聯放到一起
常數
// 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 // 預設時間格式化 )
變數
// 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 任務稽核狀態 type TaskAuditState int8 // Bad const TaskWaitHandle TaskAuditState = 0 // 待稽核 const TaskSecondReview TaskAuditState = 1 // 複審 const TaskPass TaskAuditState = 2 // 通過 const TaskRefuse TaskAuditState = 3 // 拒絕 // Good const ( TaskWaitHandle TaskAuditState = iota // 待稽核 TaskSecondReview // 複審 TaskPass // 通過 TaskRefuse // 拒絕 )
在進行Go開發時,指定一些非必選的入參時,不好區別空值是否有意義
如下 AuditState 是非必選引數,而 AuditState 在後端定義是 0 稽核中、1複審、2通過、3拒絕,這都沒什麼問題,但框架解析引數時會把入參模型結構沒有傳值的引數設定成預設值,字串型別是空串、數位型別是0等, 這樣就會有問題如果前端傳遞引數的值是0、空值或者沒有傳遞時,則無法判斷是前端傳遞過來的還是框架預設設定的,導致後續邏輯不好寫。
// QueryAuditTaskIn 查詢任務入參 type QueryAuditTaskIn struct { TeamCode string `query:"team_code" validate:"required"` // 團隊編碼 TaskType enums.RiskType `query:"task_type" validate:"required"` // 任務型別 TagId int `query:"tag_id" validate:"required"` // 標籤id AuditState constants.TaskAuditState `query:"audit_state"` // 稽核狀態 }
解決辦法就是設計時讓前端不要傳遞一些空值,整型列舉常數設定成 從1開始,這樣更好的處理後續邏輯。
// TaskAuditState 定義稽核狀態型別 type TaskAuditState int8 const ( TaskWaitHandle TaskAuditState = iota + 1 // 待稽核 TaskSecondReview // 複審 TaskPass // 通過 TaskRefuse // 拒絕 )
在Go開發中會出現好多if err != nil 的判斷
尤其我在使用 manager 運算元據庫時一呼叫方法就要處理錯誤,還要向上層依次傳遞
manager(資料庫操作層) -> logic(邏輯層) -> api(介面層),每一層都要處理錯誤從而導致
一大堆的 if err != nil
// DelSensitive 刪除內部敏感詞 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 } // 刪除對應的敏感詞字首樹,下一次文字稽核任務進來的時候會重新構造敏感詞字首樹 banWordsModel := banWords.(*models.BanWordsModel) sensitive.DelTrie(banWordsModel.Scene, banWordsModel.TeamCode) return nil }
這樣程式碼太不美觀瞭如果改成如下看看
// DelSensitive 刪除內部敏感詞 func (sl SensitiveLogic) DelSensitive(banWordId uint32) { banWordManager := managers.NewBanWordsManager() banWords := banWordManager.GetById(banWordId) if banWords == nil { return } banWordManager.DeleteById(banWordId) // 刪除對應的敏感詞字首樹,下一次文字稽核任務進來的時候會重新構造敏感詞字首樹 banWordsModel := banWords.(*models.BanWordsModel) sensitive.DelTrie(banWordsModel.Scene, banWordsModel.TeamCode) }
是不是美觀多了,但這樣出現出錯誤不能很好的定位到錯誤的位置以及紀錄檔記錄,還會 panic 拋錯誤出來,導致協程終止執行,要等到 recover 恢復協程來中止 panic 造成的程式崩潰,從而影響效能。處理與不處理各有好處,我個人認為錯誤應該要處理但不要無腦的 if err != nil , 從而
可以在設計與規範上面來解決,對於一些嚴重的一定會導致程式奔潰的錯誤,可以自己統一設計錯誤型別,例如 資料庫error 和 網路error 等,這種是很難避免的,即是避免了,系統也不能正常處理邏輯,因此對於這些 嚴重的錯誤可以手動 panic 然後在全域性錯誤處理中記錄紀錄檔資訊,從而減少程式碼中的 if err != nil 的次數。如下
這樣就不用一層一層傳遞 error ,但缺乏紀錄檔資訊,雖然可以在上面的程式碼中列印紀錄檔資訊,這樣不太好,因此可以到全域性錯誤那統一處理
這裡是之前的想法可以考慮下,但像一些業務異常太多了就會頻繁 panic,導致效能不佳以及後續的一些協程問題,所以我上文提到自己設計錯誤以及規範,什麼錯誤、異常可以 panic 什麼不可以,從而來減少 if err != nil。
其次就是在設計函數的來避免錯誤的出現
我們看一個案例:
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) }
我們可以看出,該函數失敗的原因只有一個,所以返回值的型別應該為 bool,而不是 error,重構一下程式碼:
func (self *AgentContext) IsValidHostType(hostType string) bool { return hostType == "virtual_machine" || hostType == "bare_metal" }
說明:大多數情況,導致失敗的原因不止一種,尤其是對 I/O 操作而言,使用者需要了解更多的錯誤資訊,這時的返回值型別不再是簡單的 bool,而是 error。
error 在 Golang 中是如此的流行,以至於很多人設計函數時不管三七二十一都使用 error,即使沒有一個失敗原因。我們看一下範例程式碼:
func (self *CniParam) setTenantId() error { self.TenantId = self.PodNs return nil }
對於上面的函數設計,就會有下面的呼叫程式碼:
err := self.setTenantId() if err != nil { // log // free resource return errors.New(...) }
根據我們的正確姿勢,重構一下程式碼:
func (self *CniParam) setTenantId() { self.TenantId = self.PodNs }
於是呼叫程式碼變為:
self.setTenantId()
很多人寫程式碼時,到處 return errors.New(value),而錯誤 value 在表達同一個含義時也可能形式不同,比如“記錄不存在”的錯誤 value 可能為:
errors.New("record is not existed.") errors.New("record is not exist!") errors.New("訂單不存在")
這使得相同的錯誤 value 撒在一大片程式碼裡,當上層函數要對特定錯誤 value 進行統一處理時,需要漫遊所有下層程式碼,以保證錯誤 value 統一,不幸的是有時會有漏網之魚,而且這種方式嚴重阻礙了錯誤 value 的重構。
在每個業務系統中維護一個錯誤物件定義檔案,一些公用的錯誤則封裝到Go的公用庫中
業務系統錯誤封裝:
package exceptions // err_struct.go // OrderBizError 訂單系統業務錯誤結構體 type OrderBizError struct { message string // 錯誤資訊 code ErrorCode // 響應碼 sysName string // 系統名稱 } 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", // 可抽到微服務公用庫中 } } // Code 狀態碼 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) )
返回錯誤資訊給前端則返回狀態碼和資訊,紀錄檔則記錄全部的錯誤資訊
Go公用庫錯誤封裝:
// err_struct.go // BizError 業務錯誤結構體 type BizError struct { message string // 錯誤資訊 code ErrorCode // 響應碼 } // Code 狀態碼 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" // 認證錯誤 // ... ) var ( Success = NewOrderBizError("Success", SuccessCode) FailErr = NewOrderBizError("Fail", FailCode) AuthorizationErr = NewOrderBizError("Authorization Error", AuthorizationCode) // ... )
其實每個業務系統的結構體可以繼承公用的
// BizError 業務錯誤結構體 type BizError struct { message string // 錯誤資訊 code ErrorCode // 響應碼 } // OrderBizError 訂單系統業務錯誤結構體 type OrderBizError struct { BizError sysName string // 系統名稱 }
然後使用的時候就可以不要每次都自己單獨的定義錯誤碼和資訊
生成Swaager介面檔案註釋儘量對齊
// QueryAuditTask 查詢已領取的稽核任務 // @Summary 查詢已領取的稽核任務 // @Tags 稽核管理介面 // @Accept json // @Produce json // @Param team_code query string true "團隊編碼" // @Param task_type query string true "風控型別" // @Param tag_id query string true "稽核任務型別標籤ID" // @Param audit_state query int false "任務稽核狀態 1待稽核 2複審 3通過 4拒絕" // @Success 200 {object} rsp.ResponseData // @Router /task.audit.list_get [get] func QueryAuditTask(ctx *fiber.Ctx) error {
路由註釋少不
入參出參結構體註釋少不了
// QueryAuditTaskIn 領取任務入參 type QueryAuditTaskIn struct { TeamCode string `query:"team_code" validate:"required"` // 團隊編碼 TaskType enums.RiskType `query:"task_type" validate:"required"` // 任務型別 TagId int `query:"tag_id" validate:"required"` // 標籤id AuditState constants.TaskResultType `query:"audit_state"` // 稽核狀態 } // TaskListItem 任務列表項 type TaskListItem struct { Id uint32 `json:"id"` // 主鍵id TeamCode string `json:"team_code"` // 團隊編碼 ObjectType enums.ObjectType `json:"object_type"` // 物件型別 ObjectId string `json:"object_id"` // 物件ID TaskType enums.RiskType `json:"task_type"` // 任務型別 Content datatypes.JSON `json:"content"` // 任務內容 TagId uint32 `json:"tag_id"` // 標籤ID TaskResult constants.TaskResultType `json:"task_result"` // 任務稽核結果 ReviewerId uint32 `json:"reviewer_id"` // 領取任務人ID AuditReason string `json:"audit_reason"` // 稽核理由 SourceList []interface{} `json:"source_list"` // 溯源列表 CreateTs int64 `json:"create_ts"` // 任務建立的時間戳 }
一些複雜的巢狀結構最好寫上樣例
// 獲取全部的團佇列表 teamSlice := rmc.getAllTeam() // 統計各稽核任務未領取數量 tagTaskCountMap := rmc.getTagTaskCount() // 獲取所有task_group標籤與其二級標籤 tagMenuSlice := rmc.getTagMenu() // 將風控稽核選單資訊組裝到各個團隊中並填充統計數量 // 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 { // 填充各稽核型別未領取任務數量 newTagMenuSlice := rmc.FillTagTaskCount(tagTaskCountMap, team, tagMenuSlice) // 填充各團隊未領取任務總數 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, }
一些長的SQL語句不要寫到一行裡面去,可以使用 `` 原生字串 達到在字串中換行的效果從而美化SQL語句,然後就是儘量需要什麼業務資料就查什麼,避免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}
階梯縮排、程式碼緊湊會導致程式碼不易閱讀,理解更難,可以通過一些反向判斷來拒絕一些操作,從而減少階梯縮排,程式碼緊湊則可以把一些相關的邏輯放到一起,不同的處理步驟適當換行。
// Bad // 校驗引數 VerifyParams(requestIn) // 獲取資訊 orderSlice := GetDBInfo(params) // 邏輯處理 // ... // 組織返參 for _, order := range(orderSlice){ ... } // Good // 校驗引數 VerifyParams(requestIn) // 獲取資訊 orderSlice := GetDBInfo(params) // 邏輯處理 // ... // 組織返參 for _, order := range(orderSlice){ ... }
同一步驟的邏輯太長可以封裝成函數、方法。
// 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 }
避免迴圈IO,可以用批次就改用批次。
func (itm InspectionTaskManager) BatchCreateInspectionTask(taskIdList []uint32) error { inspectionTaskList := make([]models.InspectionTaskModel, 0) // 組裝好批次建立的抽查任務 for _, id := range taskIdList { inspectionTaskList = append(inspectionTaskList, models.InspectionTaskModel{ TaskId: id, }) } // 批次建立 _, err := itm.BulkCreate(inspectionTaskList) return err }
有些資料庫表結構可以使用自關聯的方式簡化查詢從而避免迴圈IO、減少查詢次數。
// GetTagMenu 獲取所有task_group標籤與其二級標籤 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 }
然後就是上下文無關聯的可以並行執行,提高效能。
// GetPageWithTotal 獲取分頁並返回總數 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 // 設定預設值為-1, 用於判斷沒有獲取到資料的時候 go func() { // 獲取總數 total, err := bm.GetCount(condition) if err != nil { errChan <- err return } pageResult.Total = total resultChan <- pageResult }() go func() { // 獲取分頁資料 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("超時,分頁查詢失敗")) } } }
以上是借鑑網上一些處理方法和自己的一些想法與實踐經驗,可以互相探討與學習,更多關於Go 程式碼規範錯誤處理的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45