Go語言實現紀錄檔系統(支援多種輸出方式)

2020-07-16 10:05:17
紀錄檔可以用於檢視和分析應用程式的執行狀態。紀錄檔一般可以支援輸出多種形式,如命令列、檔案、網路等。

本例將搭建一個支援多種寫入器的紀錄檔系統,可以自由擴充套件多種紀錄檔寫入裝置。

紀錄檔對外介面

本例中定義一個紀錄檔寫入器介面(LogWriter),要求寫入裝置必須遵守這個介面協定才能被紀錄檔器(Logger)註冊。紀錄檔器有一個寫入器的註冊方法(Logger 的 RegisterWriter() 方法)。

紀錄檔器還有一個 Log() 方法,進行紀錄檔的輸出,這個函數會將紀錄檔寫入到所有已經註冊的紀錄檔寫入器(LogWriter)中,詳細程式碼實現請參考下面的程式碼。
package main

// 宣告紀錄檔寫入器介面
type LogWriter interface {
    Write(data interface{}) error
}

// 紀錄檔器
type Logger struct {
    // 這個紀錄檔器用到的紀錄檔寫入器
    writerList []LogWriter
}

// 註冊一個紀錄檔寫入器
func (l *Logger) RegisterWriter(writer LogWriter) {
    l.writerList = append(l.writerList, writer)
}

// 將一個data型別的資料寫入紀錄檔
func (l *Logger) Log(data interface{}) {

    // 遍歷所有註冊的寫入器
    for _, writer := range l.writerList {

        // 將紀錄檔輸出到每一個寫入器中
        writer.Write(data)
    }
}

// 建立紀錄檔器的範例
func NewLogger() *Logger {
    return &Logger{}
}
程式碼說明如下:
第 4 行,宣告紀錄檔寫入器介面。這個介面可以被外部使用。紀錄檔的輸出可以有多種裝置,這個寫入器就是用來實現一個紀錄檔的輸出裝置。
第 9 行,宣告紀錄檔器結構。紀錄檔器使用 writeList 記錄輸出到哪些裝置上。
第 15 行,使用紀錄檔器方法 RegisterWriter() 將一個紀錄檔寫入器(LogWriter)註冊到紀錄檔器(Logger)中。註冊的意思就是將紀錄檔寫入器的介面新增到 writeList 中。
第 20 行,紀錄檔器的 Log() 方法可以將 interface{} 型別的 data 寫入到註冊過的紀錄檔寫入器中。
第 23 行,遍歷紀錄檔器擁有的所有紀錄檔寫入器。
第 26 行,將本次紀錄檔的內容寫入紀錄檔寫入器。
第 31 行,建立紀錄檔器的範例。

這個例子中,為了最大程度地展示介面的用法,僅僅只是將資料直接寫入紀錄檔寫入器中。複雜一些的紀錄檔器還可以將日期、級別等資訊合併到資料中一併寫入紀錄檔。

檔案寫入器

檔案寫入器(fileWriter)是眾多紀錄檔寫入器(LogWriter)中的一種。檔案寫入器的功能是根據一個檔名建立紀錄檔檔案(fileWriter 的 SetFile 方法)。在有紀錄檔寫入時,將紀錄檔寫入檔案中。

檔案寫入器程式碼:
package main

import (
    "errors"
    "fmt"
    "os"
)

// 宣告檔案寫入器
type fileWriter struct {
    file *os.File
}

// 設定檔案寫入器寫入的檔名
func (f *fileWriter) SetFile(filename string) (err error) {

    // 如果檔案已經開啟, 關閉前一個檔案
    if f.file != nil {
        f.file.Close()
    }

    // 建立一個檔案並儲存檔案控制代碼
    f.file, err = os.Create(filename)

    // 如果建立的過程出現錯誤, 則返回錯誤
    return err
}

// 實現LogWriter的Write()方法
func (f *fileWriter) Write(data interface{}) error {

    // 紀錄檔檔案可能沒有建立成功
    if f.file == nil {

        // 紀錄檔檔案沒有準備好
        return errors.New("file not created")
    }

    // 將資料序列化為字串
    str := fmt.Sprintf("%vn", data)

    // 將資料以位元組陣列寫入檔案中
    _, err := f.file.Write([]byte(str))

    return err
}

