Go如何结合tail与utfutil读取UTF16LE BOM格式实时日志
Go实时tail追踪UTF16LE BOM编码持续写入日志实现方案
现有代码的核心问题
你写的接近可用版本的问题根本不是defer Close失效——utfutil.OpenFile返回的编码读取器的Close()方法会自动关闭底层操作系统文件句柄,你写的defer file.Close()本身是正确的。真正导致功能异常的是两个逻辑缺陷:
- 读到EOF时没有处理半行场景:游戏写入日志是逐块写入的,你可能在某行写到一半的时候触发读取,这时候拿到的半截内容会被误判为完整行,后续新写入的该行剩余内容会被拆成碎段
- 循环读取时没有处理缓存残留:bufio触发EOF后,内部缓存的残留状态如果不做半行暂存,会导致后续新写入的内容可能被漏读
修复后的完整实现
代码直接适配EVE聊天日志场景,内置"die"关键词标记逻辑,CPU占用低,不会漏读、不会乱码、句柄可正常释放:
package main import ( "bufio" "bytes" "fmt" "io" "os" "strings" "time" "github.com/TomOnTime/utfutil" ) func main() { logPath := "C:/Users/user/Documents/EVE/logs/Chatlogs/chat_20220709_022129_1006197774.txt" // 用utfutil打开自动识别UTF16LE BOM,转成标准UTF8流 f, err := utfutil.OpenFile(logPath, utfutil.WINDOWS) if err != nil { fmt.Printf("打开日志文件失败: %v\n", err) return } defer f.Close() reader := bufio.NewReader(f) // 缓存未读完的半行内容 var partialLine bytes.Buffer for { line, err := reader.ReadString('\n') if err != nil { if err == io.EOF { // 把当前读到的半截内容存到缓存,等下次写入补全 if len(line) > 0 { partialLine.WriteString(line) } // 休眠500ms避免占满CPU time.Sleep(500 * time.Millisecond) // 检查文件是否被游戏轮转/删除,触发等待逻辑 if _, statErr := os.Stat(logPath); os.IsNotExist(statErr) { fmt.Println("日志文件被轮转,等待新文件生成...") time.Sleep(1 * time.Second) continue } continue } fmt.Printf("读取文件出错: %v\n", err) break } // 拼接之前缓存的半行 + 当前读到的行,组成完整行 fullLine := partialLine.String() + line partialLine.Reset() // 去掉首尾空白字符 fullLine = strings.TrimSpace(fullLine) if len(fullLine) == 0 { continue } // 打印所有读到的日志行 fmt.Println(fullLine) // 匹配包含"die"的聊天内容,不区分大小写 if strings.Contains(strings.ToLower(fullLine), "die") { fmt.Printf("*** 命中关键词: %s ***\n", fullLine) } } }
适配说明
- 编码适配:
utfutil.WINDOWS模式会自动识别EVE日志的UTF16LE BOM头,输出标准UTF8文本,完全不会出现乱码 - 性能表现:固定500ms轮询间隔,CPU占用可以忽略,完全满足游戏日志实时性要求
- 鲁棒性:内置半行缓存、文件存在性检测逻辑,不会因为游戏切日志、写入半行就出现解析错误
- 资源释放:
defer f.Close()会正确关闭底层文件句柄,不会出现资源泄漏
如果需要从文件末尾开始读、不加载历史日志内容,可以在打开文件后先循环读取直到第一次EOF,再进入正式的轮询逻辑即可,和现有代码完全兼容。
内容的提问来源于stack exchange,提问作者Gaudard
相关产品推荐
相关产品推荐

