質問

// ws/ws.go
package ws

import (
"context"
"encoding/json"
"live-command-middleware/chrome"
"live-command-middleware/gopool"
"live-command-middleware/instruction"
"live-command-middleware/mylog/wlog"
"live-command-middleware/setting"
"live-command-middleware/trigger"
"live-command-middleware/websocket/messenger"
"time"

"github.com/gorilla/websocket"
hook "github.com/robotn/gohook"
)

const wsUrl = "ws://localhost:5000/ws"

var StopFlag = false // 停止标志,五秒内不接收Stop消息(因为stop之后前端会又广播一个)

var (
WsClient *wsClient
)

type wsClient struct {
conn *websocket.Conn
url string
}

type WsMessage = messenger.WsMessage // 引用 messenger 中的 WsMessage 类型

// NewWsClient 创建一个新的 WebSocket 客户端实例
func NewWsClient() error {
var err error
client := &wsClient{
url: wsUrl,
}
err = client.connect()
if err == nil {
WsClient = client
}
return err
}

// 连接到 WebSocket 服务器
func (c *wsClient) connect() error {
var dialer *websocket.Dialer
conn, _, err := dialer.Dial(c.url, nil)
if err != nil {
return err
}
c.conn = conn

// 初始化 messenger
messenger.InitMessenger(c.conn)

// 开始监听服务器消息
go c.listen()
return nil
}

func isUnexpectedEOF(err error) bool {
return websocket.IsCloseError(err, websocket.CloseAbnormalClosure)
}

// 监听服务器发送的消息
func (c *wsClient) listen() {
for {
_, message, err := c.conn.ReadMessage()
if err != nil {
if isUnexpectedEOF(err) {
wlog.Infof("智能中控客户端已关闭,正在尝试重新连接...")
// 在此处理特定的错误, 例如尝试重新连接
} else {
wlog.Errorf("读取消息出错:", err)
}
// 尝试重连
for {
err := c.connect()
if err == nil {
wlog.Infof("重连成功")
break
}
wlog.Errorf("重连失败,稍后重试:", err)
time.Sleep(5 * time.Second)
}
continue
}
// 处理接收到的消息
c.handleMessage(message)
}
}

// 处理接收到的消息
func (c *wsClient) handleMessage(message []byte) {
var msg WsMessage
err := json.Unmarshal(message, &msg)
if err != nil {
wlog.Errorf("JSON 解析错误:", err)
return
}

// 检查消息是否为自己发送的
if isSelfMessage(msg) {
return
}

switch msg.Type {
case "start":
wlog.Infod("收到启动指令: ", msg.Data, true)
c.handleStart(msg.Data, false)
case "reStart":
wlog.Infod("收到重启指令: ", msg.Data, true)
c.handleRestart(msg.Data)
case "stop":
if StopFlag {
return
}
wlog.Infof("收到停止指令")
c.HandleStop()
case "heartBeat":
c.handleHeartBeat(msg.Data)
default:
wlog.Warnf("未知的消息: %v", msg)
}
}

// 辅助函数,判断是否为自己发送的消息
func isSelfMessage(msg WsMessage) bool {
if dataMap, ok := msg.Data.(map[string]interface{}); ok {
if fromClient, exists := dataMap["fromClient"].(bool); exists && fromClient {
return true
}
}
return false
}

// 处理 "start" 消息
func (c *wsClient) handleStart(data interface{}, isRestart bool) {
if !isRestart {
chrome.Init()
}
if msgMap, ok := data.(map[string]interface{}); ok {
platform, _ := msgMap["platform"].(string)
token, _ := msgMap["token"].(string)
setting.UserToken = token

if !isRestart {
instruction.SetCurrentPlatformAndOpen(platform)
}

// 这里暂时还有问题,浏览器如果刚打开就关不了会的不到session,还没有监听就被关闭了,前端无法感知
go instruction.MonitorTargetClosed(chrome.Ctx)

if !setting.LoadRemoteConfig() || !setting.LoadRemoteKeyboardSetting() {
wlog.Errorf("加载远程配置失败,启动失败")
return
}

chrome.ChildCtx, chrome.ChildCancelCtx = context.WithCancel(chrome.Ctx)

go trigger.ListenKeyBoard()
} else {
wlog.Errorf("类型断言失败,无法转换为 map[string]interface{},当前data为: %v", data)
}
}

// 处理 "restart" 消息
func (c *wsClient) handleRestart(data interface{}) {

instruction.GracefulShutdownAll()
chrome.ChildCancelCtx()
hook.End()

time.Sleep(2 * time.Second)
// 保证其他goroutine尽可能退出
wlog.Infof("正在重启,等待2秒...")

// 如果当前浏览器已经被关闭了,则直接打开浏览器
c.handleStart(data, true)
}

