欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一文教你打造一個(gè)簡易的Golang日志庫

 更新時(shí)間:2023年06月13日 14:42:18   作者:游魚的編程旅行  
這篇文章主要為大家詳細(xì)介紹了如何使用不超過130行的代碼,通過一系列g(shù)olang的特性,來打造一個(gè)簡易的golang日志庫,感興趣的小伙伴可以了解一下

前言

最近去一家大公司面試,我遇到一道有趣的golang筆試題:

當(dāng)時(shí),由于筆試時(shí)間緊張,我來不及多想,這道題就空著?;貋砗?,思來想去,自己決定實(shí)現(xiàn)一個(gè)簡易的golang日志庫,完成這個(gè)缺憾的夢,也和大家一起對(duì)“golang的并發(fā)之道”進(jìn)行研究。

你可以收獲

  • channel在高并發(fā)場景下的使用;
  • for-select-case對(duì)channel的優(yōu)雅遍歷處理;
  • 定時(shí)器在select中的使用;
  • golang如何用結(jié)束信號(hào)讓程序優(yōu)雅退出。

打造一個(gè)簡易的golang日志庫

接下來,我們用不超過130行的代碼,通過一系列g(shù)olang的特性,來打造一個(gè)簡易的golang日志庫。

內(nèi)容脈絡(luò)

為了幫助大家有個(gè)大致的輪廓,我先把后面的大綱展示出來。

標(biāo)準(zhǔn)日志庫長啥樣

一個(gè)標(biāo)準(zhǔn)的日志庫一般有以下特點(diǎn):

  • 將日志內(nèi)容持久化到文件中,并同時(shí)注意磁盤io。上述面試題涉及到的就是這個(gè)。
  • 日志的基本信息需要盡量詳細(xì),需要包含文件,函數(shù)名,時(shí)間等等。
  • 支持不同的日志級(jí)別。我們所熟知的DEBUG/INFO/ERROR等等,說的就是這個(gè)。
  • 支持日志切割。支持的維度一般是時(shí)間,當(dāng)然也有根據(jù)文件大小的。

然而,在這里,我們只實(shí)現(xiàn)第一個(gè)訴求。

我們要做什么

經(jīng)過對(duì)需求的拆解,我們希望完成以下幾個(gè)功能:

  • 定時(shí)刷盤:每隔1s,將這1s內(nèi)的日志全部刷盤。
  • 超限刷盤:日志條數(shù)積壓到100條,將這100條日志日志全部刷盤。
  • 退出刷盤:程序(或服務(wù))退出時(shí),積壓在內(nèi)存中的日志全部刷盤。

自己動(dòng)手,豐衣足食

數(shù)據(jù)流

用戶先調(diào)用日志庫的New()。

此時(shí)程序會(huì)開啟一個(gè)異步協(xié)程,循環(huán)監(jiān)聽logger對(duì)象的buf。

logger對(duì)象返回用戶。

用戶通過調(diào)用logger對(duì)象的Info(),將日志輸出到日志庫。

logger對(duì)象的buf產(chǎn)生變更。

當(dāng)滿足以下條件之一的時(shí)候,buf中的日志會(huì)統(tǒng)一刷盤。

  • buf日志數(shù)達(dá)到100刷盤一次;
  • 每隔1s刷盤一次(如果buf有日志);
  • 程序退出刷盤1次。

數(shù)據(jù)結(jié)構(gòu)

既然,我們要做日志系統(tǒng),那么,數(shù)據(jù)結(jié)構(gòu)得先考慮好。

1、logger結(jié)構(gòu)體

(1)f

既然是日志系統(tǒng),那么必然有一個(gè)寫文件的過程。那么logger數(shù)據(jù)結(jié)構(gòu)中,應(yīng)該有一個(gè)文件指針字段f。

(2)bufMessages

以上三個(gè)功能中,都需要一個(gè)緩沖區(qū)暫存一些日志,到達(dá)一定的條件后,就將緩沖區(qū)的日志批量刷盤。因此,在logger結(jié)構(gòu)體中,需要有一個(gè)緩沖區(qū)切片bufMessages。

(3)mesChan

這里有一個(gè)問題,bufMessages是一個(gè)切片,線程不安全。如果日志并發(fā)寫入的話,會(huì)存在問題。這里主要有兩種方案解決,一種是加一個(gè)互斥鎖,一種是用channel。這里,我用了第二種方案。因此,logger結(jié)構(gòu)體多了一個(gè)channel。

