Building Your Own Redis in Go: TCP Server Implementation

This article walks through creating a lightweight Redis clone named EasyRedis in Go, covering a producer‑consumer logger, .conf file parsing with reflection, a TCP server with graceful shutdown, and demonstrates the running service via telnet, with all source code available on GitHub.

Nullbody Notes
Nullbody Notes
Nullbody Notes
Building Your Own Redis in Go: TCP Server Implementation

Introduction

The series aims to build a functional Redis service called EasyRedis in Go, exposing the inner workings rather than just high‑level descriptions. The first part focuses on the TCP service layer.

Logger Implementation

Path: tool/logger. The design follows a producer‑consumer model where writeLog pushes logMessage structs into a buffered channel logMsgChan. A dedicated goroutine consumes messages, writes colored logs to the console, and persists them to a file, handling log rotation across days.

func Debug(msg string) { if defaultLogger.logLevel >= DEBUG { defaultLogger.writeLog(DEBUG, callerDepth, msg) } }
func Debugf(format string, v ...any) { if defaultLogger.logLevel >= DEBUG { msg := fmt.Sprintf(format, v...); defaultLogger.writeLog(DEBUG, callerDepth, msg) } }
// ... similar functions for Info, Warn, Error, Fatal ...

The writeLog function formats messages with file and line information, reuses logMessage objects from a pool, and sends them to the channel.

func (l *logger) writeLog(level LogLevel, callerDepth int, msg string) {
    var formattedMsg string
    _, file, line, ok := runtime.Caller(callerDepth)
    if ok {
        formattedMsg = fmt.Sprintf("[%s][%s:%d] %s", levelFlags[level], file, line, msg)
    } else {
        formattedMsg = fmt.Sprintf("[%s] %s", levelFlags[level], msg)
    }
    logMsg := l.logMsgPool.Get().(*logMessage)
    logMsg.level = level
    logMsg.msg = formattedMsg
    l.logMsgChan <- logMsg
}

Configuration File Parsing

Path: tool/conf. The parser reads a Redis .conf file line‑by‑line, stores key‑value pairs in a map, then uses reflect to map those values onto a *RedisConfig struct, handling strings, booleans, integers, and string slices.

func parse(r io.Reader) *RedisConfig {
    newRedisConfig := &RedisConfig{}
    lineMap := make(map[string]string)
    scanner := bufio.NewScanner(r)
    for scanner.Scan() {
        line := strings.TrimLeft(scanner.Text(), " ")
        if len(line) == 0 || line[0] == '#' { continue }
        idx := strings.IndexAny(line, " ")
        if idx > 0 && idx < len(line)-1 {
            key := line[:idx]
            value := strings.Trim(line[idx+1:], " ")
            lineMap[strings.ToLower(key)] = value
        }
    }
    // error handling omitted for brevity
    configValue := reflect.ValueOf(newRedisConfig).Elem()
    configType := reflect.TypeOf(newRedisConfig).Elem()
    for i := 0; i < configType.NumField(); i++ {
        fieldType := configType.Field(i)
        fieldName := strings.Trim(fieldType.Tag.Get("conf"), " ")
        if fieldName == "" { fieldName = fieldType.Name } else { fieldName = strings.Split(fieldName, ",")[0] }
        fieldName = strings.ToLower(fieldName)
        if fieldValue, ok := lineMap[fieldName]; ok {
            switch fieldType.Type.Kind() {
            case reflect.String:
                configValue.Field(i).SetString(fieldValue)
            case reflect.Bool:
                configValue.Field(i).SetBool("yes" == fieldValue)
            case reflect.Int:
                if intValue, err := strconv.ParseInt(fieldValue, 10, 64); err == nil {
                    configValue.Field(i).SetInt(intValue)
                }
            case reflect.Slice:
                if fieldType.Type.Elem().Kind() == reflect.String {
                    tmpSlice := strings.Split(fieldValue, ",")
                    configValue.Field(i).Set(reflect.ValueOf(tmpSlice))
                }
            }
        }
    }
    return newRedisConfig
}

TCP Server Implementation

Path: tcpserver. The server is created with NewTCPServer, which stores configuration, a signal channel, and a Redis handler.

func NewTCPServer(conf TCPConfig, handler redis.Handler) *TCPServer {
    server := &TCPServer{conf: conf, closeTcp: 0, clientCounter: 0, quit: make(chan os.Signal, 1), redisHander: handler}
    return server
}

The Start method listens on the configured address, logs the bind, launches accept in a goroutine, and blocks until a termination signal arrives.

func (t *TCPServer) Start() error {
    listen, err := net.Listen("tcp", t.conf.Addr)
    if err != nil { return err }
    t.listener = listen
    logger.Infof("bind %s listening...", t.conf.Addr)
    go t.accept()
    signal.Notify(t.quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
    <-t.quit
    return nil
}

The accept loop continuously accepts new connections, handling temporary errors with a short retry, logging permanent errors, and spawning a goroutine to process each connection via handleConn.

func (t *TCPServer) accept() error {
    for {
        conn, err := t.listener.Accept()
        if err != nil {
            if ne, ok := err.(net.Error); ok && ne.Timeout() {
                logger.Infof("accept occurs temporary error: %v, retry in 5ms", err)
                time.Sleep(5 * time.Millisecond)
                continue
            }
            logger.Warn(err.Error())
            atomic.CompareAndSwapInt32(&t.closeTcp, 0, 1)
            t.quit <- syscall.SIGTERM
            break
        }
        go t.handleConn(conn)
    }
    return nil
}
handleConn

checks for graceful shutdown, increments connection counters, logs the new connection, and delegates request handling to the Redis handler (implemented in a later article).

func (t *TCPServer) handleConn(conn net.Conn) {
    if atomic.LoadInt32(&t.closeTcp) == 1 { conn.Close(); return }
    logger.Debugf("accept new conn %s", conn.RemoteAddr().String())
    t.waitDone.Add(1)
    atomic.AddInt64(&t.clientCounter, 1)
    defer func() { t.waitDone.Done(); atomic.AddInt64(&t.clientCounter, -1) }()
    t.redisHander.Handle(context.Background(), conn)
}

The Close method performs a graceful shutdown: it marks the server as closed, stops listening, closes the Redis handler, and waits for all active connections to finish.

func (t *TCPServer) Close() {
    logger.Info("graceful shutdown easyredis server")
    atomic.CompareAndSwapInt32(&t.closeTcp, 0, 1)
    t.listener.Close()
    t.redisHander.Close()
    t.waitDone.Wait()
}

Demonstration

Running the compiled binary and connecting via telnet shows the server accepting connections and printing colored logs, as illustrated by the screenshots below.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

ConfigurationTCPServerlogger
Nullbody Notes
Written by

Nullbody Notes

Go backend development, learning open-source project source code together, focusing on simplicity and practicality.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.