Build A Minimal TCP Server In Go

TCP, or Transmission Control Protocol, is a connection-oriented protocol, which means it establishes a dedicated end-to-end connection before any data is exchanged.

TCP includes both a connection setup and a teardown process, ensuring that both parties are ready to communicate and can properly conclude the conversation.

One of TCP’s key advantages is its reliable message delivery. It operates as a low-level communication protocol where performance is critical. Unlike HTTP, TCP has less overhead since it eliminates the need for HTTP headers.

This reduction in protocol overhead leads to faster processing times and lower latency, which becomes especially significant when systems handle millions of requests.

Unlike HTTP/1.1, where a new connection is established for every request, TCP allows for a persistent connection between microservices, eliminating repeated handshakes. This makes TCP highly efficient for service-to-service communication.

TCP connections are particularly useful in trading applications that require constant real-time price updates. They can also be leveraged in multiplayer gaming applications and IoT devices, where low latency and guaranteed message delivery are essential.

In this article, we will explore how to set up a TCP server in Go, send messages, and receive responses.

Setting Up A TCP Server

We will start by setting up a project. In this we will first create a module with name tcpserver and create a file main.go in the same directory:

$ mkdir learntcp
$ cd learntcp
$ go mod init tcpserver
$ touch main.go

We will write all the server related code in the main.go file.

Now, since TCP establishes a dedicated connection between two services, once the connection is established, we must continuously listen for incoming messages to process them.

Below is the TCP server code, let's put it in the main.go file.

package main

import (
    "log"
    "net"
)

func handleConnection(conn net.Conn) {
    defer func() {
        conn.Close()
        log.Printf("connection closed: %s", conn.RemoteAddr().String())
    }()
    
    // we will write the message consumption logic in a while

}

func main() {
    address := "localhost:6000"

    // create TCP listener
    listener, err := net.Listen("tcp", address)
    if err != nil {
        log.Fatalf("error creating listener: %v", err)
    }

    defer listener.Close()

    log.Printf("server listening on %s", address)

    // accept connections in loop
    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Printf("error accepting connection: %v", err)
            continue
        }
        go handleConnection(conn)
    }
}

In the code above, we have set up a server, and it is listening to connections on port 6000, allowing clients to connect to our server on the same port. Our main function runs an infinite loop to continuously listen for incoming connections. Each new connection is passed to the handleConnection goroutine, which processes incoming messages.

Now, let's complete the handleConnection function. This function will receive incoming messages, print them to the terminal, and send a response that the client can consume.

Additionally, we will handle scenarios where the client remains idle for more than 60 seconds, in which case the TCP connection will be disconnected. We will also handle cases where the client disconnects on its own.

Below is the updated handleConnection function, which prints incoming messages and returns a response. If the client sends the message "bye", the function will close the connection and terminate.

import (
	"bufio"  // new addition
	"errors" // new addition
	"fmt"    // new addition
	"io"     // new addition
	"log"
	"net"
	"strings" // new addition
	"time"    // new addition
)

func handleConnection(conn net.Conn) {
	defer func() {
		conn.Close()
		log.Printf("connection closed: %s", conn.RemoteAddr().String())
	}()

	// print client info
	log.Printf("client connected: %s", conn.RemoteAddr().String())

	reader := bufio.NewReader(conn)

	for {
		// timeout for stale connections
		// we are putting it inside the loop so that the
		// timeout resets after every message received
		timeout := time.Second * 15

		// wait time for stale connections
		err := conn.SetDeadline(time.Now().Add(timeout))
		if err != nil {
			log.Printf("error setting connection deadlines: %v", err)
		}

		// reading the incoming data until a newline character ('\n') is encountered.
        // note: '\n' is a byte in Go, whereas "\n" would be a string.
		message, err := reader.ReadString('\n') 
		if err != nil {
			var netErr net.Error
			if errors.As(err, &netErr) && netErr.Timeout() {
				log.Printf("conection timed out: %v", err)
			} else if errors.Is(err, io.EOF) {
				log.Printf("client closed connection")
			} else {
				log.Printf("error reading connection: %v", err)
			}
			return
		}

		message = strings.TrimSpace(message)
		fmt.Println(message)

		// simple response
		response := fmt.Sprintf("server received: %s\n", message)

		// sending a response back to the client
		_, err = conn.Write([]byte(response))
		if err != nil {
			log.Printf("error writing connection: %v", err)
			return
		}

		// if client sends "exit", close the connection
		if strings.ToLower(message) == "exit" {
			log.Printf("client requested to close the connection")
			return
		}
	}

}

Our server code is ready. Let's start the server:

$ go run main.go
2025/03/06 17:48:42 server listening on localhost:6000

We can see that the server has successfully started on port 6000. Now, let's try connecting to the TCP server and sending a message.

Connecting Via Netcat

Netcat (nc) is a command-line tool used for reading and writing data over network connections. We will connect to the TCP server using nc.

$ nc localhost 6000

As soon as we connect via nc, the server prints a message on the terminal displaying the client's IP address and port.

$ 2025/03/06 17:52:58 client connected: 127.0.0.1:61628

Let's try to send some messages via nc:

$ nc localhost 6000
hey
server received: hey
hello
server received: hello

As we send messages via nc, we can see that the server returns a response as well.

This was a very basic article on setting up a TCP server and connecting to it using nc. To keep it concise, I did not cover how two services communicate via TCP. In the next article, I will demonstrate how to establish TCP communication between two services.


My First Go Book

My First Go Book

Learn Go from scratch with hands-on examples! Whether you're a beginner or an experienced developer, this book makes mastering Go easy and fun.

🚀 Get it Now