Golang 優(yōu)化之路——自己造一個(gè)日志輪子

寫(xiě)在前面

Golang 的log包內(nèi)容不多,說(shuō)實(shí)話,直接用來(lái)做日志開(kāi)發(fā)有些簡(jiǎn)易。主要是缺少一些功能:

  1. 按日志級(jí)別打印和控制日志;
  2. 日志文件自動(dòng)分割;
  3. 異步打印日志。

按日志級(jí)別打印和控制日志

我們實(shí)現(xiàn)的日志模塊將會(huì)支持4個(gè)級(jí)別:

const (
    LevelError = iota
    LevelWarning
    LevelInformational
    LevelDebug
)

定義一個(gè)日志結(jié)構(gòu)體:

type Logger struct {
    level int
    l     *log.Logger
}

func (ll *Logger) Error(format string, v ...interface{}) {
    if LevelError > ll.level {
        return
    }
    msg := fmt.Sprintf("[E] "+format, v...)
    ll.l.Printf(msg)
}

這樣就能實(shí)現(xiàn)日志級(jí)別控制輸出,并且打印的時(shí)候追加一個(gè)標(biāo)記,比如上面的例子,Error 級(jí)別就會(huì)追加[E]

這個(gè)實(shí)現(xiàn)已經(jīng)可以了,但是還是有優(yōu)化的空間。比如打印追加標(biāo)記[E]的時(shí)候,用的是字符串加法。字符串加法會(huì)申請(qǐng)新的內(nèi)存,對(duì)性能不是很優(yōu)化。我們需要通過(guò)字符數(shù)組來(lái)優(yōu)化。

但我不會(huì)這么去優(yōu)化了。這個(gè)時(shí)候看一下 log 包的 API,可以發(fā)現(xiàn)原生包是支持設(shè)置前綴的:

func (l *Logger) SetPrefix(prefix string)

再去看一下具體的實(shí)現(xiàn):

func (l *Logger) formatHeader(buf *[]byte, t time.Time, file string, line int) {
    *buf = append(*buf, l.prefix...)

原生包在寫(xiě)日志前綴的時(shí)候就用到了[]byte來(lái)提升性能。既然人家已經(jīng)提供了,我們還是不要自己造了。那么問(wèn)題來(lái)了,設(shè)置前綴是初始化的時(shí)候就要設(shè)置,打印的時(shí)候自動(dòng)輸出出來(lái)。一個(gè)log.Logger對(duì)象只能有一個(gè)前綴,而我們需要4種級(jí)別的前綴,這個(gè)如何打印?

type Logger struct {
    level int
    err   *log.Logger
    warn  *log.Logger
    info  *log.Logger
    debug *log.Logger
}

我們直接申請(qǐng)4個(gè)日志對(duì)象就能解決。為了保證所有級(jí)別都打印到同一個(gè)文件里面,初始化的時(shí)候設(shè)置成同一個(gè)io.Writer即可。

logger := new(LcLogger)

logger.err = log.New(w, "[E] ", flag)
logger.warn = log.New(w, "[W] ", flag)
logger.info = log.New(w, "[I] ", flag)
logger.debug = log.New(w, "[D] ", flag)

設(shè)置日志級(jí)別:

func (ll *Logger) SetLevel(l int) {
    ll.level = l
}

打印的時(shí)候根據(jù)日志級(jí)別控制輸出。(講一個(gè)我遇到的坑。之前有一次打印日志打太多了,磁盤(pán)都打滿了,就尋思著把日志級(jí)別調(diào)高減少打印內(nèi)容。把級(jí)別調(diào)成 Error 后發(fā)現(xiàn)還是沒(méi)有效果,最后看了看代碼發(fā)現(xiàn)出問(wèn)題的日志打印的是 Error 級(jí)別。。。Error級(jí)別的日志要盡量少打。)

func (ll *Logger) Error(format string, v ...interface{}) {
    if LevelError > ll.level {
        return
    }
    ll.err.Printf(format, v...)
}

日志文件自動(dòng)分割

日志文件需要自動(dòng)分割。否則一個(gè)文件過(guò)大,清理磁盤(pán)的時(shí)候這個(gè)文件因?yàn)檫€是打印日志沒(méi)辦法清理。

日志分割我覺(jué)得簡(jiǎn)單的以大小分割就好。

那么日志分割功能如何接入咱們上面實(shí)現(xiàn)的日志模塊呢?關(guān)鍵就在io.Writer

type Writer interface {
    Write(p []byte) (n int, err error)
}

Writer這個(gè)接口只有一個(gè)方法,如此簡(jiǎn)單。原生包默認(rèn)打印日志會(huì)輸出到os.Stderr里面,這是一個(gè)os.File類(lèi)型的變量,它實(shí)現(xiàn)了Writer這個(gè)接口。

func (f *File) Write(b []byte) (n int, err error)

寫(xiě)日志的時(shí)候,log 包會(huì)自動(dòng)調(diào)用Write方法。我們可以自己實(shí)現(xiàn)一個(gè)Writer,在Write的時(shí)候計(jì)算一下寫(xiě)入此行日志之后當(dāng)前日志文件大小,如果超過(guò)設(shè)定的值,執(zhí)行一次分割。按日子分割日志也是這個(gè)時(shí)候操作。

推薦用 gopkg.in/natefinch/lumberjack.v2 這個(gè)包來(lái)做日志分割,功能很強(qiáng)大。

jack := &lumberjack.Logger{
    Filename: lfn,
    MaxSize:  maxsize, // megabytes
}

使用也很簡(jiǎn)單,jack對(duì)象就是一個(gè)Writer了,可以直接復(fù)制給Logger使用。

日志的異步輸出