2、message結(jié)構(gòu)體

當(dāng)然,我們還需要一個(gè)message結(jié)構(gòu)體,來存儲(chǔ)日志的詳細(xì)信息。這里,我們做得比較簡單,只有內(nèi)容和時(shí)間。

// 日志對(duì)象
type logger struct {
   f           *os.File      // 日志文件指針
   bufMessages []*message    // 存儲(chǔ)每一次需要同步的消息,相當(dāng)于緩沖區(qū),刷盤就會(huì)被清空
   mesChan     chan *message // 該管道接收每一條日志消息
}
// 日志消息
type message struct {
   content     string    // 日志內(nèi)容
   currentTime time.Time // 日志寫入時(shí)間
}

代碼框架

基于上述數(shù)據(jù)結(jié)構(gòu),我們可以把代碼的架子逐漸地搭起來:

1、調(diào)用日志庫的第一步:通過New()初始化一個(gè)logger對(duì)象

const (
   MsgChanLength = 10000           // 日志消息管道最大長度
   MaxMsgBufLength = 100            // 日志消息緩沖區(qū)最大長度
   FlushTimeInterval = time.Second // 日志刷盤周期
)
// 初始化一個(gè)logger對(duì)象
func New(logPath string) *logger {
   // 打開一個(gè)文件,這里的模式為創(chuàng)建或者追加
   f, err := os.OpenFile(logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0777)
   if err != nil {
      panic(err)
   }
   logger := &logger{
      f:           f,
      bufMessages: make([]*message, 0, MaxMsgBufLength),
      mesChan:     make(chan *message, MsgChanLength),
   }
   // todo: 這里需要做一點(diǎn)事兒,監(jiān)聽日志刷盤情況
   // ...
   return logger
}

2、當(dāng)用戶調(diào)用Info()的時(shí)候,日志消息直接進(jìn)入管道。

const (
   MsgChanLength = 10000           // 日志消息管道最大長度
   MaxMsgBufLength = 100            // 日志消息緩沖區(qū)最大長度
   FlushTimeInterval = time.Second // 日志刷盤周期
)
// 初始化一個(gè)logger對(duì)象
func New(logPath string) *logger {
   // 打開一個(gè)文件,這里的模式為創(chuàng)建或者追加
   f, err := os.OpenFile(logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0777)
   if err != nil {
      panic(err)
   }
   logger := &logger{
      f:           f,
      bufMessages: make([]*message, 0, MaxMsgBufLength),
      mesChan:     make(chan *message, MsgChanLength),
   }
   // todo: 這里需要做一點(diǎn)事兒,監(jiān)聽日志刷盤情況
   // ...
   return logger
}

3、格式化一下日志,讓日志好看一點(diǎn)兒~~

// 格式化一下日志,讓日志好看一點(diǎn)兒
func (l *logger) formatMsg(mes *message) string {
   builder := &strings.Builder{}
   builder.WriteString(mes.currentTime.Format("2006-01-02 15:04:05.999999"))
   builder.WriteString(" ")
   builder.WriteString(mes.content)
   builder.WriteString("\n")
   return builder.String()
}

4、批量將日志刷盤,實(shí)際上就是操作buf,然后將buf清空的過程。

// 公用方法:批量將buf內(nèi)容刷新到日志文件
// 由于該方法放在同一個(gè)select中調(diào)用,因此線程安全
func (l *logger) batchFlush() (err error) {
   builder := strings.Builder{}
   for _, mes := range l.bufMessages {
      // 將所有的buffer內(nèi)容全部拼接起來
      builder.WriteString(l.formatMsg(mes))
   }
   content := builder.String()
   if content == "" {
      return
   }
   // 重置bufMessages
   l.bufMessages = make([]*message, 0, MaxMsgBufLength)
   // 寫入日志文件
   _, err = l.f.WriteString(content)
   if err != nil {
      fmt.Println("寫入日志文件失敗,", err)
      return
   }
   fmt.Println("成功寫入日志文件,", time.Now().String())
   return
}

定時(shí)刷盤

既然是要定時(shí)刷盤,我們很容易想到用ticker來處理。

