用Go寫(xiě)業(yè)務(wù)系統(tǒng)需要制造哪些輪子?

如果之前主要是用Java做業(yè)務(wù)系統(tǒng) 倚舀,那么想用go重寫(xiě)的話還是比較痛苦的忍宋,最主要的原因就是你會(huì)發(fā)現(xiàn)要啥沒(méi)啥,需要自己重寫(xiě)(造輪子)芯侥。下面列舉了一些需要施工的基礎(chǔ)設(shè)施乳讥。

錯(cuò)誤處理

在Java中,只要你沒(méi)有刻意的使用4參數(shù)的Exception構(gòu)造方法去定義自己的異常類唉工,那么默認(rèn)情況下都是會(huì)記錄調(diào)用棧的汹忠,這樣基本上就能馬上定位到事故第一現(xiàn)場(chǎng),排查效率很高谣膳。Go則不然铅乡,如果使用默認(rèn)的error機(jī)制,那么在報(bào)錯(cuò)的時(shí)候你得到的只是一個(gè)簡(jiǎn)單的字符串花履,沒(méi)有任何現(xiàn)場(chǎng)信息挚赊。我在調(diào)試的時(shí)候最大的痛苦也是如此,報(bào)錯(cuò)了掷漱,但一時(shí)很難快速定位到出錯(cuò)的代碼喂窟,如果是比較陳舊的項(xiàng)目举反,那就更不知道這個(gè)錯(cuò)誤是在哪返回的了。不僅如此火鼻,因?yàn)間o里如果遇到panic且沒(méi)有被"捕獲",那么就會(huì)直接導(dǎo)致進(jìn)程退出融撞,整個(gè)服務(wù)直接崩潰粗蔚,這也是不可接受的。
為了解決錯(cuò)誤現(xiàn)場(chǎng)的問(wèn)題致扯,我們可以自己定義一個(gè)結(jié)構(gòu)體当辐,它在實(shí)現(xiàn)error接口的同時(shí),再添加一個(gè)PrevError的字段用于記錄上層錯(cuò)誤耍群,類似于Java Exception的cause()方法:

type Error struct {
    Message string
    PrevError error
}

然后定義一個(gè)Wrap()方法找筝,在遇到錯(cuò)誤時(shí),先將先前的錯(cuò)誤傳進(jìn)去耘婚,然后再填寫(xiě)一條符合本層邏輯的描述信息:

// prevError: 原始錯(cuò)誤
// src: 可以填寫(xiě)源文件名
// desp: 新error對(duì)象的錯(cuò)誤描述
func Wrap(prevError error, src string, desp string) error {
    var msg string
    if "" != src {
        msg = "[" + src + "] " + desp
    } else {
        msg = desp
    }

    err := &Error{
        Message: msg,
        PrevError: prevError,
    }

    return err
}
if nil != err {
    return er.Wrap(err, sourceFile, "failed to convert id")
}

注意第二個(gè)參數(shù)src, 這里可以直接通過(guò)硬編碼的形式將當(dāng)前源文件名傳進(jìn)去沐祷,這樣日志中就會(huì)出現(xiàn)

[xxxx.go] failed to convert id

方便錯(cuò)誤排查攒岛。相比較標(biāo)準(zhǔn)庫(kù)的runtime.Call()方法我更傾向于自己手動(dòng)把文件名傳進(jìn)來(lái),由于行號(hào)會(huì)經(jīng)常變動(dòng)就不傳了兢榨,而文件名很少改動(dòng),因此這是開(kāi)銷最低的記錄現(xiàn)場(chǎng)的方法凌那。
有了自定義的錯(cuò)誤以后吟逝,在最上層(一般是你的HTTP框架的Handler函數(shù))獲取到error后還需要把這個(gè)錯(cuò)誤鏈條打印出來(lái),如:

func Message(e error) string {
    thisErr := e

    strBuilder := bytes.Buffer{}
    nestTier := 0
    for {
        for ix := 0; ix < nestTier; ix++ {
            strBuilder.WriteString("\t")
        }
        strBuilder.WriteString(thisErr.Error())
        strBuilder.WriteString("\n")

        myErrType, ok := thisErr.(*Error)
        if !ok || nil == myErrType.PrevError {
            break
        }

        thisErr = myErrType.PrevError
        nestTier++
    }

    return strBuilder.String()
}

直接使用Message()函數(shù)打印錯(cuò)誤鏈:

// 調(diào)用用戶邏輯
        resp, err := handlerFunc(ctx)
        if nil != err {

            log.Println(er.Message(err))
            return
        }

效果如下:

2019/07/26 17:28:48 failed to query task
    [query_task.go] failed to parse record
        [db.go] failed to parse record
            [query_task.go] failed to convert id
                strconv.Atoi: parsing "": invalid syntax

嗯励稳,是不是有點(diǎn)意思了囱井?對(duì)于業(yè)務(wù)錯(cuò)誤這樣是可以的,因?yàn)轭愃朴趨?shù)格式不對(duì)新翎、參數(shù)不存在這樣的問(wèn)題是會(huì)經(jīng)常發(fā)生的住练,使用這種方式能以最小的開(kāi)銷將問(wèn)題記錄下來(lái)。但對(duì)于panic來(lái)說(shuō)髓绽,我們需要在最上層使用recover()debug.Stack()函數(shù)拿到更加詳細(xì)的錯(cuò)誤信息:

        // 處理panic防止進(jìn)程退出
        defer func() {
            if err := recover(); err != nil {
                log.Println(err)
                log.Println(string(debug.Stack()))
                                // ... ...
            }
        }()

因?yàn)間o里遇到panic如果沒(méi)有recover顺呕,整個(gè)進(jìn)程都會(huì)直接退出 ,這顯然是不可接受的株茶,因此上面的方式是必須的图焰,我們不想因?yàn)橐粋€(gè)空指針就讓整個(gè)服務(wù)直接掛掉。(聽(tīng)起來(lái)有點(diǎn)像C++僵闯?)

HTTP請(qǐng)求路由

因?yàn)槲矣玫腍TTP框架fasthttp是不帶Router的藤滥,因此需要我們選擇一個(gè)第三方的Router實(shí)現(xiàn),比如fasthttprouter向图。這樣一來(lái)我們啟動(dòng)在啟動(dòng)的時(shí)候就要有一個(gè)注冊(cè)路由的過(guò)程,比如

router.GET("/a/b/c", xxxFunc)
router.POST("/efg/b", yyyFunc)

確實(shí)遠(yuǎn)遠(yuǎn)沒(méi)有SpringMVC里直接寫(xiě)Controller來(lái)的方便榄攀。

請(qǐng)求參數(shù)綁定

想直接定義一個(gè)結(jié)構(gòu)體,然后請(qǐng)求來(lái)了參數(shù)就自動(dòng)填寫(xiě)到對(duì)應(yīng)字段上吕嘀?不好意思漠畜,沒(méi)有坞靶。fasthttp中獲取參數(shù)的姿勢(shì)是這樣的:

func GetQueryArg(ctx *fasthttp.RequestCtx, key string) string {
    buf := ctx.QueryArgs().Peek(key)
    if nil == buf {
        return ""
    }

    return string(buf)
}

對(duì),拿到以后還是個(gè)字節(jié)數(shù)據(jù)瘾敢,還需要你手動(dòng)轉(zhuǎn)成string尿这,不僅如此,你還得進(jìn)行非空判斷碟摆,如果想獲取int類型叨橱,還需要調(diào)用轉(zhuǎn)換函數(shù)strconv.Atoi(),然后再判斷一下轉(zhuǎn)換是否成功愉舔,十分繁瑣伙菜。如果想實(shí)現(xiàn)像SpringMVC那樣的參數(shù)綁定,你需要自己寫(xiě)一套通過(guò)反射創(chuàng)建對(duì)象并根據(jù)字段名設(shè)置參數(shù)值的邏輯火的。不過(guò)筆者認(rèn)為這一步并不是必須的,寫(xiě)幾個(gè)工具方法也能解決問(wèn)題馏鹤,比如上面踊淳。

數(shù)據(jù)庫(kù)查詢

