Olivine:以 Go 建構與 Redis 相容的服務 · 章節目錄

第 2 章

Day 2:應用程式架構

App struct

把所有程式碼都放在 main.go 並不是好主意。隨著專案成長,邏輯、紀錄與註解不斷擠進同一個檔案,會變得難以維護。

我通常會在 cmd/olivine/app.go 建立 App{} struct,管理應用程式生命週期,包含初始化、執行與關閉。

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
	}
}

Server 實作

雖然建立了 App{} 管理生命週期,但 request handler 仍寫在這個 struct 中,這是一個需要改善的設計。

標準函式庫的 http.Server 提供 ListenAndServe(),可以參考 Go by Example: HTTP Servers。net.Listener 沒有提供這個函式,因此要自己實作。

在 internal/server 建立 internal package,作為 olivine 專案內部使用的 package。相關說明見 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
}

為什麼使用 Server interface?

我建立了含有 ListenAndServe() error 的 type Server interface,再以 type simpleSrv struct{} 實作它。

目前這對專案來說確實有些過度設計,但我計畫加入其他 server 實作,用來比較效能或進行其他有趣的實驗。

listener.Close() 呢?

與 http.Server 不同,simpleSrv{} 目前還沒有 Close() 或 Shutdown()。

未來或許會加入 Shutdown(),實作 graceful shutdown。