func (l *logger) listenFlush() {
   // 注冊定時(shí)器
   ticker := time.NewTicker(FlushTimeInterval)
   // 這里我們使用select-case組合,對(duì)channel進(jìn)行優(yōu)雅處理
   for {
      select {
      case <-ticker.C:
         fmt.Println("每隔1s,將日志刷盤")
         l.batchFlush()
      }
   }
}

由于這個(gè)函數(shù)會(huì)阻塞,因此,我們要將其放在一個(gè)協(xié)程中,這個(gè)協(xié)程應(yīng)該在New()中去創(chuàng)建。

// 初始化一個(gè)logger對(duì)象
func New(logPath string) *logger {
   // 打開一個(gè)文件,這里的模式為創(chuàng)建或者追加
   f, err := os.OpenFile(logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0777)
   if err != nil {
      panic(err)
   }
   logger := &logger{
      f:           f,
      bufMessages: make([]*message, 0, MaxMsgBufLength),
      mesChan:     make(chan *message, MsgChanLength),
   }
   // 監(jiān)聽定時(shí)日志刷盤情況
   go logger.listenFlush()
   return logger
}

超限刷盤

“超限刷盤”的實(shí)現(xiàn),其實(shí)就是接收mesChan的消息,將其塞到bufMessages中,當(dāng)bufMessages達(dá)到100條,則觸發(fā)批量刷盤。由于我們不清楚啥時(shí)候需要close這個(gè)mesChan,因此,我們需要用for-select-case來接收其消息。這里,我們可以把它加到ListenFlush中。

func (l *logger) listenFlush() {
   // 注冊定時(shí)器
   ticker := time.NewTicker(FlushTimeInterval)
   for {
      select {
      case mes := <-l.mesChan:
         l.bufMessages = append(l.bufMessages, mes)
         if len(l.bufMessages) == MaxMsgBufLength {
            fmt.Println("緩沖區(qū)日志到達(dá)上限,將日志刷盤")
            l.batchFlush()
         }
      case <-ticker.C:
         fmt.Println("每隔1s,將日志刷盤")
         l.batchFlush()
      }
   }
}

退出刷盤

無論是上面的“定時(shí)刷盤”,還是“超限刷盤”,都有一個(gè)問題,那就是,當(dāng)主進(jìn)程退出后,如果bufMessages中還有日志,那么這部分日志就會(huì)丟失。為了解決這個(gè)問題,我們可以做一個(gè)優(yōu)化,利用golang的結(jié)束信號(hào),讓程序優(yōu)雅退出:退出前將buffer刷盤。

這部分代碼,我們可以加到上限刷盤的代碼里面:

func (l *logger) listenFlush() {
   // 這里,我們加一個(gè)結(jié)束信號(hào),來優(yōu)雅退出
   c := make(chan os.Signal)
   // 監(jiān)聽信號(hào)
   signal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
   // 注冊定時(shí)器
   ticker := time.NewTicker(FlushTimeInterval)
   for {
      select {
      case mes := <-l.mesChan:
         l.bufMessages = append(l.bufMessages, mes)
         if len(l.bufMessages) == MaxMsgBufLength {
            fmt.Println("緩沖區(qū)日志到達(dá)上限,將日志刷盤")
            l.batchFlush()
         }
      case <-ticker.C:
         fmt.Println("每隔1s,將日志刷盤")
         l.batchFlush()
      case <-c:
         fmt.Println("收到結(jié)束信號(hào),將日志刷盤")
         l.batchFlush()
         return
      }
   }
}

這樣,我們就完成了整個(gè)日志庫。

完整代碼