協(xié)程池也整個(gè)包:github.com/ivpusic/grpool。協(xié)程池就不展開(kāi)說(shuō)了,有興趣的可以看看這個(gè)包的實(shí)現(xiàn)。

日志的結(jié)構(gòu)體再一次升級(jí):

type Logger struct {
    level int
    err   *log.Logger
    warn  *log.Logger
    info  *log.Logger
    debug *log.Logger
    p     *grpool.Pool
}

初始化:

logger.p = grpool.NewPool(numWorkers, jobQueueLen)

日志輸出:

func (ll *Logger) Error(format string, v ...interface{}) {
    if LevelError > ll.level {
        return
    }
    ll.p.JobQueue <- func() {
        ll.err.Printf(format, v...)
    }
}

日志行號(hào)

如果你一步一步按上面的做了,打印日志設(shè)置了Lshortfile,展示行號(hào)的花,你可能會(huì)發(fā)現(xiàn)這個(gè)時(shí)候打印出來(lái)的行號(hào)有問(wèn)題。打印日志的時(shí)候用到了runtime里面的堆棧信息,因?yàn)槲覀兎庋b了一層,所以打印的堆棧深度會(huì)發(fā)生變化。簡(jiǎn)單的說(shuō)就是深了一層。

原生的日志包提供了func (l *Logger) Output(calldepth int, s string) error來(lái)控制日志堆棧深度輸出,我們?cè)俅螌?duì)代碼進(jìn)行調(diào)整。

type Logger struct {
    level int
    err   *log.Logger
    warn  *log.Logger
    info  *log.Logger
    debug *log.Logger
    p     *grpool.Pool
    depth int
}

func (ll *Logger) Error(format string, v ...interface{}) {
    if LevelError > ll.level {
        return
    }
    ll.p.JobQueue <- func() {
        ll.err.Output(ll.depth, fmt.Sprintf(format, v...))
    }
}

我們只封裝了一層,所以深度設(shè)置成3就可以了。

線程安全

原生包打印日志是線程安全的:

func (l *Logger) Output(calldepth int, s string) error {
    now := time.Now() // get this early.
    var file string
    var line int
    l.mu.Lock() // 看到這里了么?
    defer l.mu.Unlock()
    if l.flag&(Lshortfile|Llongfile) != 0 {
        // release lock while getting caller info - it's expensive.
        l.mu.Unlock()
        var ok bool
        _, file, line, ok = runtime.Caller(calldepth)
        if !ok {
            file = "???"
            line = 0
        }
        l.mu.Lock()
    }
    l.buf = l.buf[:0]
    l.formatHeader(&l.buf, now, file, line)
    l.buf = append(l.buf, s...)
    if len(s) == 0 || s[len(s)-1] != '\n' {
        l.buf = append(l.buf, '\n')
    }
    _, err := l.out.Write(l.buf)
    return err
}

有它的保證,我們也不需要考慮線程安全的問(wèn)題了。

那么問(wèn)題來(lái)了,fmt包打印日志是線程安全的么?println安全么?fmtprintln打印日志都打印到了哪里?有興趣的可以留言一下一起討論。

最后

日志的打印會(huì)用到諸如fmt.Sprintf的東西,這個(gè)在實(shí)現(xiàn)的時(shí)候?qū)?huì)用到反射。反射會(huì)對(duì)性能有影響,但是不用反射的話代碼過(guò)于惡心。

完整的代碼放到了 GitHub 上面,地址

上面介紹的日志只是在針對(duì)輸出到文件。如果你想輸出有郵件、ElasticSearch等其它地方,不要在初始化的時(shí)候通過(guò)各種復(fù)雜配置參數(shù)來(lái)實(shí)現(xiàn)。

我說(shuō)的是這樣:

NewLogger("es", ...)
NewLogger("smtp", ...)

這樣做的問(wèn)題就是,我只能用你提供好的東西,如果想擴(kuò)展只能修改日志包了。如果這個(gè)包是第三方的包,那讓別人怎么擴(kuò)展呢?而且這種實(shí)現(xiàn)也不是 Golang 的實(shí)現(xiàn)風(fēng)格。

其實(shí)大家看看原生的這些包,很多都是通過(guò)接口串聯(lián)起來(lái)的。原生的 log 包,你可以認(rèn)為他提供的服務(wù)主要是流程方面的服務(wù),拼接好要打印的內(nèi)容,包括行號(hào)、時(shí)間等等,保證線程安全,然后調(diào)用Writer來(lái)打印。如果我們要把日志打印到 ES 里面,就實(shí)現(xiàn)一個(gè)ESWriter。這才是 Golang 風(fēng)格的代碼。


參考文獻(xiàn)

  • 【1】《Go 語(yǔ)言實(shí)戰(zhàn)》
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,431評(píng)論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,637評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 178,555評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,900評(píng)論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,629評(píng)論 6 412
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,976評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,976評(píng)論 3 448
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 43,139評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,686評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,411評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,641評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,129評(píng)論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,820評(píng)論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 35,233評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 36,567評(píng)論 1 295
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,362評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,604評(píng)論 2 380

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

  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,922評(píng)論 6 342
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,828評(píng)論 18 139
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,761評(píng)論 25 708
  • 有些故事不需要轟轟烈烈,反而生活中一言一行拼湊出來(lái)的故事,才更讓人感動(dòng)。 ...
    趙趙趙啊閱讀 380評(píng)論 2 3
  • 智商是成功的關(guān)鍵?錯(cuò)了,毅力才是 如何培養(yǎng)毅力?養(yǎng)成成長(zhǎng)性思維,在讀書(shū)和學(xué)習(xí)中鍛煉大腦,在失敗時(shí)不氣餒繼續(xù)堅(jiān)持,這...
    Nicole_jingqing閱讀 581評(píng)論 0 0