iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0

在實作 Redis 指令之前,我們可以預先構想一下這些指令應該怎麼被處理。

工廠模式 是一個重要的設計模式之一,它讓開發者能夠專注在單一功能的實作上(以這個專案來說,就是每個 Redis 指令的實作)。

註:有許多人誤會只有被選入 Design Patterns: Elements of Reusable Object-Oriented Software 才算是設計模式,然而設計模式本身多種多樣,且根據不同的程式語言特性會有不同的設計模式的出現與消亡。

實現工廠模式

server.(*simpleHandler) 中加入 factory 作為工廠:

// file: ./internal/server/handler.go
type HandlerFunc func(context.Context, *resp.Command) (resp.Value, error)

func NewHandler() Handler {
	return &simpleHandler{
		factory: make(map[string]HandlerFunc),
	}
}

type simpleHandler struct {
	factory map[string]HandlerFunc
}

其中 HandlerFunc 作為一個函式,就是真正執行任務的部份,利用 factory map[string]HandlerFunc 就可以達成根據不同的指令達成不同任務的功能:

// file: ./internal/server/handler.go

func (h *simpleHandler) ServeRESP(ctx context.Context, rd *resp.Reader) (resp.Value, error) {
	cmd, err := resp.ReadCommand(rd)
	if err != nil {
		if errors.Is(err, resp.ErrProtocol) {
			return resp.NewSimpleError(err), fmt.Errorf("%w: %w", ErrClient, err)
		} else {
			return nil, fmt.Errorf("%w: %w", ErrServer, err)
		}
	}

	slog.Info("Read RESP command:", slog.Any("command", cmd))

	return h.exec(ctx, cmd)
}

func (h *simpleHandler) exec(ctx context.Context, cmd *resp.Command) (resp.Value, error) {
	f, ok := h.factory[cmd.Command()]
	if !ok {
		err := fmt.Errorf("%w: unknown command: %s", ErrClient, cmd.Command())
		return resp.NewSimpleError(err), err
	}

	return f(ctx, cmd)
}

cmd.Command() 是另外加入的函式,其實就只是把 Redis 指令的 BulkString 轉為大寫(因為 Redis 對於指令是大小寫不敏感的)

cmd.(Command) 介面與實作

Redis 支援多種不同的指令,因此我們可以先定義一個 cmd.(Command) 介面,未來所有的指令只要實現這個介面就可以被支援。

// file: ./internal/service/cmd/cmd.go
package cmd

import (
	"context"
	"olivine/pkg/resp"
)

type Command interface {
	Command() string
	Exec(context.Context, *resp.Command) (resp.Value, error)
}

func NewCommands() []Command {
	return []Command{}
}

其中,cmd.(Command).Exec() 其實就是 server.(HandlerFunc),這意味著實作了 cmd.(Command) 介面的話,都可以被應用到 server.(Server).ServeRESP() 中。

server.NewHandler() 中把相應的指令注入即可。

註:這邊本來有個 Code Block,但 ithome 奇怪的 Cloudflare 規則把我的內文檔掉了所以我沒辦法放 code 上來,這邊先用圖片代替
https://ithelp.ithome.com.tw/upload/images/20260802/20183331XTjMS8V8SK.png

https://ithelp.ithome.com.tw/upload/images/20260802/20183331XU5KQhzCmB.png

不要忘記在 main.NewApp() 時也一併注入 cmd.NewCommands()

// file: ./cmd/olivine/app.go
func NewApp() *App {
	return &App{
		srv: server.NewServer(server.NewHandler(cmd.NewCommands())),
	}
}

加入 PING 指令

有了以上的基礎之後,加入 Redis 指令就變得相當簡單:只要實作 cmd.(Command) 並註冊到 cmd.NewCommands() 中就可以支援。

在此之前,我們先來看一下 PING 指令的規則:

PING [message]

其中 [message] 是可選的,如果沒有設定就回應 PONG,如果有設定就按照設定的回覆。

127.0.0.1:6379> PING
PONG
127.0.0.1:6379> PING hello
"hello"

按照這個邏輯,我們實作如下:

// file: ./internal/service/cmd/ping.go
package cmd

import (
	"context"
	"olivine/pkg/resp"
)

type Ping struct{}

func (c *Ping) Command() string {
	return "PING"
}

func (c *Ping) Exec(_ context.Context, cmd *resp.Command) (resp.Value, error) {
	args := cmd.Args()
	if len(args) == 0 {
		return resp.SimpleString("PONG"), nil
	}

	return args[0], nil
}

然後將 cmd.(*Ping) 註冊到 cmd.NewCommands() 中:

// file: ./internal/service/cmd/cmd.go
func NewCommands() []Command {
	return []Command
		&Ping{},
	}
}

上一篇
[Day 10] Redis 指令管線
系列文
從零開始打造 Redis:以 Go 建立 Production Ready 應用程式11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言