好吧陕靠,最痛苦的還是查數(shù)據(jù)庫(kù)剪芥。標(biāo)準(zhǔn)庫(kù)中定義的數(shù)據(jù)庫(kù)查詢接口非常難用琴许,難用到發(fā)指,遠(yuǎn)不如JDBC規(guī)范好使榜田。里面最反人類的就是這個(gè)rows.Scan()方法,因?yàn)樗邮?code>interface{}類型的參數(shù)净捅,所以你還得把你的具體類型"轉(zhuǎn)換"成interface{}才參傳進(jìn)去:

    values := make([]sql.RawBytes, len(columns))
    scanArgs := make([]interface{}, len(columns))
    for i := range columns {
        // 反人類的操作!!!
        scanArgs[i] = &values[i]
    }

    for rows.Next() {
        err = rows.Scan(scanArgs...)

此外蛔六,你肯定不想每次查數(shù)據(jù)都要把這一套Prepare... Query... Scan... Next寫(xiě)一遍吧废亭,所以需要做一下封裝,比如可以將結(jié)果集轉(zhuǎn)成一個(gè)map, 然后調(diào)用用戶自定義的傳進(jìn)來(lái)的函數(shù)來(lái)處理液兽,如:

// 執(zhí)行查詢語(yǔ)句;
// processor: 行處理函數(shù), 每讀取到一行都會(huì)調(diào)用一次processor
func ExecuteQuery(querySql string, processor func(resultMap map[string]string) error, args ...interface{}) error {}
    for rows.Next() {
        err = rows.Scan(scanArgs...)
        if nil != err {
            return err
        }

        // 行數(shù)據(jù)轉(zhuǎn)成map
        resultMap := make(map[string]string)
        for ix, val := range values {
            key := columns[ix]
            resultMap[key] = string(val)
        }

        // 調(diào)用用戶邏輯
        err = processor(resultMap)
        if nil != err {
            return er.Wrap(err, srcFile, "failed to parse record")
        }
    }

即便這樣掌动,用戶的處理函數(shù)processor()也是非常丑陋的:

    err := db.ExecuteQuery(sql, func(result map[string]string) error {
        task := vo.PvTask{}

        taskIdStr, _ := result["id"]
        taskId, err := strconv.Atoi(taskIdStr)
        if nil != err {
            return er.Wrap(err, sourceFile, "failed to convert id")
        }
        task.TaskId = taskId

        taskName, _ := result["task_name"]
        task.TaskName = taskName

        status, _ := result["status"]
        task.Status = status

        createByStr, _ := result["create_by"]
        createBy, err := strconv.Atoi(createByStr)
        if nil != err {
            return er.Wrap(err, sourceFile, "failed to load create_by")
        }
        task.CreatedBy = createBy

        update, _ := result["update_time"]
        task.UpdateTime = update

        tasks = append(tasks, &task)

        return nil
    }, args...)

一個(gè)字段一個(gè)字段的讀坏匪,還得進(jìn)行錯(cuò)誤判斷,要死人的敦迄。
上面這個(gè)問(wèn)題解決方案只有一個(gè)凭迹,那就是使用第三方的ORM框架。然而嗅绸,現(xiàn)在三方ORM眼花繚亂,沒(méi)有一個(gè)公認(rèn)的權(quán)威猛拴,這樣就為項(xiàng)目埋下很多隱患,比如日后你用的框架可能不維護(hù)了愉昆,可能要換框架,可能有奇怪的bug等等焊切。筆者建議還是自己寫(xiě)一套吧芳室,遇到問(wèn)題修改起來(lái)也方便。

數(shù)據(jù)庫(kù)事務(wù)

想在方法上標(biāo)注@Transactional來(lái)開(kāi)啟事務(wù)嚎尤?不好意思,想多了诺苹。你要手動(dòng)使用db.Start(), db.Commit(), db.Rollback()雹拄。

日志框架問(wèn)題

日志框架到底用哪個(gè)一直是非常讓我頭疼的問(wèn)題掌呜。標(biāo)準(zhǔn)庫(kù)的log包缺乏自動(dòng)切割文件的基本功能,github上star最多的logrus居然不能輸出人看著舒服的日志格式势篡,還美其名曰鼓勵(lì)結(jié)構(gòu)化模暗。你結(jié)構(gòu)化方便程序解析也好,關(guān)鍵是你也得提供一個(gè)正常的日志輸出格式吧兑宇?之前用過(guò)log4go,可惜已經(jīng)不維護(hù)了瓷产。
這個(gè)問(wèn)題至今無(wú)解枚驻,實(shí)在不行,自己寫(xiě)吧尔邓。

組件初始化順序問(wèn)題

我們已經(jīng)被Spring給慣壞了,只管把@Component寫(xiě)好梯嗽,然后Spring會(huì)自己幫你初始化,尤其是順序也幫你安排好了慷荔。然而,go不行贷岸。因?yàn)闆](méi)有spring這樣的IoC框架磷雇,所以你必須自己手動(dòng)觸發(fā)每個(gè)模塊的初始化工作,比如先初始化日志螟蒸,加載配置文件崩掘,再初始化數(shù)據(jù)庫(kù)連接、Redis連接苞慢,然后是請(qǐng)求路由的注冊(cè),等等等等绍赛,大概長(zhǎng)這樣:

    // 初始化日志庫(kù)
    initLogger()

    // 加載配置文件
    log.Println("load config")
    config := config.LoadConfig("gopv.yaml")
    log.Println(config)

    // 加載SQL配置
    template.InitSqlMap("sql-template/pv-task.xml")

    // 初始化Router
    log.Println("init router")
    router := initRouter(config)

    // 初始化DB
    log.Println("init db")
    initDb(config)