package log
import (
   "fmt"
   "os"
   "os/signal"
   "strings"
   "syscall"
   "time"
)
const (
   MsgChanLength = 10000       // 日志消息管道最大長度
   MaxMsgBufLength = 100        // 日志消息緩沖區(qū)最大長度
   FlushTimeInterval = time.Second // 日志刷盤周期
)
// 日志對(duì)象
type logger struct {
   f           *os.File      // 日志文件指針
   bufMessages []*message    // 存儲(chǔ)每一次需要同步的消息,相當(dāng)于緩沖區(qū),刷盤就會(huì)被清空
   mesChan     chan *message // 該管道接收每一條日志消息
}
// 日志消息
type message struct {
   content     string    // 日志內(nèi)容
   currentTime time.Time // 日志寫入時(shí)間
}
// 初始化一個(gè)logger對(duì)象
func New(logPath string) *logger {
   // 打開一個(gè)文件,這里的模式為創(chuàng)建或者追加
   f, err := os.OpenFile(logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0777)
   if err != nil {
      panic(err)
   }
   logger := &logger{
      f:           f,
      bufMessages: make([]*message, 0, MaxMsgBufLength),
      mesChan:     make(chan *message, MsgChanLength),
   }
   // 監(jiān)聽日志buf刷盤情況
   go logger.listenFlush()
   return logger
}
// 格式化一下日志,讓日志好看一點(diǎn)兒
func (l *logger) formatMsg(mes *message) string {
   builder := &strings.Builder{}
   builder.WriteString(mes.currentTime.Format("2006-01-02 15:04:05.999999"))
   builder.WriteString(" ")
   builder.WriteString(mes.content)
   builder.WriteString("\n")
   return builder.String()
}
// 將日志入隊(duì)
func (l *logger) Info(content string) {
   l.mesChan <- &message{
      content:     content,
      currentTime: time.Now(),
   }
}
// 批量將buf內(nèi)容刷新到日志文件
func (l *logger) batchFlush() (err error) {
   builder := strings.Builder{}
   for _, mes := range l.bufMessages {
      // 將所有的buffer內(nèi)容全部拼接起來
      builder.WriteString(l.formatMsg(mes))
   }
   content := builder.String()
   if content == "" {
      return
   }
   // 重置bufMessages
   l.bufMessages = make([]*message, 0, MaxMsgBufLength)
   // 寫入日志文件
   _, err = l.f.WriteString(content)
   if err != nil {
      fmt.Println("寫入日志文件失敗,", err)
      return
   }
   fmt.Println("成功寫入日志文件,", time.Now().String())
   return
}
// 監(jiān)聽刷盤情況
func (l *logger) listenFlush() {
   // 這里,我們加一個(gè)結(jié)束信號(hào),來優(yōu)雅退出
   c := make(chan os.Signal)
   // 監(jiān)聽信號(hào)
   signal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
   // 注冊定時(shí)器
   ticker := time.NewTicker(FlushTimeInterval)
   for {
      select {
      case mes := <-l.mesChan:
         l.bufMessages = append(l.bufMessages, mes)
         if len(l.bufMessages) == MaxMsgBufLength {
            fmt.Println("緩沖區(qū)日志到達(dá)上限,將日志刷盤")
            l.batchFlush()
         }
      case <-ticker.C:
         fmt.Println("每隔1s,將日志刷盤")
         l.batchFlush()
      case <-c:
         fmt.Println("收到結(jié)束信號(hào),將日志刷盤")
         l.batchFlush()
         return
      }
   }
}

測試文件:log_test.go

package log
import (
   "math/rand"
   "testing"
   "time"
)
// 模擬生產(chǎn)場景進(jìn)行日志輸出
func TestBatchLog(t *testing.T) {
   // 初始化一個(gè)logger對(duì)象
   logger := New("./batch.log")
   for i := 0; i < 1000; i++ {
      // 開啟1000個(gè)協(xié)程,每個(gè)協(xié)程都是一個(gè)死循環(huán),不定期地往batch.log里面寫日志
      go func() {
         for {
            n := rand.Intn(100)
            logger.Info("hello" + time.Now().String())
            time.Sleep(time.Duration(n) * time.Millisecond)
         }
      }()
   }
   // 阻塞程序
   select {}
}

小結(jié)

雖然,這個(gè)日志庫和zerolog等標(biāo)準(zhǔn)日志庫相比,還有一定的差距,但是,通過這個(gè)小巧的項(xiàng)目,我們可以對(duì)golang的并發(fā)處理有更加清晰的認(rèn)識(shí),例如channel、select、定時(shí)器的使用,golang程序退出的優(yōu)雅處理,切片非線程安全的應(yīng)對(duì)等等。

不僅如此,我們還可以在此基礎(chǔ)上繼續(xù)探索,一個(gè)標(biāo)準(zhǔn)日志庫的實(shí)現(xiàn),需要具備哪些要素,甚至打造出一套深受業(yè)界青睞的好庫。

