Olivine: Building a Redis-Compatible Service in Go. · 章節目錄

第 2 章

[Day 2] Application

App struct

We wrote all codes in main.go, and it is not a good idea: when project scales, developers puts lots of code, logic, logging, comments in single file, that will make everyone crazy.

I always create a App{} struct in cmd/olivine/app.go, it controls application lifecycle (bootstrap, run, close, …).

package main

import (
	"bufio"
	"fmt"
	"log/slog"
	"net"
	"strings"
)

type App struct{}

func (app *App) Run() error {
	slog.Info("starting olivine server...")
	listener, err := net.Listen("tcp", ":16879")
	if err != nil {
		return fmt.Errorf("failed to listen tcp: %w", err)
	}
	defer listener.Close()

	for {
		connection, err := listener.Accept()
		if err != nil {
			slog.Error("failed to accept connection:", slog.Any("error", err))
			continue
		}
		go handleConnection(connection)
	}
}

func handleConnection(conn net.Conn) {
	reader := bufio.NewReader(conn)
	message, err := reader.ReadString('\n')
	if err != nil {
		slog.Error("failed to read from conn:", slog.Any("error", err))
		return
	}

	ackmsg := strings.ToUpper(strings.ToUpper(message))
	response := "ACK: " + ackmsg
	if _, err := conn.Write([]byte(response)); err != nil {
		slog.Error("failed to write to conn:", slog.Any("error", err))
		return
	}
}

The Server implementation

I create App{} for application lifecycle handling, but I still write request handler in this sturct, it’s bad smell in code.

The http.Server in stdard libary provides ListenAndServe() function, there’s an example: Go by Exaple: HTTP Servers. However, net.Listener didn’t provide this function, it’s time to make it by self!

Create an “internal package” in internal/server, it is a private package can be used by olivine package only. For more info: Organize a Go module: Package or command with supporting packages.

package server

import (
	"bufio"
	"log/slog"
	"net"
	"strings"
)

type Server interface {
	ListenAndServe() error
}

func NewServer() Server {
	return &simpleSrv{}
}

type simpleSrv struct {
	listener net.Listener
}

func (s *simpleSrv) ListenAndServe() (err error) {
	if s.listener, err = net.Listen("tcp", ":16879"); err != nil {
		return
	}

	for {
		conn, err := s.listener.Accept()
		if err != nil {
			slog.Error("failed to accept connection:", slog.Any("error", err))
			return err
		}

		go s.serve(conn)
	}
}

func (s *simpleSrv) serve(conn net.Conn) {
	defer conn.Close()

	rd := bufio.NewReader(conn)
	msg, err := rd.ReadString('\n')
	if err != nil {
		slog.Error("failed to read from conn:", slog.Any("error", err))
		return
	}

	if _, err := conn.Write([]byte("ACK: " + strings.ToUpper(msg))); err != nil {
		slog.Error("failed to write to conn:", slog.Any("error", err))
		return
	}
}
package main

import (
	"log/slog"
	
	"olivine/internal/server"
)

type App struct {
	srv server.Server
}

func NewApp() *App {
	return &App{
		srv: server.NewServer(),
	}
}

func (app *App) Run() error {
	slog.Info("starting olivine server...")
	if err := app.srv.ListenAndServe(); err != nil {
		return err
	}

	return nil
}

Why Server interface?

I built type Server interface with ListenAndServe() error function, and implement type simpleSrv struct{} for this interface.

But, why?

It’s undoubt over-design for this project at this moment, but I plan to implement other server implementations to compare performance or something interesting.

Where’s listener.Close()?

Unlike http.Server, there’s no Close() or Shutdown() in simpleSrv{}, not yet.

I’ll add Shutdown() for graceful shutdown in the future, maybe.