Golang聊天应用问题:Sender函数用Goroutine导致客户端立即退出
Golang聊天应用协程问题排查与解决
问题
用Golang开发聊天应用时遇到如下问题:将发送逻辑的for循环封装到Sender()函数中并通过Goroutine调用,客户端会立即关闭;但将该循环直接置于main函数时,程序可正常运行。
客户端代码
package main import ( "fmt" "net" "os" "os/signal" "syscall" ) const ( SERVER_HOST = "localhost" SERVER_PORT = "9988" SERVER_TYPE = "tcp" ) var connection net.Conn var err error func main() { SetupCloseHandler() //establish connection connection, err = net.Dial(SERVER_TYPE, SERVER_HOST+":"+SERVER_PORT) if err != nil { panic(err) } //receive some data go Receiver() // go Sender() for { var input string fmt.Print("Enter text: ") fmt.Scan(&input) _, err = connection.Write([]byte(input)) if err != nil { panic(err) } } } // func Sender() { // for { // var input string // fmt.Print("Enter text: ") // fmt.Scan(&input) // _, err = connection.Write([]byte(input)) // if err != nil { // panic(err) // } // } // } func Receiver() { for { buffer := make([]byte, 1024) mLen, err := connection.Read(buffer) if err != nil { panic(err) } fmt.Println("Received: ", string(buffer[:mLen])) } } func SetupCloseHandler() { c := make(chan os.Signal, 2) signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { <-c fmt.Println("\r- Ctrl+C pressed in Terminal") os.Exit(0) }() }
服务端代码
package main import ( "fmt" "net" "os" "github.com/google/uuid" ) const ( SERVER_HOST = "localhost" SERVER_PORT = "9988" SERVER_TYPE = "tcp" ) var clientMap = make(map[string]net.Conn) func main() { fmt.Println("Server Running...") server, err := net.Listen(SERVER_TYPE, SERVER_HOST+":"+SERVER_PORT) if err != nil { fmt.Println("Error listening:", err.Error()) os.Exit(1) } defer server.Close() fmt.Println("Listening on " + SERVER_HOST + ":" + SERVER_PORT) fmt.Println("Waiting for client...") for { connection, err := server.Accept() id := uuid.New().String() clientMap[id] = connection if err != nil { fmt.Println("Error accepting: ", err.Error()) os.Exit(1) } fmt.Printf("client %s connected\n", id) go processClient(connection, id) } } func processClient(connection net.Conn, id string) { defer connection.Close() for { buffer := make([]byte, 1024) mLen, err := connection.Read(buffer) if err != nil { fmt.Println("Error reading:", err.Error()) delete(clientMap, id) connection.Close() return } fmt.Println("Received: ", string(buffer[:mLen])) for k, c := range clientMap { if k != id { c.Write(buffer[:mLen]) } } } }
原因分析
Golang程序的主协程(执行main函数的协程)退出时,整个进程会直接终止,所有其他协程都会被强制停止。当你把发送逻辑放到Sender()协程中,main函数在启动Receiver()和Sender()协程后,没有任何阻塞逻辑,会直接执行完毕,导致进程退出,客户端连接自然关闭。而将循环直接放在main函数里时,main函数会一直卡在循环中,不会退出,其他协程就能正常运行。
解决方案
核心是让main函数保持运行,不提前退出,以下是两种可行方式:
方式一:用select{}阻塞主协程
修改main函数,在启动协程后添加select{},它会让主协程一直阻塞,直到收到中断信号(你的SetupCloseHandler已经处理了Ctrl+C等终止信号):
func main() { SetupCloseHandler() //establish connection connection, err = net.Dial(SERVER_TYPE, SERVER_HOST+":"+SERVER_PORT) if err != nil { panic(err) } //receive some data go Receiver() go Sender() // 阻塞主协程,防止进程提前退出 select {} }
方式二:使用sync.WaitGroup(适合需等待协程完成的场景)
如果后续需要等待特定协程完成后再退出,可以用sync.WaitGroup,但由于本场景中Sender()和Receiver()都是无限循环,第一种方式更简洁适用。
内容的提问来源于stack exchange,提问作者vvv
相关产品推荐
相关产品推荐