以上就是一文教你打造一個(gè)簡易的Golang日志庫的詳細(xì)內(nèi)容,更多關(guān)于Golang日志庫的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • LRU?LFU?TinyLFU緩存算法實(shí)例詳解

    LRU?LFU?TinyLFU緩存算法實(shí)例詳解

    這篇文章主要為大家介紹了LRU?LFU?TinyLFU緩存算法實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-09-09
  • Go語言中更優(yōu)雅的錯(cuò)誤處理

    Go語言中更優(yōu)雅的錯(cuò)誤處理

    Go語言中的錯(cuò)誤處理是一個(gè)被大家經(jīng)常拿出來討論的話題(另外一個(gè)是泛型)。篇文章我們將討論一下如何在現(xiàn)行的 Golang 框架下提供更友好和優(yōu)雅的錯(cuò)誤處理。需要的朋友們可以參考借鑒,下面來一起看看吧。
    2017-02-02
  • 詳解go 動(dòng)態(tài)數(shù)組 二維動(dòng)態(tài)數(shù)組

    詳解go 動(dòng)態(tài)數(shù)組 二維動(dòng)態(tài)數(shù)組

    這篇文章主要介紹了go 動(dòng)態(tài)數(shù)組 二維動(dòng)態(tài)數(shù)組,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2020-07-07
  • RabbitMQ延時(shí)消息隊(duì)列在golang中的使用詳解

    RabbitMQ延時(shí)消息隊(duì)列在golang中的使用詳解

    延時(shí)隊(duì)列常使用在某些業(yè)務(wù)場景,使用延時(shí)隊(duì)列可以簡化系統(tǒng)的設(shè)計(jì)和開發(fā)、提高系統(tǒng)的可靠性和可用性、提高系統(tǒng)的性能,下面我們就來看看如何在golang中使用RabbitMQ的延時(shí)消息隊(duì)列吧
    2023-11-11
  • 使用client go實(shí)現(xiàn)自定義控制器的方法

    使用client go實(shí)現(xiàn)自定義控制器的方法

    本文我們來使用client-go實(shí)現(xiàn)一個(gè)自定義控制器,通過判斷service的Annotations屬性是否包含ingress/http,如果包含則創(chuàng)建ingress,如果不包含則不創(chuàng)建,對(duì)client go自定義控制器相關(guān)知識(shí)感興趣的朋友一起看看吧
    2022-05-05
  • Goland 斷點(diǎn)調(diào)試Debug的操作

    Goland 斷點(diǎn)調(diào)試Debug的操作

    這篇文章主要介紹了Goland 斷點(diǎn)調(diào)試Debug的操作方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2021-04-04
  • 詳解在Go語言單元測試中如何解決文件依賴問題

    詳解在Go語言單元測試中如何解決文件依賴問題

    現(xiàn)如今的?Web?應(yīng)用程序往往采用?RESTful?API?接口形式對(duì)外提供服務(wù),后端接口直接向前端返回?HTML?文件的情況越來越少,所以在程序中操作文件的場景也變少了,在編寫單元測試時(shí),文件就成了被測試代碼的外部依賴,本文就來講解下測試過程中如何解決文件外部依賴問題
    2023-08-08
  • Go defer使用時(shí)的兩個(gè)常見陷阱與避免方法

    Go defer使用時(shí)的兩個(gè)常見陷阱與避免方法

    這篇文章主要將帶大家一起深入探討 Go 1.20 中 defer 的優(yōu)化機(jī)制,并揭示在使用 defer 時(shí)需要避免的兩個(gè)常見陷阱,有需要的可以了解下
    2025-03-03
  • Go type關(guān)鍵字(類型定義與類型別名的使用差異)用法實(shí)例探究

    Go type關(guān)鍵字(類型定義與類型別名的使用差異)用法實(shí)例探究

    這篇文章主要為大家介紹了Go type關(guān)鍵字(類型定義與類型別名的使用差異)用法實(shí)例探究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2024-01-01
  • golang兩種調(diào)用rpc的方法

    golang兩種調(diào)用rpc的方法

    這篇文章主要介紹了golang兩種調(diào)用rpc的方法,結(jié)合實(shí)例形式分析了Go語言調(diào)用rpc的原理與實(shí)現(xiàn)方法,需要的朋友可以參考下
    2016-07-07

最新評(píng)論