Table of Contents
- Introduction
- Prerequisites
- Setting Up the Project
- Creating a TCP Server
- Implementing the Key-Value Store
- Handling Multiple Clients
- Adding Error Handling
- Conclusion
Introduction
In this tutorial, we will learn how to implement a simple key-value store over TCP in Go. We will build a server that can handle client requests to store, retrieve, and delete key-value pairs. By the end of this tutorial, you will have a basic understanding of TCP networking, concurrency, and working with key-value stores in Go.
Prerequisites
To follow along with this tutorial, you should have basic knowledge of the Go programming language. Familiarity with networking concepts and basic command-line usage is also beneficial.
Setting Up the Project
Before we start coding, we need to set up a new Go project. Open a terminal and create a new directory for the project:
mkdir key-value-store
cd key-value-store
Next, initialize a Go module:
go mod init github.com/your-username/key-value-store
Now, let’s create the main file for our project:
touch main.go
Open main.go
in a text editor and let’s begin implementing our key-value store over TCP.
Creating a TCP Server
First, we need to import the necessary packages for working with TCP connections and handling requests:
package main
import (
"bufio"
"fmt"
"log"
"net"
"strings"
)
Next, we’ll define a constant for the server’s address and port number:
const (
address = "localhost"
port = "1234"
)
Now, let’s create a function handleConnection
that will be responsible for handling client connections:
func handleConnection(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
writer := bufio.NewWriter(conn)
// TODO: Implement key-value store functionality
}
The handleConnection
function takes a net.Conn
object as a parameter, which represents a TCP connection. We wrap the connection’s input and output streams with a bufio.Reader
and bufio.Writer
for convenient reading and writing.
Implementing the Key-Value Store
Inside the handleConnection
function, we can now implement the key-value store functionality. Let’s add a map
to store our key-value pairs:
database := make(map[string]string)
We can then enter a loop to continuously read and process client requests:
for {
// Read client request
request, err := reader.ReadString('\n')
if err != nil {
log.Printf("Error reading request from client: %v", err)
return
}
// Process request
parts := strings.Split(request, " ")
command := strings.TrimSpace(parts[0])
switch command {
case "GET":
// TODO: Implement GET command
case "SET":
// TODO: Implement SET command
case "DEL":
// TODO: Implement DEL command
default:
// Unknown command
writer.WriteString("Unknown command\n")
writer.Flush()
}
}
In this example, we split the client request into its parts and extract the command. We then use a switch
statement to handle different commands. For now, let’s leave the implementation of the GET
, SET
, and DEL
commands as TODOs.
Handling Multiple Clients
To support multiple clients concurrently, we can modify our code to use goroutines. In the main
function, we can create a listener for incoming connections:
func main() {
listener, err := net.Listen("tcp", address+":"+port)
if err != nil {
log.Fatalf("Failed to start server: %v", err)
}
log.Printf("Server listening on %s:%s", address, port)
for {
conn, err := listener.Accept()
if err != nil {
log.Printf("Error accepting connection: %v", err)
continue
}
go handleConnection(conn)
}
}
By using go handleConnection(conn)
inside a goroutine, each incoming connection will be handled concurrently.
Adding Error Handling
It’s important to handle errors properly to provide useful feedback to clients. Let’s update our code with error handling for various scenarios:
func handleConnection(conn net.Conn) {
// ...
for {
request, err := reader.ReadString('\n')
if err != nil {
log.Printf("Error reading request from client: %v", err)
writer.WriteString("Error reading request\n")
writer.Flush()
return
}
// ...
switch command {
case "GET":
// ...
value, ok := database[key]
if !ok {
writer.WriteString("Key not found\n")
} else {
writer.WriteString(value + "\n")
}
writer.Flush()
case "SET":
// ...
database[key] = value
writer.WriteString("OK\n")
writer.Flush()
case "DEL":
// ...
delete(database, key)
writer.WriteString("OK\n")
writer.Flush()
default:
writer.WriteString("Unknown command\n")
writer.Flush()
}
}
}
This example demonstrates error handling for reading client requests and handling key not found errors.
Conclusion
In this tutorial, we implemented a simple key-value store over TCP in Go. We covered creating a TCP server, implementing a basic key-value store, handling multiple clients concurrently, and adding error handling. You should now have a good foundation for building more complex networked applications in Go.
Feel free to modify and extend this implementation to fit your needs.