How do you build a TCP server with net.Listen? Handle per-connection goroutines, read/write deadlines, and graceful shutdown of the listener.
Question 541HardGo 1.22 to 1.25
The pattern:
- Call
Acceptin a loop. - Start one goroutine per connection.
- Set deadlines on every read and write so that slow or idle clients cannot pin goroutines forever.
- To shut down, close the listener. This unblocks
Acceptwithnet.ErrClosed. Then wait for the handlers with aWaitGroup.
func serve(ctx context.Context, addr string) error {
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
stop := context.AfterFunc(ctx, func() { ln.Close() }) // Go 1.21
defer stop()
var wg sync.WaitGroup
for {
conn, err := ln.Accept()
if err != nil {
if errors.Is(err, net.ErrClosed) {
break
}
log.Printf("accept: %v", err)
time.Sleep(50 * time.Millisecond) // back off on EMFILE etc.
continue
}
wg.Add(1)
go func() { // conn is per-iteration in Go 1.22+
defer wg.Done()
handle(ctx, conn)
}()
}
wg.Wait() // Go 1.25 alternative: wg.Go(func() { handle(ctx, conn) })
return nil
}
func handle(ctx context.Context, c net.Conn) {
defer c.Close()
// Wake a Read blocked on an idle client when shutdown starts.
stop := context.AfterFunc(ctx, func() { c.SetDeadline(time.Now()) })
defer stop()
sc := bufio.NewScanner(c)
for ctx.Err() == nil {
c.SetReadDeadline(time.Now().Add(30 * time.Second))
if !sc.Scan() {
return // EOF, timeout or error
}
c.SetWriteDeadline(time.Now().Add(5 * time.Second))
if _, err := fmt.Fprintf(c, "echo: %s\n", sc.Text()); err != nil {
return
}
}
}
Gotchas: a deadline is an absolute time, not a timeout, so reset it for every operation. A timeout shows up as an error where errors.Is(err, os.ErrDeadlineExceeded) is true. Also, TCP is a byte stream, so you must frame messages yourself (lines, length prefixes) because one Read is not one message. Limit the number of concurrent connections, for example with a semaphore channel.
More on More Standard Library Essentials
- Q539How does regexp (RE2) differ from PCRE engines? Why doesn't Go support backreferences, and what are the performance guarantees?
- Q540How do you run external commands safely with os/exec? Explain CommandContext, capturing stdout/stderr, exit codes, and why shell injection is not the default risk.
- Q542Explain io/fs and fs.FS. How do you walk a directory with filepath.WalkDir and test file-system code with testing/fstest.MapFS?
- Q543What is os.Root (Go 1.24), and how does it prevent path-traversal attacks compared to filepath.Join plus filepath.Clean?
- Q544How do you parse CLI flags with the flag package? How do subcommands work, and why is flag.Parse in init() a bug?
- Q545Compare strconv with fmt for number/string conversion. What do strconv.ParseInt's base and bitSize arguments do, and how do you check for range errors?