Golang TCP server and client: step-by-step from basic to advanced

Step-by-step golang tcp server and golang tcp client: net.Listen and net.Dial, tcp server golang and tcp client golang patterns, deadlines and concurrent accepts with the net package.

Published

Updated

Read time 6 min read

Reviewed byDeepak Prasad

Golang TCP server and client: step-by-step from basic to advanced

Searches such as golang tcp server, golang tcp client, go tcp server, go tcp client, golang tcp connection, golang tcp, tcp server golang, tcp client golang, tcp golang, and go tcp all map to the same net workflow: net.Listen on the server, net.Dial on the client, then Read / Write on a net.Conn. TCP gives a reliable byte stream between two endpoints; your code still has to define message boundaries (line-based, length-prefix, JSON per line, and so on).

This page is a step-by-step path: a minimal blocking server and client, a concurrent server with one goroutine per connection, then a few advanced controls (read deadline, cleaner shutdown). The net package is the portable entry point; crypto/tls reuses the same Conn shape when you need TLS.

Examples assume Go on Linux and two terminals in the same machine. Pick a free port (here 127.0.0.1:9090) or change it if something else is bound.


Prerequisites

  1. Install Go (a recent stable release) and confirm go version prints a toolchain you expect.
  2. Create a working directory for this tutorial, for example mkdir tcpdemo && cd tcpdemo. You will keep server.go and client.go in that folder.
  3. Open two terminals in that directory: Terminal A runs the server, Terminal B runs the client after the server is listening.

Part 1 — Basic: one client, blocking server

Goal: prove golang tcp connection end-to-end with the smallest correct Read / Write loop.

Step 1: add server.go (listen, accept one connection, echo once)

Save as server.go:

go
package main

import (
	"fmt"
	"log"
	"net"
)

func main() {
	const addr = "127.0.0.1:9090"
	ln, err := net.Listen("tcp", addr)
	if err != nil {
		log.Fatal(err)
	}
	defer ln.Close()
	log.Println("listening on", addr)

	conn, err := ln.Accept()
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	buf := make([]byte, 4096)
	n, err := conn.Read(buf)
	if err != nil {
		log.Fatal(err)
	}
	msg := string(buf[:n])
	log.Println("from client:", msg)

	_, err = fmt.Fprintf(conn, "echo: %s\n", msg)
	if err != nil {
		log.Fatal(err)
	}
}

net.Listen binds 127.0.0.1:9090. The first (and only) Accept waits for one client. Read returns how many bytes arrived in n—use buf[:n], not the whole slice, when building a string.

Step 2: run the server (Terminal A)

bash
go run ./server.go

Leave it running until you see listening on 127.0.0.1:9090.

Step 3: add client.go (dial, write, read once)

Save as client.go:

go
package main

import (
	"fmt"
	"io"
	"log"
	"net"
)

func main() {
	const addr = "127.0.0.1:9090"
	conn, err := net.Dial("tcp", addr)
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	_, err = fmt.Fprintf(conn, "hello tcp\n")
	if err != nil {
		log.Fatal(err)
	}

	buf := make([]byte, 4096)
	n, err := conn.Read(buf)
	if err != nil && err != io.EOF {
		log.Fatal(err)
	}
	fmt.Print(string(buf[:n]))
}

net.Dial with "tcp" is the usual go tcp client / golang tcp client pattern; it returns net.Conn.

Step 4: run the client (Terminal B)

bash
go run ./client.go

You should see echo: hello tcp on the client. The server logs the incoming line. Stop the server with Ctrl+C when finished.

Step 5: sanity checks if something fails

  • bind: address already in use: another process uses 9090—change the port in both files or stop the other program. On Linux services, the same class of error often appears as cannot assign requested address when the bind IP or port is wrong.
  • connection refused: the server is not listening yet, or the address/port does not match.

Part 2 — Intermediate: golang tcp server with one goroutine per client

Goal: keep Accept in a loop and do not block the listener while handling a slow client. Spawn a handler per connection (see goroutines); use defer on each conn.Close() in the handler.

Step 1: replace server.go with a concurrent echo server

go
package main