// 处理 "stop" 消息
func (c *wsClient) HandleStop() {
if chrome.Ctx == nil || chrome.Ctx.Err() != nil {
return
}
if StopFlag {
return
}
StopFlag = true
// Note: 这里Stop之后服务端还会广播一个STOP消息,加一个延时等待,5秒内不接收Stop消息
go func() {
time.Sleep(5 * time.Second)
StopFlag = false
}()

instruction.GracefulShutdownAll()
chrome.CancelCtx()

// 通知前端调整为关闭按钮状态
messenger.SendMessage(
WsMessage{
Type: "stop",
Data: "null",
},
)
hook.End()

wlog.Infof("chrome被客户端关闭")
wlog.Infof("当前Goroutine数量: %d", gopool.GlobalPool.Running())
go func() {
time.Sleep(1 * time.Second)
if gopool.GlobalPool.Running() != 0 {
// 有一个是监听关闭状态的goroutine,在allocator.go中
wlog.Errorf("当前Gopool中Goroutine数量异常,请检查")
}
}()
}

// 处理 "heartBeat" 消息
func (c *wsClient) handleHeartBeat(data interface{}) {
wlog.Infof("收到心跳消息: %s", data.(string))
}

// 开始定时发送心跳
func (c *wsClient) StartHeartBeat() {
ticker := time.NewTicker(3 * time.Second)
go func() {
for range ticker.C {
err := messenger.SendHeartBeat("ping")
if err != nil {
wlog.Errorf("发送Ping消息出错:", err)
}
data := map[string]interface{}{
"instruction": "heartbeat",
"operateNo": 1,
"action": "on",
}
err = messenger.SendKeepAlive(data)
if err != nil {
wlog.Errorf("发送键盘保活状态失败:", err)
}
}
}()
}


上面是我跟前端进行通信的websocket连接的代码,
var startCmd = &cobra.Command{
Use: "start",
Short: "开始服务",
Long: `会开始监听操作`,
Run: func(cmd *cobra.Command, args []string) {
wlog.Info("程序开始")

err := ws.NewWsClient()
if err != nil {
// 重试连接
reTryNum := 100
for i := 0; i < reTryNum; i++ {
time.Sleep(1 * time.Second)
wlog.Infof("第 %d 次重试连接", i+1)
err = ws.NewWsClient()
if err == nil {
break
}
}
wlog.Errorf("ws连接失败 Error: %v", err)
os.Exit(10002)
}
ws.WsClient.StartHeartBeat()
wlog.Infof("ws连接成功,等待接受信号...")

select {}
},
}

func init() {
rootCmd.AddCommand(startCmd)
}

现在遇到了很奇怪的事情,就是我在vscode以调试模式启动,是一切正常的,但是我打包成exe之后,当我在进行了一系列的通信之后,突然程序就会死掉,然后日志也不会输出,我不知道是哪里错了,我应该怎么排查?用中文给我回答

思考

问题分析: 在将代码打包为exe并运行后,程序在websocket通信后突然停止工作且没有日志输出,可能是exe的异常退出或日志引发的问题,需深入调查。

调试问题的可能原因

正在分析 Go 代码在调试模式下运行正常,然而编译成 exe 后却错误的可能原因。包括恐慌或错误导致程序退出,日志未及时刷新,或者代码差异等。

探讨环境影响

当调试模式运行时,环境因素可能会有所不同。

追踪错误位置

在代码中添加日志语句,确保捕获并记录所有 panic 错误,使用调试符号或详细模式构建 exe,并检查 os.Exit() 调用,采用日志框架。


调整日志记录

我正在优化日志记录,确保捕获未处理的 panic 错误。使用日志框架,在退出前刷新日志,这对调试非常有帮助。

回答

