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
- Install Go (a recent stable release) and confirm
go versionprints a toolchain you expect. - Create a working directory for this tutorial, for example
mkdir tcpdemo && cd tcpdemo. You will keepserver.goandclient.goin that folder. - 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:
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)
go run ./server.goLeave 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:
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)
go run ./client.goYou 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 uses9090—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
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.
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.

