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

第 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),
	}
}