// 建立檔案寫入器範例
func newFileWriter() *fileWriter {
    return &fileWriter{}
}
程式碼說明如下:
  • 第 10 行,宣告檔案寫入器,在結構體中儲存一個檔案控制代碼,以方便每次寫入時操作。
  • 第 15 行,檔案寫入器通過檔名建立檔案,這裡通過 SetFile 的引數提供一個檔名,並建立檔案。
  • 第 18 行,考慮到 SetFile() 方法可以被多次呼叫(函數可重入性),假設之前已經呼叫過 SetFile() 後再次呼叫,此時的 f.file 不為空,就需要關閉之前的檔案,重新建立新的檔案。
  • 第 23 行,根據檔名建立檔案,如果發生錯誤,通過 SetFile 的返回值返回。
  • 第 30 行,fileWriter 的 Write() 方法實現了 LogWriter 介面的 Write() 方法。
  • 第 33 行,如果檔案沒有準備好,檔案控制代碼為 nil,此時使用 errors 包的 New() 函數返回一個錯誤物件,包含一個字串“file not created”。
  • 第 40 行,通過 Write() 方法傳入的 data 引數是 interface{} 型別,而 f.file 的 Write() 方法需要的是 []byte 型別。使用 fmt.Sprintf 將 data 轉換為字串,這裡使用的格式化引數是%v,意思是將 data 按其本來的值轉換為字串。
  • 第 43 行,通過 f.file 的 Write() 方法,將 str 字串轉換為 []byte 位元組陣列,再寫入到檔案中。如果發生錯誤,則返回。

在操作檔案時,會出現檔案無法建立、無法寫入等錯誤。開發中盡量不要忽略這些底層報出的錯誤,應該處理可能發生的所有錯誤。

檔案使用完後,要注意使用 os.File 的 Close() 方法進行及時關閉,否則檔案再次存取時會因為其屬性出現無法讀取、無法寫入等錯誤。

提示

一個完備的檔案寫入器會提供多種寫入檔案的模式,例子中使用的模式是將紀錄檔新增到紀錄檔檔案的尾部。隨著檔案越來越大,檔案的存取效率和檢視便利性也會大大降低。此時,就需要另外一種寫入模式:捲動寫入檔案。

捲動寫入檔案模式也是將紀錄檔新增到檔案的尾部,但當檔案達到設定的期望大小時,會自動開啟一個新的檔案繼續寫入檔案,最終將獲得多個紀錄檔檔案。

紀錄檔檔名不僅可以按照檔案大小進行分割,還可以按照日期範圍進行分割。在到達設定的日期範圍,如每天、每小時的周期範圍時,紀錄檔器會自動建立新的紀錄檔檔案。這種紀錄檔檔案建立方法也能方便開發者按紀錄檔檢視紀錄檔。

命令列寫入器

在 UNIX 的思想中,一切皆檔案。檔案包括記憶體、磁碟、網路和命令列等。這種抽象方法方便我們存取這些看不見摸不著的虛擬資源。命令列在Go語言中也是一種檔案,os.Stdout 對應標準輸出,一般表示螢幕,也就是命令列,也可以被重定向為印表機或者磁碟檔案;os.Stderr 對應標準錯誤輸出,一般將錯誤輸出到紀錄檔中,不過大多數情況,os.Stdout 會與 os.Stderr 合併輸出;os.Stdin 對應標準輸入,一般表示鍵盤。os.Stdout、os.Stderr、os.Stdin 都是 *os.File 型別,和檔案一樣實現了 io.Writer 介面的 Write() 方法。

下面的程式碼展示如何將命令列抽象為紀錄檔寫入器:
package main

import (
    "fmt"
    "os"
)

// 命令列寫入器
type consoleWriter struct {
}

// 實現LogWriter的Write()方法
func (f *consoleWriter) Write(data interface{}) error {

    // 將資料序列化為字串
    str := fmt.Sprintf("%vn", data)

    // 將資料以位元組陣列寫入命令列中
    _, err := os.Stdout.Write([]byte(str))

    return err
}

