在golang中封装日志设置的正确模式

时间:2014-08-16 18:47:15

标签: go deferred-execution

当尝试将日志设置代码移动到单独的函数中时,我遇到了无法从main函数隐藏目标文件对象的问题。在下面的 INCORRECT 简化示例中,尝试通过单个函数调用设置日志写入Stderr和文件:

package main

import (
    "io"
    "log"
    "os"
)

func SetupLogging() {
    logFile, err := os.OpenFile("test.log", os.O_APPEND|os.O_CREATE, 0666)
    if err != nil {
        log.Panicln(err)
    }
    defer logFile.Close()

    log.SetOutput(io.MultiWriter(os.Stderr, logFile))
}

func main() {
    SetupLogging()
    log.Println("Test message")
}

显然不起作用,因为defer关闭SetupLogging函数末尾的日志文件。

下面的一个工作示例添加了额外的代码,如果在较大的应用程序中重复作为模式,IMHO会失去一些清晰度:

package main

import (
    "io"
    "log"
    "os"
)

func SetupLogging() *os.File {
    logFile, err := os.OpenFile("test.log", os.O_APPEND|os.O_CREATE, 0666)
    if err != nil {
        log.Panicln(err)
    }

    log.SetOutput(io.MultiWriter(os.Stderr, logFile))
    return logFile
}

func main() {
    logf := SetupLogging()
    defer logf.Close()

    log.Println("Test message")
}

有没有不同的方法将开放文件管理完全封装到一个函数中,但仍然可以很好地释放句柄?

4 个答案:

答案 0 :(得分:5)

我现在已经成功地在多个项目中使用了以下方法大约一年。我们的想法是从设置调用中返回一个函数。结果函数包含销毁逻辑。这是一个例子:

package main

import (
    "fmt"
    "io"
    "log"
    "os"
)

func LogSetupAndDestruct() func() {
    logFile, err := os.OpenFile("test.log", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)
    if err != nil {
        log.Panicln(err)
    }

    log.SetOutput(io.MultiWriter(os.Stderr, logFile))

    return func() {
        e := logFile.Close()
        if e != nil {
            fmt.Fprintf(os.Stderr, "Problem closing the log file: %s\n", e)
        }
    }
}

func main() {
    defer LogSetupAndDestruct()()

    log.Println("Test message")
}

正在使用一个关闭清理逻辑的闭包。

使用此方法的一个更精细的公开示例在Viper代码中:here is the return from a test initializerhere it is used to encapsulate the cleanup logic and objects

答案 1 :(得分:1)

执行此操作的正确方法是将main中的句柄传递给SetupLogging

func SetupLogging(lf *os.File) {
    log.SetOutput(io.MultiWriter(os.Stderr, logFile))
    log.Println("Started")
}

func main() {
    logFile, err := os.OpenFile("test.log", os.O_APPEND|os.O_CREATE, 0666)
    if err != nil {
        log.Panicln(err)
    }
    defer logFile.Close()
    SetupLogging(logFile)
    log.Println("Test message")
}

另一种选择是使用runtime.SetFinalizer,但并不总是保证在主要退出之前运行。

func SetupLogging() {
    logFile, err := os.OpenFile("test.log", os.O_APPEND|os.O_CREATE, 0666)
    if err != nil {
        log.Panicln(err)
    }
    runtime.SetFinalizer(logFile, func(h *os.File) {
        h.Close()
    })

    log.SetOutput(io.MultiWriter(os.Stderr, logFile))
}

答案 2 :(得分:1)

您可以使用频道执行此操作,这是我的方法

type InfoLog struct {
    InfoChan chan string
    CloseChan chan struct{} //empty signal
    log *log.Logger
    file *os.File
}

func NewInfoLog(file *os.File) *InfoLog {
    return &InfoLog{
        InfoChan: make(chan string),
        CloseChan: make(chan struct{}),
        log: log.New(file, "TAG", log.Ldate|log.Ltime),
        file: file,
    }
}

func (i *InfoLog) listen() {
    for {
        select {
        case infoMsg := <-i.InfoChan:
            i.log.Println(infoMsg)
        case <-i.CloseChan:
            i.file.Close()
            close(i.InfoChan)
        }
    }
}

然后在主

func main() {
    infoLog := NewInfoLog(ANY_OPEN_FILE_HERE)
    go infoLog.listen()
    infoLog.InfoChan <- "msg"
    infoLog.InfoChan <- "msg"
    infoLog.InfoChan <- "msg"
    infoLog.CloseChan <- struct{}{}
    // exits normaly
}

您可以看到我为完整示例制作的异步日志系统:https://github.com/sescobb27/ciudad-gourmet/blob/master/services/log_service.go

答案 3 :(得分:0)

如果需要多个“拆卸”流程,那么使用Google上下文包(https://blog.golang.org/context)就可以找到很好的解决方案。优点是您可以使用单个上下文拆除所有当前正在执行的过程。像这样的人:

package main

import (
    "fmt"
    "io"
    "log"
    "os"

    "golang.org/x/net/context"
)

func LogSetup(ctx context.Context) error {
    logFile, err := os.OpenFile("test.log", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)
    if err != nil {
        return err
    }

    log.SetOutput(io.MultiWriter(os.Stderr, logFile))
    // here we could f.ex. execute:
    // sendLogOutputToExternalService(ctx)
    // and it could have it's own teardown procedure
    // which would be called on main context's expiration
    go func() {
       for _ = range ctx.Done() {
         err := logFile.Close()
         if err = nil {
             fmt.Fprintf(os.Stderr, "Problem closing the log file: %s\n", e)
        }
    }()
    return nil
}


func main() {
    var stopAll func()
    mainContext, stopAll = context.WithCancel(context.Background())
    defer stopAll()

    err := LogSetup(mainContext)
    if err!=nil {
        log.Fatal("error while initializing logging") 
    }

    log.Println("Test message")
}