C語言中文網 目錄
首頁 > Go語言教程 > Go語言接口 閱讀:3,382

Go語言實現日志系統(支持多種輸出方式)

日志可以用于查看和分析應用程序的運行狀態。日志一般可以支持輸出多種形式,如命令行、文件、網絡等。

本例將搭建一個支持多種寫入器的日志系統,可以自由擴展多種日志寫入設備。

日志對外接口

本例中定義一個日志寫入器接口(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("%v\n", 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("%v\n", 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 包的基礎上進行擴展,形成方便自己使用的日志庫。

精美而實用的網站,提供C語言C++STLLinuxShellJavaGo語言等教程,以及socketGCCviSwing設計模式JSP等專題。

Copyright ?2011-2018 biancheng.net, 陜ICP備15000209號

底部Logo