// 建立命令列寫入器範例
func newConsoleWriter() *consoleWriter {
    return &consoleWriter{}
}
程式碼說明如下:
  • 第 9 行,宣告 consoleWriter 結構,以實現命令列寫入器。
  • 第 13 行,consoleWriter 的 Write() 方法實現了紀錄檔寫入介面(LogWriter)的 Write() 方法。
  • 第 16 行,與 fileWriter 類似,這裡也將 data 通過 fmt.Sprintf 序列化為字串。
  • 第 19 行,與 fileWriter 類似,這裡也將 str 字串轉換為位元組陣列並寫入標準輸出 os.Stdout。寫入後的內容就會顯示在命令列中。
  • 第 25 行,建立命令列寫入器的範例。

除了命令列寫入器(consoleWriter)和檔案寫入器(fileWriter),讀者還可以自行使用 net 包中的 Socket 封裝實現網路寫入器 socketWriter,讓紀錄檔可以寫入遠端的伺服器中或者可以跨進程進行紀錄檔儲存和分析。

使用紀錄檔

在程式中使用紀錄檔器一般會先通過程式碼建立紀錄檔器(Logger),為紀錄檔器新增輸出裝置(fileWriter、consoleWriter等)。這些裝置中有一部分需要一些引數設定,如檔案紀錄檔寫入器需要提供檔名(fileWriter 的 SetFile() 方法)。

下面程式碼中展示了使用紀錄檔器的過程:
package main

import "fmt"

// 建立紀錄檔器
func createLogger() *Logger {

    // 建立紀錄檔器
    l := NewLogger()

    // 建立命令列寫入器
    cw := newConsoleWriter()

    // 註冊命令列寫入器到紀錄檔器中
    l.RegisterWriter(cw)

    // 建立檔案寫入器
    fw := newFileWriter()

    // 設定檔名
    if err := fw.SetFile("log.log"); err != nil {
            fmt.Println(err)
    }

    // 註冊檔案寫入器到紀錄檔器中
    l.RegisterWriter(fw)

    return l
}

func main() {

    // 準備紀錄檔器
    l := createLogger()

    // 寫一個紀錄檔
    l.Log("hello")
}
程式碼說明如下:
  • 第 6 行,一個建立紀錄檔的過程。這個過程一般隱藏在系統初始化中。程式啟動時初始化一次。
  • 第 9 行,建立一個紀錄檔器的範例,後面的程式碼會使用到它。
  • 第 12 行,建立一個命令列寫入器。如果全域性有很多紀錄檔器,命令列寫入器可以被共用,全域性只會有一份。
  • 第 18 行,建立一個檔案寫入器。一個程式的紀錄檔一般只有一個,因此不同的紀錄檔器也應該共用一個檔案寫入器。
  • 第 21 行,建立好的檔案寫入器需要初始化寫入的檔案,通過檔名確定寫入的檔案。設定的過程可能會發生錯誤,發生錯誤時會輸出錯誤資訊。
  • 第 26 行,將檔案寫入器註冊到紀錄檔器中。
  • 第 34 行,在程式一開始建立紀錄檔器。
  • 第 37 行,往建立好的紀錄檔器中寫入紀錄檔。

編譯整個程式碼並執行,輸出如下:

hello

同時,當前目錄的 log.log 檔案中也會出現 hello 字元。

提示

Go語言的 log 包實現了一個小型的紀錄檔系統。這個紀錄檔系統可以在建立紀錄檔器時選擇輸出裝置、紀錄檔字首及 flag,函數定義如下:
func New(out io.Writer, prefix string, flag int) *Logger {
    return &Logger{out: out, prefix: prefix, flag: flag}
}
在 flag 中,還可以客製化紀錄檔中是否輸出日期、日期精度和詳細檔名等。

這個紀錄檔器在編寫時,也最大程度地保證了輸出的效率,如果讀者對紀錄檔器的編寫比較感興趣,可以在 log 包的基礎上進行擴充套件,形成方便自己使用的紀錄檔庫。