A straightforward way to understand TCP communication in Go is to build a tiny chat program: one server, multiple clients, and simple message broadcasting between them.
In this example, each client connects to the server over TCP, sends a username first, and then starts exchanging messages. The server keeps track of connected clients, forwards join and leave notifications, and broadcasts chat messages to everyone except the sender.
The interaction looks roughly like this:

Server side
The server listens on 127.0.0.1:9220, accepts incoming TCP connections, stores each client connection in a slice, and starts a goroutine for every connected client.
The basic flow is:
- create a TCP listener
- accept client connections in a loop
- save each connection
- read the first message as the user's name
- send a welcome message back to that client
- notify other clients that someone has come online
- keep reading messages and broadcast them to the rest of the group
- remove the client when the connection is closed
The code below keeps that logic minimal and direct:
package main
import (
"fmt"
"log"
"net"
"os"
)
const (
host = "127.0.0.1"
port = "9220"
)
var (
// 存放所有连接的客户端
clients []net.Conn
// 读取客户端的消息体
data = make([]byte, 1024)
)
func main() {
// 创建一个 tcp 服务端连接
listener, err := net.Listen("tcp", host+":"+port)
if err != nil {
log.Println(err)
os.Exit(0)
}
defer listener.Close()
log.Println("tcp server start")
// 处理客户端的连接
for {
// 客户端初次连接
conn, err := listener.Accept()
if err != nil {
log.Print(err)
os.Exit(1)
}
// 保存客户端的连接
clients = append(clients, conn)
// todo for 嵌套 for 需要 协程???
go func(conn net.Conn) {
// 当前连接的客户端地址
ClientRemoteAddr := conn.RemoteAddr().String()
log.Println("客户端连接来自:", ClientRemoteAddr)
// 读取客户端的数据包
_, errRead := conn.Read(data)
if errRead != nil {
fmt.Printf("Client %v quit.\n", conn.RemoteAddr())
conn.Close()
disconnect(conn, conn.RemoteAddr().String())
return
}
name := string(data) + "(" + ClientRemoteAddr + ")"
conn.Write([]byte("欢迎你," + name))
notify(conn, name+" 上线了")
log.Println("客户端:" + name + "上线了")
// 处理消息交互
for {
_, err := conn.Read(data)
if err != nil {
fmt.Printf("Client %s quit.\n", name)
conn.Close()
disconnect(conn, name)
return
}
res := string(data)
sprdMsg := name + ":" + res
fmt.Println(sprdMsg)
res = "我:" + res
conn.Write([]byte(res))
notify(conn, sprdMsg)
}
}(conn)
}
}
// 通知除自己的其他所有客户端
func notify(conn net.Conn, msg string) {
for _, con := range clients {
if con.RemoteAddr() != conn.RemoteAddr() {
con.Write([]byte(msg))
}
}
}
// 离开通知所有其他在线的客户端
func disconnect(conn net.Conn, name string) {
for index, con := range clients {
if con.RemoteAddr() == conn.RemoteAddr() {
disMsg := name + " 离开了."
fmt.Println(disMsg)
clients = append(clients[:index], clients[index+1:]...)
notify(conn, disMsg)
}
}
}
A few details are worth noticing here.
Each client runs in its own goroutine
After Accept() returns a new connection, the server immediately launches a goroutine to handle that client. Without this, one connected client would block the handling of others.
The first packet is treated as the username
Once connected, the client sends its name first. The server combines that with the remote address and uses the result as the client's displayed identity.
Messages are echoed locally and broadcast to others
When a client sends a chat message, the server does two things:
- sends
我:back to the same client - forwards
name:messageto all other connected clients
Disconnect handling removes the client from the list
If Read() fails, the connection is closed and the server calls disconnect() so the client is removed from clients and everyone else gets a leave notification.
Client side
The client is even simpler. It connects to the server, reads a username from terminal input, sends that username once, and then enters chat mode.
One goroutine is used to receive messages from the server continuously, while the main flow keeps reading input from standard input and sending it out.
package main
import (
"bufio"
"fmt"
"net"
"os"
"strings"
)
var (
// 定义写入跟读取服务端的消息体
writeStr, readStr = make([]byte, 1024), make([]byte, 1024)
)
func main() {
var (
host = "127.0.0.1"
port = "9220"
// 获取用户在终端的输入
reader = bufio.NewReader(os.Stdin)
)
conn, err := net.Dial("tcp", host+":"+port)
if err != nil {
fmt.Println("Error connecting:", err)
os.Exit(1)
}
defer conn.Close()
fmt.Println("连接成功 Connecting to " + "127.0.0.1:9220")
fmt.Printf("输入用户名进行聊天: \n")
fmt.Scanf("%s", &writeStr)
in, errWrite := conn.Write(writeStr)
if errWrite != nil {
fmt.Printf("Error when send to server: %d\n", in)
os.Exit(0)
}
// 一定得用协程!!! 获取服务端的消息
go read(conn)
// todo main 下不能有两个或以上 for{} !!!
// 将客户端的输入通知服务端
readLineWrite(reader, conn)
}
func readLineWrite(reader *bufio.Reader, conn net.Conn) {
for {
//fmt.Printf(":")
writeStr, _, _ = reader.ReadLine()
if string(writeStr) == "quit" {
fmt.Println("Communication terminated.")
os.Exit(1)
}
in, err := conn.Write(writeStr)
if err != nil {
fmt.Printf("Error when send to server: %d\n", in)
os.Exit(0)
}
}
}
func read(conn net.Conn) {
for {
// 这里需要捕捉错误,不然服务端连接关闭会进入死循环
_, err := conn.Read(readStr)
if err != nil {
fmt.Printf("Client quit.\n")
conn.Close()
return
}
msg := string(readStr)
msg = strings.Replace(msg, "\r\n", "", -1)
fmt.Println(msg)
}
}
How the client works in practice
After the TCP connection is established, the program prompts for a username. That value is sent to the server immediately.
From there, two things happen at the same time:
readLineWrite()keeps reading terminal input and sends every line to the serverread()keeps listening for server responses and prints them to the screen
This split is the reason a goroutine is necessary on the client side as well. If receiving and sending were handled sequentially in a single path, one side would block the other.
Typing quit exits the client process directly.
What this example demonstrates
Although the implementation is small, it already covers the essential pieces of TCP communication in Go:
- starting a TCP server with
net.Listen - accepting connections with
Accept - connecting from the client with
net.Dial - exchanging data through
ReadandWrite - handling multiple clients concurrently with goroutines
- broadcasting messages to all connected peers except the sender
As a basic multi-client chat demo, it is enough to show how server and client communication fits together over TCP in Go.