最近使用 wails 库做一个工具时,遇到了日志的问题。为了方便查找问题,我选择使用 logrus 库打印日志,且为了方便开发和正式运行时查看日志,选择使用如下的方式设置,让日志同时打印到文件和控制台上:
f, err := os.OpenFile("logs.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0755)
if err != nil {
log.Fatal(err)
}
log.SetOutput(io.MultiWriter(os.Stdout, f))
在开发工具中时,一切正常,但打包之后虽然创建了日志文件,但却没有内容。一开始查找 logrus 库的问题,没有找到相关的情况,后面又怀疑时 wails 库的原因,也找不到答案。
看了 wails 也有日志功能,猜测可能需要使用自带的日志库才能正确输出,不过修改了之后也仅仅是会将网页端的日志也记录下来,但打包后的应用还是没有输出日志。
在一度的迷茫之后,忽然想到如果只用文件呢?测试了一下,只输出到文件就正常了,开发/或打包成应用后都能输出到文件。
于是猜测,可能是打包之后的应用,os.Stdout 也许没有了?查看 io.MultiWriter 的实现:
func (t *multiWriter) Write(p []byte) (n int, err error) {
for _, w := range t.writers {
n, err = w.Write(p)
if err != nil {
return
}
if n != len(p) {
err = ErrShortWrite
return
}
}
return len(p), nil
}
果然其 Write 方法就是循环写入,有一个报错,后面就返回了。而我设置的顺序,文件恰好在后面,于是还没到它就跳过了。
那就得检查一下 os.Stdout 是否能写入,AI 表示,写了试一下就知道了:
if _, err = os.Stdout.Write([]byte{}); err == nil {
log.SetOutput(io.MultiWriter(os.Stdout, f))
} else {
log.SetOutput(f)
}
没想到这样又不行了,意思是 os.Stdout.Write 并没有报错。尝试在 []byte{} 中加入内容,不写入空的字节切片,没想到又可以了,比如这样:
if _, err = os.Stdout.Write([]byte{1}); err == nil {
log.SetOutput(io.MultiWriter(os.Stdout, f))
} else {
log.SetOutput(f)
}
查看 os.Stdout.Write 在 windows 上的实现,go\pkg\mod\golang.org\toolchain@v0.0.1-go1.23.4.windows-amd64\src\internal\poll\fd_windows.go
func (fd *FD) Write(buf []byte) (int, error) {
if err := fd.writeLock(); err != nil {
return 0, err
}
defer fd.writeUnlock()
if fd.isFile {
fd.l.Lock()
defer fd.l.Unlock()
}
ntotal := 0
for len(buf) > 0 {
b := buf
if len(b) > maxRW {
b = b[:maxRW]
}
var n int
var err error
if fd.isFile {
switch fd.kind {
case kindConsole:
n, err = fd.writeConsole(b)
default:
n, err = syscall.Write(fd.Sysfd, b)
if fd.kind == kindPipe && err == syscall.ERROR_OPERATION_ABORTED {
// Close uses CancelIoEx to interrupt concurrent I/O for pipes.
// If the fd is a pipe and the Write was interrupted by CancelIoEx,
// we assume it is interrupted by Close.
err = ErrFileClosing
}
}
if err != nil {
n = 0
}
} else {
if race.Enabled {
race.ReleaseMerge(unsafe.Pointer(&ioSync))
}
o := &fd.wop
o.InitBuf(b)
n, err = execIO(o, func(o *operation) error {
return syscall.WSASend(o.fd.Sysfd, &o.buf, 1, &o.qty, 0, &o.o, nil)
})
}
ntotal += n
if err != nil {
return ntotal, err
}
buf = buf[n:]
}
return ntotal, nil
}
可以看到,当传入的内容为空切片时,直接就跳过返回了,也没有错误。
总结:wails 在编译为正式运行的程序时,会自动加上 -H windowsgui 标记,避免弹出控制台界面,此时的 os.Stdout 写入内容是会报错的,测试会返回 /dev/stdout: The handle is invalid. 错误,但写入空字节切片不会报错。而 logrus 使用 io.MultiWriter 来实现多处输出时,MultiWriter 中的 Write 方法是循环挨个写入 writer,如果遇到报错,就直接返回了,会让后面的就跳过了,不写入。