第 6 章
Day 6:PING、SET 與 GET
Command interface
就像將 Handler 注入 Server 一樣,Redis Command 的實作也應注入 Handler。
在 internal/service/cmd/cmd.go 定義 Command interface:
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{
&Ping{},
}
}
接著將 cmds []cmd.Commands 加入 internal/server/handler.go 的 NewHandler() 參數:
func NewHandler(cmds []cmd.Command) Handler {
factory := make(map[string]HandlerFunc)
for _, cmd := range cmds {
factory[cmd.Command()] = cmd.Exec
}
return &simpleHandler{
factory: factory,
}
}
在 cmd/olivine/app.go 的 NewApp() 加入 NewCommands():
func NewApp() *App {
return &App{
srv: server.NewServer(server.NewHandler(cmd.NewCommands())),
}
}
加入 Ping 指令
Redis 的 PING 語法如下:
PING [message]
[message] 是選填參數。沒有指定時回傳 PONG,否則回傳 message:
(127.0.0.1:16879) > PING
PONG
(127.0.0.1:16879) > PING hello
"hello"
建立 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
}
注意:別忘了在
*resp.Command加入Args()。
func (cmd *Command) Command() string {
return strings.ToUpper(string(cmd.cmd.data))
}
將 Ping 註冊到 NewCommands():
func NewCommands() []Command {
return []Command{
&Ping{},
}
}
加入 Get 與 Set
實作這兩個指令之前,必須先決定資料如何儲存。
Storage interface
在 internal/repo/storage.go 定義 Storage interface:
package repo
import (
"context"
"errors"
"sync"
)
var (
ErrNotFound = errors.New("not found")
)
type Storage interface {
Set(ctx context.Context, k, v string) error
Get(ctx context.Context, k string) (string, error)
}
注意:目前還沒使用
ctx context.Context,但在 goroutine 間傳遞操作時,預先納入 context 是實用的做法。
mapStorage 實作
Go 內建的 map 可以用來儲存資料。
注意:Go 的 map 使用 Swiss Table,並利用 SIMD、branch prediction 等現代 CPU 特性最佳化。背景資料見 Faster Go maps with Swiss Tables。
建立實作 Storage 的 mapStorage:
type mapStorage struct {
storage map[string]string
mu sync.RWMutex
}
func (s *mapStorage) Set(_ context.Context, k, v string) error {
s.mu.Lock()
defer s.mu.Unlock()
s.storage[k] = v
return nil
}
func (s *mapStorage) Get(_ context.Context, k string) (v string, err error) {
s.mu.RLock()
defer s.mu.RUnlock()
var ok bool
if v, ok = s.storage[k]; !ok {
return "", ErrNotFound
}
return
}
Map 並非 concurrency-safe 資料型別。多個 goroutines 同時存取可能造成 panic,因此讀寫時以 sync.RWMutex 保護。
sync.RWMutex 允許多個讀取者並行讀取,適合讀取較多的情境。
NewStorage() constructor
加入 NewStorage():
func NewStorage() Storage {
s := mapStorage{}
s.storage = make(map[string]string)
return &s
}
接著在 cmd.NewCommand() 加入 repo.Storage 參數,對應的 diff 如下:
// file: internal/service/cmd/cmd.go
-func NewCommands() []Command {
+func NewCommands(storage repo.Storage) []Command {
return []Command{
&Ping{},
}
}
在 NewApp() 加入 NewStorage():
func NewApp() *App {
return &App{
srv: server.NewServer(server.NewHandler(cmd.NewCommands(repo.NewStorage()))),
}
}
加入 Set 與 Get
接著實作 Set 與 Get:
package cmd
import (
"context"
"fmt"
"olivine/internal/repo"
"olivine/pkg/resp"
)
type Set struct {
storage repo.Storage
}
func NewSet(storage repo.Storage) *Set {
return &Set{storage: storage}
}
func (c *Set) Command() string {
return "SET"
}
func (c *Set) Exec(ctx context.Context, cmd *resp.Command) (resp.Value, error) {
args := cmd.Args()
if len(args) != 2 {
return nil, fmt.Errorf("%w: argument count mismatch: expect '%d' got '%d'", ErrValidation, len(args), 2)
}
k, v := args[0], args[1]
if err := c.storage.Set(ctx, k.String(), v.String()); err != nil {
return nil, fmt.Errorf("%w: %w", ErrStorage, err)
}
return resp.SimpleString("OK"), nil
}
package cmd
import (
"context"
"errors"
"fmt"
"olivine/internal/repo"
"olivine/pkg/resp"
)
type Get struct {
storage repo.Storage
}
func NewGet(storage repo.Storage) *Get {
return &Get{
storage: storage,
}
}
func (c *Get) Command() string {
return "GET"
}
func (c *Get) Exec(ctx context.Context, cmd *resp.Command) (resp.Value, error) {
args := cmd.Args()
if len(args) != 1 {
return nil, fmt.Errorf("%w: argument count mismatch: expect '%d' got '%d'", ErrValidation, len(args), 1)
}
k := args[0]
v, err := c.storage.Get(ctx, k.String())
if err != nil {
if errors.Is(err, repo.ErrNotFound) {
return resp.NewNullBulkString(), nil
}
return nil, fmt.Errorf("%w: %w", ErrStorage, err)
}
return resp.NewBulkString(v), nil
}
將它們加入 NewCommands():
func NewCommands(storage repo.Storage) []Command {
return []Command{
&Ping{},
NewSet(storage),
NewGet(storage),
}
}