TCP Server
net.Listen + Accept in a loop; handle each net.Conn in a goroutine and set deadlines - without them a stalled client holds a goroutine open for the process lifetime.
net.Listen binds a TCP address and returns a net.Listener. Calling Accept in a loop gives you a net.Conn for each client - a bidirectional stream you read from and write to. Handle each connection in a goroutine so the server can serve multiple clients concurrently.
The core loop: Listen, then Accept in a loop, then hand each net.Conn to a goroutine. The goroutine reads a line, echoes it back, and closes the connection.
package main
import (
"bufio"
"fmt"
"net"
"strings"
)
func handleConn(conn net.Conn) {
defer conn.Close()
scanner := bufio.NewScanner(conn)
for scanner.Scan() {
line := scanner.Text()
fmt.Fprintf(conn, "echo: %s\n", strings.ToUpper(line))
}
}
func main() {
ln, err := net.Listen("tcp", ":9000")
if err != nil {
panic(err)
}
defer ln.Close()
fmt.Println("tcp server listening on :9000")
for {
conn, err := ln.Accept()
if err != nil {
fmt.Println("accept error:", err)
continue
}
go handleConn(conn)
}
}Set deadlines on the connection to prevent slow or stalled clients from holding a goroutine forever. SetDeadline applies to both reads and writes; SetReadDeadline and SetWriteDeadline set them independently.
package main
import (
"bufio"
"fmt"
"net"
"time"
)
func handleConn(conn net.Conn) {
defer conn.Close()
// Extend the deadline on each successful read so active clients never time out
scanner := bufio.NewScanner(conn)
for scanner.Scan() {
conn.SetDeadline(time.Now().Add(30 * time.Second))
line := scanner.Text()
fmt.Fprintf(conn, "received: %s\n", line)
}
if err := scanner.Err(); err != nil {
fmt.Println("read error:", err)
}
}
func main() {
ln, err := net.Listen("tcp", ":9000")
if err != nil {
panic(err)
}
defer ln.Close()
for {
conn, err := ln.Accept()
if err != nil {
continue
}
// Set an initial deadline before the first read
conn.SetDeadline(time.Now().Add(30 * time.Second))
go handleConn(conn)
}
}In production
Raw TCP servers underpin database drivers, message queues, and custom protocols (Redis, Memcached, NATS). Always handle each accepted connection in a goroutine - a blocking handleConn on the accept goroutine would serialize all clients. Set SetReadDeadline and SetWriteDeadline to prevent a slow or stalled client from holding a goroutine and its stack indefinitely; without deadlines, a client that opens a connection and never sends data occupies a goroutine for the lifetime of the process. For production servers, bound the total number of concurrent connections with a semaphore (a buffered channel of capacity N) - unbounded goroutine creation from untrusted input is a denial-of-service vector. For graceful shutdown, stop calling Accept (close the listener) and wait for active handleConn goroutines to drain using a sync.WaitGroup. If your protocol is line-oriented, bufio.Scanner is convenient; for binary protocols with length-prefixed frames, use encoding/binary with io.ReadFull to read exactly N bytes at a time.
Enjoyed this? Get more essays on software craft delivered to your inbox.
Subscribe free