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.
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
} handleConnchecks 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Nullbody Notes
Go backend development, learning open-source project source code together, focusing on simplicity and practicality.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
