You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 17:50:25