第 6 章
[Day 6] PING, SET and GET
The Command{} interface
As Handler{} is injected into Server{}, the Redis commands implementation should be injected into Handler{}.
Define a Command{} interface in 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{
&Ping{},
}
}
Add cmds []cmd.Commands as argument for NewHandler() in internal/server/handler.go:
func NewHandler(cmds []cmd.Command) Handler {
factory := make(map[string]HandlerFunc)
for _, cmd := range cmds {
factory[cmd.Command()] = cmd.Exec
}
return &simpleHandler{
factory: factory,
}
}
Add NewCommands() into NewApp() in cmd/olivine/app.go:
func NewApp() *App {
return &App{
srv: server.NewServer(server.NewHandler(cmd.NewCommands())),
}
}
Add Ping{} command
The syntax of PING command of Redis is:
PING [message]
The [message] is optional: if not set, return PONG; otherwise, return [message]:
(127.0.0.1:16879) > PING
PONG
(127.0.0.1:16879) > PING hello
"hello"
Create a new file in 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
}
Note: remember to add
Args()into*resp.Command.func (cmd *Command) Command() string { return strings.ToUpper(string(cmd.cmd.data)) }
Register Ping{} into NewCommands():
func NewCommands() []Command {
return []Command{
&Ping{},
}
}
Add Get{} and Set{}
Before adding these commands, I need to decide how the data be stored.
The Storage{} interface
Define a Storage{} interface in internal/repo/storage.go:
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)
}
Note: honestly, the
ctx context.Contexthaven’t in used yet, but it’s a good practice for usingcontext.Contextwhen chaning with goroutine.
The mapStorage{} implementation
There’s a built-in data type in Go for storage: Map.
Note: Go use Swiss Table as built-in map implementation, it’s a data structure which optimized with SIMD, branch prediction and more modern CPU features. Read more detail in Faster Go maps with Swiss Tables.
Create mapStorage{} to implement Storage{} interface:
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 is not a concurrency safe data type, it could panic when two or more goroutines access the same map, that’s why it needs to lock with sync.RWMutex when reading and writing.
The sync.RWMutex doesn’t block concurrency reading, it is useful in read-heavy scenario.
NewStorage() constructor
Add NewStorage() constructor for Storage{} interface:
func NewStorage() Storage {
s := mapStorage{}
s.storage = make(map[string]string)
return &s
}
Add repo.Storage as argument in cmd.NewCommand():
// file: internal/service/cmd/cmd.go
-func NewCommands() []Command {
+func NewCommands(storage repo.Storage) []Command {
return []Command{
&Ping{},
}
}
Add NewStorage() in NewApp():
func NewApp() *App {
return &App{
srv: server.NewServer(server.NewHandler(cmd.NewCommands(repo.NewStorage()))),
}
}
Add Set{} and Get{}
It’s easy to add Set{} and 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
}
Add Set{} and Get{} into NewCommands():
func NewCommands(storage repo.Storage) []Command {
return []Command{
&Ping{},
NewSet(storage),
NewGet(storage),
}
}