在實作 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 上來,這邊先用圖片代替

不要忘記在 main.NewApp() 時也一併注入 cmd.NewCommands():
// file: ./cmd/olivine/app.go
func NewApp() *App {
return &App{
srv: server.NewServer(server.NewHandler(cmd.NewCommands())),
}
}
有了以上的基礎之後,加入 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{},
}
}