而且順序要把握好辑畦,比如日志框架要放在所有模塊之前初始化,否則日志框架可能會(huì)有問(wèn)題蚯妇。

分包問(wèn)題

在Java里潦刃,你A文件import B里定義的類,然后 B文件又import A文件定義的類分扎,這是OK的胧洒。但go不行墨状,編譯時(shí)會(huì)直接報(bào)循環(huán)引用錯(cuò)誤菲饼。所以在包的定義上真的就不能隨心所欲了,每次創(chuàng)建新的package镐确,你都要考慮好饼煞,不能出現(xiàn)循環(huán)引用,這有時(shí)候還是很隔應(yīng)人的砖瞧。當(dāng)然你可以說(shuō),如果出現(xiàn)A import B, B import A荣堰,那就是代碼有問(wèn)題竭翠,從哲學(xué)上來(lái)看貌似沒(méi)問(wèn)題。但現(xiàn)實(shí)是在Java中這種情況很普遍屡拨。

依賴問(wèn)題

這個(gè)在go1.11以后可以說(shuō)已經(jīng)不算是大問(wèn)題了褥实,使用官方的module即可损离。但是在此之前,go的依賴管理就是一場(chǎng)災(zāi)難僻澎。

或許有一天能出現(xiàn)一個(gè)權(quán)威的框架來(lái)一站式的解決上面這些問(wèn)題十饥,只有那時(shí)候,Go才能變成實(shí)現(xiàn)業(yè)務(wù)系統(tǒng)的好語(yǔ)言秉氧。在此之前蜒秤,還是老老實(shí)實(shí)的做基礎(chǔ)應(yīng)用吧亚斋。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末帅刊,一起剝皮案震驚了整個(gè)濱河市漂问,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蚤假,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件抡爹,死亡現(xiàn)場(chǎng)離奇詭異冬竟,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)泵殴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門笑诅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)疮鲫,“玉大人,你說(shuō)我怎么就攤上這事俊犯。” “怎么了者祖?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵绢彤,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我械巡,道長(zhǎng),這世上最難降的妖魔是什么芦鳍? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任葛账,我火速辦了婚禮,結(jié)果婚禮上菲宴,老公的妹妹穿的比我還像新娘。我一直安慰自己喝峦,他們只是感情好呜达,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著眉踱,像睡著了一般霜威。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上婿禽,一...
    開(kāi)封第一講書(shū)人閱讀 52,262評(píng)論 1 308
  • 那天大猛,我揣著相機(jī)與錄音,去河邊找鬼吆录。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的哀卫。 我是一名探鬼主播,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼趾撵,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了暂题?” 一聲冷哼從身側(cè)響起究珊,我...
    開(kāi)封第一講書(shū)人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎言津,沒(méi)想到半個(gè)月后取试,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡初婆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年猿棉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宪躯。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡位迂,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出掂林,到底是詐尸還是另有隱情,我是刑警寧澤精置,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布锣杂,位于F島的核電站,受9級(jí)特大地震影響元莫,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜火欧,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望赶盔。 院中可真熱鬧,春花似錦于未、人聲如沸哀军。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至片习,卻和暖如春蹬叭,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背秽五。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留盲再,地道東北人瓣铣。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像梦碗,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子洪规,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容

  • ORA-00001: 違反唯一約束條件 (.) 錯(cuò)誤說(shuō)明:當(dāng)在唯一索引所對(duì)應(yīng)的列上鍵入重復(fù)值時(shí)淹冰,會(huì)觸發(fā)此異常。 O...
    我想起個(gè)好名字閱讀 5,334評(píng)論 0 9
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒(méi)有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,111評(píng)論 1 32
  • 點(diǎn)擊查看原文 Web SDK 開(kāi)發(fā)手冊(cè) SDK 概述 網(wǎng)易云信 SDK 為 Web 應(yīng)用提供一個(gè)完善的 IM 系統(tǒng)...
    layjoy閱讀 13,785評(píng)論 0 15
  • stream TARS 框架的編解碼工具 結(jié)構(gòu)體的使用示例我們演示結(jié)構(gòu)體在三個(gè)典型場(chǎng)景的使用方法:第一種場(chǎng)景:當(dāng)結(jié)...
    宮若石閱讀 1,615評(píng)論 0 1
  • 1. 分布式系統(tǒng)核心問(wèn)題 參考書(shū)籍:《區(qū)塊鏈原理回还、設(shè)計(jì)與應(yīng)用》 一致性問(wèn)題例子:兩個(gè)不同的電影院買同一種電影票,如...
    molscar閱讀 916評(píng)論 0 0