import (
	"fmt"
	"log"
	"net"
)

func main() {
	const addr = "127.0.0.1:9090"
	ln, err := net.Listen("tcp", addr)
	if err != nil {
		log.Fatal(err)
	}
	defer ln.Close()
	log.Println("listening on", addr)

	for {
		conn, err := ln.Accept()
		if err != nil {
			log.Println("accept:", err)
			return
		}
		go handle(conn)
	}
}

func handle(conn net.Conn) {
	defer conn.Close()
	buf := make([]byte, 4096)
	for {
		n, err := conn.Read(buf)
		if err != nil {
			return
		}
		if n == 0 {
			continue
		}
		_, err = fmt.Fprintf(conn, "echo: %s\n", string(buf[:n]))
		if err != nil {
			return
		}
	}
}

Step 2: run server (A), then client (B) multiple times

Run go run ./server.go again. Run go run ./client.go several times; each run gets its own connection. This matches typical tcp server golang behavior under light load.

Step 3: limitations you hit next

TCP is a stream: Read can return a chunk smaller than “one message,” and Write can be partial unless you loop or use helpers like io.Copy. For line protocols, bufio.NewScanner(conn) or bufio.NewReader is usually the next upgrade.


Part 3 — Advanced: read deadline and stopping Accept

These patterns improve golang tcp services that must not hang forever.

Read deadline (per-connection)

SetReadDeadline bounds how long Read may wait. After the deadline, Read typically returns a net.Error with Timeout() true so you can retry, close, or log.

This self-contained example uses net.Pipe so you can go run one file without a second terminal: the writer sleeps longer than the read deadline, so Read times out.

go
package main

import (
	"errors"
	"log"
	"net"
	"time"
)

func main() {
	c1, c2 := net.Pipe()
	defer c1.Close()
	defer c2.Close()

	go func() {
		time.Sleep(200 * time.Millisecond)
		_, _ = c2.Write([]byte("late"))
	}()

	_ = c1.SetReadDeadline(time.Now().Add(50 * time.Millisecond))
	buf := make([]byte, 16)
	_, err := c1.Read(buf)
	var ne net.Error
	if errors.As(err, &ne) && ne.Timeout() {
		log.Println("read timed out as expected")
		return
	}
	log.Println("read:", err)
}

In a real server, call SetReadDeadline (or SetDeadline) on each conn accepted from your listener, usually resetting the deadline after each successful Read.

Stop listening (shutdown path)

Calling Close() on the net.Listener makes Accept return an error, which ends the for loop—useful when another goroutine signals shutdown. Pair that with a sync.WaitGroup if handlers must finish; the usual sequence is close listener → wait for in-flight handlers → exit.


Summary

You can stand up a golang tcp server with net.Listen("tcp", addr), Accept, and Read / Write on net.Conn, and a golang tcp client with net.Dial("tcp", addr). Use the byte count from Read when turning buffers into string. For real traffic, move from “one Read is one message” to framing, bufio, or streaming io helpers, and add per-connection goroutines, deadlines, and listener close as you grow. That covers the intent behind go tcp, golang tcp connection, and tcp golang searches in code-shaped terms.


References


Frequently Asked Questions

1. What is the difference between net.Dial and net.DialTCP?

Dial("tcp", hostport) returns net.Conn and is enough for most clients. DialTCP returns *net.TCPConn when you need TCP-specific knobs like SetLinger.

2. Why use 127.0.0.1 instead of localhost for examples?

localhost can resolve to IPv6 ::1 first; pinning 127.0.0.1 avoids mismatch when the server listens only on IPv4.

3. Why is my server string full of NUL characters?

Do not use the whole buffer after Read; slice with the returned length n: string(buf[:n]).

4. Can one Read call return a full message?

Not guaranteed; TCP delivers a byte stream. Loop Read until you have a frame, use bufio, or net.Conn deadlines with a protocol.
Tuan Nguyen

Data Scientist

Proficient in Golang, Python, Java, MongoDB, Selenium, Spring Boot, Kubernetes, Scrapy, API development, Docker, Data Scraping, PrimeFaces, Linux, Data Structures, and Data Mining. With expertise …