看起来你遇到了一个在调试模式下程序正常运行,但打包成可执行文件后,运行一段时间后程序意外退出且没有日志输出的问题。这种情况通常比较棘手,但我们可以一步一步地进行排查。以下是一些可能的原因和相应的解决方案:

  1. 程序发生了未捕获的 panic

    • 原因:在 Go 中,如果发生了未捕获的 panic,程序会直接崩溃退出。如果没有相应的日志或错误处理,可能看不到任何错误信息。
    • 解决方案
      • main 函数或者 goroutine 的入口处使用 deferrecover 捕获 panic。例如:

        func main() { defer func() { if r := recover(); r != nil { wlog.Errorf("捕获到 panic:%v", r) } }() // 你的程序逻辑 }
      • 对于每个新的 goroutine,都建议使用类似的方式捕获 panic,以防止 goroutine 中的 panic 导致程序崩溃。

  2. 日志未正确刷新到输出

    • 原因:程序可能在日志还未写入文件或控制台之前就退出了,导致你看不到最后的错误信息。
    • 解决方案
      • 确保你的日志库在程序退出前刷新了缓冲区。检查你的日志库 wlog 是否提供了类似 Flush 的方法,在程序退出前调用。
      • 使用 os.Stderros.Stdout 直接打印关键的错误信息,以确保能够看到。
  3. 可能的 os.Exit 调用

    • 原因:程序中某些地方可能调用了 os.Exit,导致程序直接退出而不执行 defer 语句。
    • 解决方案
      • 全局搜索 os.Exit 的调用,确保只有在必要的地方才使用。
      • 如果必须调用 os.Exit,确保在调用之前已经处理了所有需要的清理工作,如日志刷新。
  4. Windows 系统特性

    • 原因:在 Windows 上运行控制台程序,如果程序异常退出,控制台窗口可能会立即关闭,导致你看不到任何错误信息。
    • 解决方案
      • 从命令行(cmd 或 PowerShell)手动运行你的可执行文件,这样即使程序退出,控制台窗口也会保留,你可以看到输出的日志和错误信息。

      • 或者在程序末尾添加一个等待输入的代码,仅用于调试目的,例如:

        fmt.Println("按回车键退出...") fmt.Scanln()
  5. 编译器优化或环境差异

    • 原因:在调试模式和发布模式下,编译器的优化级别可能不同,或者环境变量、依赖库版本有差异。
    • 解决方案
      • 在构建可执行文件时,添加调试信息,避免过度的编译器优化。例如,使用 go build -gcflags "all=-N -l"
      • 确保运行环境与开发环境一致,包括 Go 版本、依赖库版本等。
  6. Goroutine 泄漏或死锁

    • 原因:某些 Goroutine 可能意外退出,或者出现死锁,导致程序无法正常运行。
    • 解决方案
      • 使用 pprof 等工具分析 Goroutine 的运行状态。
      • 在关键的 Goroutine 启动和退出时添加日志,确认它们是否正常运行。
  7. 网络连接错误

    • 原因:网络连接在某些情况下可能会中断,导致程序未正确处理错误并退出。
    • 解决方案
      • 检查所有网络操作的错误处理,确保任何错误都被正确捕获和处理。
      • 在网络连接断开时,程序是否有重连机制?如果有,是否可能因为重连失败而导致程序退出?
  8. 使用了特定的系统调用或第三方库

    • 原因:某些系统调用或第三方库在不同的运行环境下行为不一致。
    • 解决方案
      • 检查代码中是否有调用特定于操作系统的函数,或者使用了可能不稳定的第三方库。
      • 对这些部分添加更多的日志,或者在不同的环境下进行测试。
  9. 信号处理

    • 原因:程序可能接收到了操作系统发送的信号(如 SIGINT、SIGTERM),导致退出。
    • 解决方案
      • 在程序中添加对信号的捕获和处理,确认程序是否因为信号而退出。
      • 使用 os/signal 包来捕获并处理系统信号。
  10. 与前端通信的问题

    • 原因:如果前端发送了某些特殊的消息,导致程序在处理时发生错误。
    • 解决方案
      • handleMessage 等处理函数中添加更多的日志,记录每次接收到的消息内容和处理结果。
      • 检查是否有某些特定的消息会触发程序的崩溃。

建议的调试步骤

  1. 增加日志覆盖面:在可能出错的地方增加详细的日志,特别是在程序退出前添加日志,确认程序是从哪里退出的。

  2. 使用命令行运行程序:从命令行启动你的可执行文件,这样可以直接看到程序的标准输出和错误输出。

  3. 捕获所有的 panic:确保在所有的入口函数和 goroutine 中捕获 panic,并将错误信息记录下来。

  4. 检查所有的错误处理:仔细检查代码中所有可能返回错误的地方,确保错误被正确处理,而不是被忽略。

  5. 使用调试构建:在构建可执行文件时,包含调试信息,以便获得更详细的错误报告。

    go build -o your_program -gcflags "all=-N -l"
  6. 监控程序运行状态:使用一些监控工具或打印程序的运行状态,例如当前的 Goroutine 数量、内存使用情况等。

  7. 简化问题:如果可能,尝试简化代码,去掉一些不相关的部分,看看问题是否依然存在。这有助于定位问题的根源。

示例:在 goroutine 中捕获 panic

func safeGo(f func()) { go func() { defer func() { if r := recover(); r != nil { wlog.Errorf("捕获到 goroutine panic:%v", r) } }() f() }() } // 使用示例 safeGo(func() { // 你的 goroutine 逻辑 })

总结

  • 问题可能是由于未捕获的 panic 导致的,确保所有的 panic 都被捕获和记录。
  • 确保日志在程序退出前被正确地刷新到输出。
  • 从命令行运行程序,观察是否有错误输出。
  • 检查程序中是否有任何直接导致退出的代码,如 os.Exit
  • 仔细检查网络通信和 goroutine 的错误处理。

通过上述方法,相信你能够找到问题的原因。如果仍然无法解决,建议你提供更多的日志信息或简化后的代码,以便进一步协助。

質問

所以是不是每次都 go build -gcflags "all=-N -l" 比直接go build好?

思考

构建选项的探讨
用户询问是否始终使用 go build -gcflags "all=-N -l" 选项,而非单纯的 go build,以避免调试模式的中断。

明确 gcbflags

我正在研究 Go 编译器的 -gcflags "all=-N -l" 选项,了解其禁用优化和内联的影响。

考虑构建设置

在调试中禁用优化和内联可以更接近源代码,而在生产构建中则应启用优化以提升性能。需谨慎选择。

评估性能
使用 "-gcflags "all=-N -l"" 可以更接近生成的代码和源代码,便于调试。但在生产环境中应避免,因而不必始终使用这些标志。

回答

不是的,不建议每次都使用 go build -gcflags "all=-N -l" 来替代直接的 go build。让我详细解释一下原因。

-gcflags "all=-N -l" 的作用

  • -N:禁止编译器的优化。
  • -l:禁止函数内联。

当你使用 go build -gcflags "all=-N -l" 时,你告诉 Go 编译器对所有的包都禁用优化和内联。这在调试时非常有用,因为:

  • 更容易调试:禁用优化后,生成的机器代码更直接地对应源代码,调试器(如 Delve)更容易跟踪代码执行路径。
  • 堆栈跟踪更清晰:禁用内联后,函数调用的层级关系保持不变,堆栈跟踪的信息更加准确。

为什么不应该在生产环境中使用

  • 性能影响:编译器优化和内联可以显著提高程序的运行性能。禁用这些功能会导致程序运行变慢,可能无法满足生产环境的性能需求。
  • 二进制文件体积:禁用优化可能会导致生成的二进制文件更大,占用更多的磁盘空间和内存。

何时使用 -gcflags "all=-N -l"

  • 调试阶段:当你需要深入调试程序,尤其是定位一些在优化后出现的问题时,可以使用这些标志。
  • 复现问题:如果你的程序在正常编译时出现了问题,而在调试模式下正常运行,使用这些标志可以帮助你确定问题是否与编译器优化有关。

你的问题的具体建议

  • 暂时使用 -gcflags "all=-N -l" 进行调试:由于你在打包成可执行文件后遇到了程序崩溃的问题,使用这些标志可以帮助你排查问题。
    • 目的:减少调试模式和可执行文件之间的差异,帮助你定位问题是否与编译器优化有关。
  • 找到问题后,恢复正常编译:一旦你定位并解决了程序崩溃的原因,应当恢复正常的编译方式,以确保程序在生产环境中的性能。
    • 确保程序稳定:在没有 -gcflags "all=-N -l" 的情况下再次测试程序,确认问题已经解决。
    • 性能测试:在正常编译下进行性能测试,确保程序满足性能要求。

可能的进一步措施

  • 优化代码而非禁用优化:如果发现问题是由于编译器优化引起的,可能需要检查代码中是否有未定义行为或依赖于特定的执行顺序。
  • 更新编译器或依赖库:有时编译器的 Bug 会导致优化问题,尝试更新 Go 版本或相关的依赖库。
  • 使用单独的调试构建:在开发和调试阶段使用调试构建,在生产环境中使用优化后的构建。这是常见的实践。

总结

  • 不要在生产环境中禁用编译器优化:这会对程序性能产生负面影响。
  • 仅在需要调试时使用 -gcflags "all=-N -l":帮助你定位和解决问题。
  • 解决问题后,恢复正常编译:确保程序在最佳性能下运行。

希望这些信息能帮助你理解为什么不应该总是使用 -gcflags "all=-N -l",以及如何更好地排查和解决你遇到的问题。如果你有更多疑问,欢迎继续提问!

このQ&Aを共有