Bölüm 18/03: In-Memory Key-Value Store
Development
Diğer pek çok framework’ün belli bir yoğurt yeme tarzı olur. Python’cuların çok sevdiği Django, Ruby’cilerin Ruby on Rails, PHP’cilerin Laravel, Node’cuların Express gibi sizin adınıza pek çok sorunu çözdükleri harika framework’leri var. İçinde bir çok fonksiyon ve mantık barındıran devasa kod yığınları.
Go’da bu tür uçtan-uca her derde deva bir framework ne yazık ki yok. Eğer http server ihtiyacımız varsa, sadece http katmanını çözen, sadece veritabanı katmanını çözen, aynı lego parçaları gibi ayrı-ayrı kütüphaneler bulunur. Bunları birbirine bağlamak da geliştirciye kalır :)
Dolayısıyla, go için en fazla best practice’ler (en iyi pratikler) söz konusudur. GitHub’ta pek çok "go application / project structre" gibi repo’lar bulmak mümkün.
Genelde ben, go’nun kaynak kodunu referans alıyorum, acaba go’yu icad edenler ne tür bir yaklaşım içine girmişler, neler uygulamışlar hep bunlara bakıyorum.
Bu bağlamda proje yapımız:
.
├── Dockerfile
├── README.md
├── cmd
│ └── server
├── go.mod
└── src
├── apiserver
│ ├── apiserver.go
│ └── middlewares.go
├── internal
│ ├── kverror
│ ├── service
│ │ └── kvstoreservice
│ ├── storage
│ │ └── memory
│ └── transport
│ └── http
└── releaseinfo
şeklinde. src/internal/
dizini altında;
storage/
service/
transport/http
paket tanımlarımızı yapıyoruz. Neden internal/
kullanıyoruz, eğer bu projeyi
herhangi bir kullanıcı go get
ile sanki bir kütüphaneymiş gibi projesine
eklerse, internal/
altındaki hiçbir pakete erişimi olamayacak! Şu an sadece;
src/apiserver
src/releaseinfo
Paketleri exportable yani import
edilebilir durumda. Esas uygulamanın
çalışacağı yer cmd/server/
altındaki main.go
dosyası olacak. Server
ile ilgili tanımlamaları apiserver/
altında yapacağız. Özel bir error
tipimiz var: kverror
.
storage/memory/
kullanacağımız in-memory storage davranışı ve işi yapan
fonksiyonlar burada olacak. Yarın "artık veritabanı kullananım" dersek;
storage/postgresql/
altına gereken davranışları ve fonksiyonları
tanımlayabiliriz.
Keza, aynı şekilde, yarın sadece http
protokolü yerine rpc
ya da grpc
sunmak istersek: transport/rpc/
ya da transport/grpc/
gibi ilerleyebiliriz.
go mod init
Şimdi geliştirme yapacağımız dizine gidip projeyi başlatalım:
$ cd /path/to/development/
$ mkdir kvstore
$ cd kvstore/
$ git init
$ git commit --allow-empty -m '[root] add initial commit'
Şimdi https://gitignore.io sitesinden projemiz için gereken .gitignore
dosyasını alıyoruz ve projenin ana dizininde (root) touch .gitignore
yaparak
içine paste ediyoruz;
$ git add .
$ git commit -m 'add gitignore file'
Şimdi go modülümüzü oluşturalım;
$ go mod init github.com/<GITHUB-KULLANICI-ADINIZ>/kvstore
$ git add .
$ git commit -m 'add go.mod file'
İlk olarak storage katmanından başlıyoruz;
$ mkdir -p src/internal/storage/memory/kvstorage
$ tree .
.
├── go.mod
└── src
└── internal
└── storage
└── memory
└── kvstorage <--- paket adı
Şimdi storage ile ilgili tanımları yapmak için;
$ touch src/internal/storage/memory/kvstorage/base.go
Şimdi projeyi kod editöründe açalım ve
src/internal/storage/memory/kvstorage/base.go
dosyasına şunu yazalım ve
kaydedelim:
package kvstorage
Go koduna başladık, hemen linter konfigürasyon
dosyamızı root dizine atalım, sonra base.go
dosyasını aşağıdaki gibi
düzenleyelim;
package kvstorage
import (
"sync"
)
var _ Storer = (*memoryStorage)(nil) // compile time proof
// MemoryDB is a custom type definition uses map[string]any for in memory-db type.
type MemoryDB map[string]any
// Storer defines storage behaviours.
type Storer interface {
Set(key string, value any) (any, error)
Get(key string) (any, error)
Update(key string, value any) (any, error)
Delete(key string) error
List() MemoryDB
}
type memoryStorage struct {
mu sync.RWMutex // guarding db only
db MemoryDB
}
// StorageOption represents storage option type.
type StorageOption func(*memoryStorage)
// WithMemoryDB sets db option.
func WithMemoryDB(db MemoryDB) StorageOption {
return func(s *memoryStorage) {
s.db = db
}
}
// New instantiates new storage instance.
func New(options ...StorageOption) Storer {
ms := &memoryStorage{}
for _, o := range options {
o(ms)
}
return ms
}
sonra;
$ git add .
$ git commit -m 'start storage implementation'
Şimdi tüm metotları implemente edelim:
$ touch src/internal/storage/memory/kvstorage/{delete,get,list,set,update}.go
$ tree .
.
├── go.mod
└── src
└── internal
└── storage
└── memory
└── kvstorage
├── base.go
├── delete.go
├── get.go
├── list.go
├── set.go
└── update.go
Şimdi bize özel error tipimizi oluşturalım;
$ mkdir -p src/internal/kverror
$ touch src/internal/kverror/kverror.go
src/internal/kverror/kverror.go
package kverror
var (
_ error = (*Error)(nil) // compile time proof
_ KVError = (*Error)(nil) // compile time proof
)
// sentinel errors.
var (
ErrKeyExists = New("key exist", true)
ErrKeyNotFound = New("key not found", false)
ErrUnknown = New("unknown error", true)
)
// KVError defines custom error behaviours.
type KVError interface {
Wrap(err error) KVError
Unwrap() error
AddData(any) KVError
DestoryData() KVError
Error() string
}
// Error is a custom type definition uses struct, custom error.
type Error struct {
Err error
Message string
Data any `json:"-"`
Loggable bool
}
// AddData adds extra data to error.
func (e *Error) AddData(data any) KVError {
e.Data = data
return e
}
// Unwrap unwraps error.
func (e *Error) Unwrap() error {
return e.Err
}
// DestoryData removes added data from error.
func (e *Error) DestoryData() KVError {
e.Data = nil
return e
}
// Wrap wraps given error.
func (e *Error) Wrap(err error) KVError {
e.Err = err
return e
}
func (e *Error) Error() string {
if e.Err != nil {
return e.Err.Error() + ", " + e.Message
}
return e.Message
}
// New instantiates new Error instance.
func New(m string, l bool) KVError {
return &Error{
Message: m,
Loggable: l,
}
}
sonra;
$ git add src/internal/kverror/kverror.go
$ git commit -m 'implement custom error type'
src/internal/storage/memory/kvstorage/delete.go
package kvstorage
func (ms *memoryStorage) Delete(key string) error {
if _, err := ms.Get(key); err != nil { // can not delete! key doesn't exist
return err
}
ms.mu.Lock()
defer ms.mu.Unlock()
delete(ms.db, key)
return nil
}
src/internal/storage/memory/kvstorage/get.go
package kvstorage
import (
"fmt"
"github.com/<GITHUB-KULLANICI-ADINIZ>/kvstore/src/internal/kverror"
)
func (ms *memoryStorage) Get(key string) (any, error) {
ms.mu.RLock()
defer ms.mu.RUnlock()
value, ok := ms.db[key]
if !ok {
return nil, fmt.Errorf("%w", kverror.ErrKeyNotFound.AddData("'"+key+"' does not exist"))
}
return value, nil
}
src/internal/storage/memory/kvstorage/list.go
package kvstorage
func (ms *memoryStorage) List() MemoryDB {
ms.mu.RLock()
defer ms.mu.RUnlock()
return ms.db
}
src/internal/storage/memory/kvstorage/set.go
package kvstorage
import (
"fmt"
"github.com/<GITHUB-KULLANICI-ADINIZ>/kvstore/src/internal/kverror"
)
func (ms *memoryStorage) Set(key string, value any) (any, error) {
if _, err := ms.Get(key); err == nil {
return nil, fmt.Errorf("%w", kverror.ErrKeyExists.AddData("'"+key+"' already exist"))
}
ms.mu.Lock()
defer ms.mu.Unlock()
ms.db[key] = value
return value, nil
}
src/internal/storage/memory/kvstorage/update.go
package kvstorage
func (ms *memoryStorage) Update(key string, value any) (any, error) {
if _, err := ms.Get(key); err != nil { // can not update! key doesn't exist
return nil, err
}
ms.mu.Lock()
defer ms.mu.Unlock()
ms.db[key] = value
return value, nil
}
sonra;
$ git add .
$ git commit -m 'implement memory storage'
Nedir son durum ?
$ tree .
.
├── go.mod
└── src
└── internal
├── kverror
│ └── kverror.go
└── storage
└── memory
└── kvstorage
├── base.go
├── delete.go
├── get.go
├── list.go
├── set.go
└── update.go
Service Layer
$ mkdir -p src/internal/service/kvstoreservice
$ touch src/internal/service/kvstoreservice/base.go
src/internal/service/kvstoreservice/base.go
package kvstoreservice
import (
"context"
"github.com/<GITHUB-KULLANICI-ADINIZ>/kvstore/src/internal/storage/memory/kvstorage"
)
var _ KVStoreService = (*kvStoreService)(nil) // compile time proof
// KVStoreService defines service behaviours.
type KVStoreService interface {
Set(context.Context, *SetRequest) (*ItemResponse, error)
Get(context.Context, string) (*ItemResponse, error)
Update(context.Context, *UpdateRequest) (*ItemResponse, error)
Delete(context.Context, string) error
List(context.Context) (*ListResponse, error)
}
type kvStoreService struct {
storage kvstorage.Storer
}
// ServiceOption represents service option type.
type ServiceOption func(*kvStoreService)
// WithStorage sets storage option.
func WithStorage(strg kvstorage.Storer) ServiceOption {
return func(s *kvStoreService) {
s.storage = strg
}
}
// New instantiates new service instance.
func New(options ...ServiceOption) KVStoreService {
kvs := &kvStoreService{}
for _, o := range options {
o(kvs)
}
return kvs
}
sonra;
$ touch src/internal/service/kvstoreservice/{delete,get,list,requests,responses,set,update}.go
$ tree .
.
├── go.mod
└── src
└── internal
├── kverror
│ └── kverror.go
├── service
│ └── kvstoreservice
│ ├── base.go
│ ├── delete.go
│ ├── get.go
│ ├── list.go
│ ├── requests.go
│ ├── responses.go
│ ├── set.go
│ └── update.go
└── storage
└── memory
└── kvstorage
├── base.go
├── delete.go
├── get.go
├── list.go
├── set.go
└── update.go
src/internal/service/kvstoreservice/delete.go
package kvstoreservice
import (
"context"
"fmt"
)
func (s *kvStoreService) Delete(ctx context.Context, key string) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := s.storage.Delete(key); err != nil {
return fmt.Errorf("kvstoreservice.Set storage.Delete err: %w", err)
}
return nil
}
}
src/internal/service/kvstoreservice/get.go
package kvstoreservice
import (
"context"
"fmt"
)
func (s *kvStoreService) Get(ctx context.Context, key string) (*ItemResponse, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
value, err := s.storage.Get(key)
if err != nil {
return nil, fmt.Errorf("kvstoreservice.Set storage.Get err: %w", err)
}
return &ItemResponse{
Key: key,
Value: value,
}, nil
}
}
src/internal/service/kvstoreservice/list.go
package kvstoreservice
import (
"context"
)
func (s *kvStoreService) List(ctx context.Context) (*ListResponse, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
items := s.storage.List()
response := make(ListResponse, len(items))
var i int
for k, v := range items {
response[i] = ItemResponse{
Key: k,
Value: v,
}
i++
}
return &response, nil
}
}
src/internal/service/kvstoreservice/requests.go
package kvstoreservice
// SetRequest is an input payload for Set behaviour.
type SetRequest struct {
Key string
Value any
}
// UpdateRequest is an input payload for Update behaviour.
type UpdateRequest struct {
Key string
Value any
}
src/internal/service/kvstoreservice/responses.go
package kvstoreservice
// ItemResponse represents common k/v response element.
type ItemResponse struct {
Key string
Value any
}
// ListResponse is a collection on ItemResponse.
type ListResponse []ItemResponse
src/internal/service/kvstoreservice/set.go
package kvstoreservice
import (
"context"
"fmt"
)
func (s *kvStoreService) Set(ctx context.Context, sr *SetRequest) (*ItemResponse, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
if _, err := s.storage.Set(sr.Key, sr.Value); err != nil {
return nil, fmt.Errorf("kvstoreservice.Set storage.Set err: %w", err)
}
return &ItemResponse{
Key: sr.Key,
Value: sr.Value,
}, nil
}
}
src/internal/service/kvstoreservice/update.go
package kvstoreservice
import (
"context"
"fmt"
)
func (s *kvStoreService) Update(ctx context.Context, sr *UpdateRequest) (*ItemResponse, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
value, err := s.storage.Update(sr.Key, sr.Value)
if err != nil {
return nil, fmt.Errorf("kvstoreservice.Set storage.Update err: %w", err)
}
return &ItemResponse{
Key: sr.Key,
Value: value,
}, nil
}
}
sonra;
$ git add .
$ git commit -m 'implement service layer'
HTTP Handler Layer
$ mkdir -p src/internal/transport/http/{basehttp,kvstore}handler
$ touch src/internal/transport/http/basehttphandler/basehttphandler.go
$ touch src/internal/transport/http/kvstorehandler/base.go
$ tree .
.
├── go.mod
└── src
└── internal
├── kverror
│ └── kverror.go
├── service
│ └── kvstoreservice
│ ├── base.go
│ ├── delete.go
│ ├── get.go
│ ├── list.go
│ ├── requests.go
│ ├── responses.go
│ ├── set.go
│ └── update.go
├── storage
│ └── memory
│ └── kvstorage
│ ├── base.go
│ ├── delete.go
│ ├── get.go
│ ├── list.go
│ ├── set.go
│ └── update.go
└── transport
└── http
├── basehttphandler
│ └── basehttphandler.go
└── kvstorehandler
├── base.go
├── delete.go
├── get.go
├── list.go
├── set.go
└── update.go
src/internal/transport/http/basehttphandler/basehttphandler.go
package basehttphandler
import (
"encoding/json"
"log/slog"
"net/http"
"time"
)
// Handler respresents common http handler functionality.
type Handler struct {
ServerEnv string
Logger *slog.Logger
CancelTimeout time.Duration
}
// JSON generates json response.
func (h *Handler) JSON(w http.ResponseWriter, status int, d any) {
j, err := json.Marshal(d)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_, _ = w.Write(j)
}
src/internal/transport/http/kvstorehandler/base.go
package kvstorehandler
import (
"log/slog"
"net/http"
"time"
"github.com/<GITHUB-KULLANICI-ADINIZ>/kvstore/src/internal/service/kvstoreservice"
"github.com/<GITHUB-KULLANICI-ADINIZ>/kvstore/src/internal/transport/http/basehttphandler"
)
var _ KVStoreHTTPHandler = (*kvstoreHandler)(nil) // compile time proof
// KVStoreHTTPHandler defines /store/ http handler behaviours.
type KVStoreHTTPHandler interface {
Set(http.ResponseWriter, *http.Request)
Get(http.ResponseWriter, *http.Request)
Update(http.ResponseWriter, *http.Request)
Delete(http.ResponseWriter, *http.Request)
List(http.ResponseWriter, *http.Request)
}
type kvstoreHandler struct {
basehttphandler.Handler
service kvstoreservice.KVStoreService
}
// StoreHandlerOption represents store handler option type.
type StoreHandlerOption func(*kvstoreHandler)
// WithService sets service option.
func WithService(srvc kvstoreservice.KVStoreService) StoreHandlerOption {
return func(s *kvstoreHandler) {
s.service = srvc
}
}
// WithContextTimeout sets handler context cancel timeout.
func WithContextTimeout(d time.Duration) StoreHandlerOption {
return func(s *kvstoreHandler) {
s.Handler.CancelTimeout = d
}
}
// WithServerEnv sets handler server env.
func WithServerEnv(env string) StoreHandlerOption {
return func(s *kvstoreHandler) {
s.Handler.ServerEnv = env
}
}
// WithLogger sets handler logger.
func WithLogger(l *slog.Logger) StoreHandlerOption {
return func(s *kvstoreHandler) {
s.Handler.Logger = l
}
}
// New instantiates new kvstoreHandler instance.
func New(options ...StoreHandlerOption) KVStoreHTTPHandler {
kvsh := &kvstoreHandler{
Handler: basehttphandler.Handler{},
}
for _, o := range options {
o(kvsh)
}
return kvsh
}
src/internal/transport/http/kvstorehandler/delete.go
package kvstorehandler
import (
"context"
"errors"
"net/http"
"github.com/<GITHUB-KULLANICI-ADINIZ>/kvstore/src/internal/kverror"
)
func (h *kvstoreHandler) Delete(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
h.JSON(
w,
http.StatusMethodNotAllowed,
map[string]string{"error": "method " + r.Method + " not allowed"},
)
return
}
if len(r.URL.Query()) == 0 {
h.JSON(
w,
http.StatusNotFound,
map[string]string{"error": "key query param required"},
)
return
}
keys, ok := r.URL.Query()["key"]
if !ok {
h.JSON(
w,
http.StatusNotFound,
map[string]string{"error": "key not present"},
)
return
}
key := keys[0]
ctx, cancel := context.WithTimeout(r.Context(), h.CancelTimeout)
defer cancel()
if err := h.service.Delete(ctx, key); err != nil {
if errors.Is(err, context.DeadlineExceeded) {
h.JSON(
w,
http.StatusGatewayTimeout,
map[string]string{"error": err.Error()},
)
return
}
var kvErr *kverror.Error
if errors.As(err, &kvErr) {
clientMessage := kvErr.Message
if kvErr.Data != nil {
data, ok := kvErr.Data.(string)
if ok {
clientMessage = clientMessage + ", " + data
}
}
if kvErr.Loggable {
h.Logger.Error("kvstorehandler Delete service.Delete", "err", clientMessage)
}
if kvErr == kverror.ErrKeyNotFound {
h.JSON(w, http.StatusNotFound, map[string]string{"error": clientMessage})
return
}
}
h.JSON(
w,
http.StatusInternalServerError,
map[string]string{"error": err.Error()},
)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNoContent)
}
src/internal/transport/http/kvstorehandler/get.go
package kvstorehandler
import (
"context"
"errors"
"net/http"
"github.com/<GITHUB-KULLANICI-ADINIZ>/kvstore/src/internal/kverror"
)
func (h *kvstoreHandler) Get(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
h.JSON(
w,
http.StatusMethodNotAllowed,
map[string]string{"error": "method " + r.Method + " not allowed"},
)
return
}
if len(r.URL.Query()) == 0 {
h.JSON(
w,
http.StatusNotFound,
map[string]string{"error": "key query param required"},
)
return
}
keys, ok := r.URL.Query()["key"]
if !ok {
h.JSON(
w,
http.StatusNotFound,
map[string]string{"error": "key not present"},
)
return
}
key := keys[0]
ctx, cancel := context.WithTimeout(r.Context(), h.CancelTimeout)
defer cancel()
serviceResponse, err := h.service.Get(ctx, key)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
h.JSON(
w,
http.StatusGatewayTimeout,
map[string]string{"error": err.Error()},
)
return
}
var kvErr *kverror.Error
if errors.As(err, &kvErr) {
clientMessage := kvErr.Message
if kvErr.Data != nil {
data, ok := kvErr.Data.(string)
if ok {
clientMessage = clientMessage + ", " + data
}
}
if kvErr.Loggable {
h.Logger.Error("kvstorehandler Get service.Get", "err", clientMessage)
}
if kvErr == kverror.ErrKeyNotFound {
h.JSON(w, http.StatusNotFound, map[string]string{"error": clientMessage})
return
}
}
h.JSON(
w,
http.StatusInternalServerError,
map[string]string{"error": err.Error()},
)
return
}
handlerResponse := ItemResponse{
Key: serviceResponse.Key,
Value: serviceResponse.Value,
}
h.JSON(
w,
http.StatusOK,
handlerResponse,
)
}
src/internal/transport/http/kvstorehandler/list.go
package kvstorehandler
import (
"context"
"errors"
"net/http"
"github.com/<GITHUB-KULLANICI-ADINIZ>/kvstore/src/internal/kverror"
)
func (h *kvstoreHandler) List(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
h.JSON(
w,
http.StatusMethodNotAllowed,
map[string]string{"error": "method " + r.Method + " not allowed"},
)
return
}
ctx, cancel := context.WithTimeout(r.Context(), h.CancelTimeout)
defer cancel()
serviceResponse, err := h.service.List(ctx)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
h.JSON(
w,
http.StatusGatewayTimeout,
map[string]string{"error": err.Error()},
)
return
}
var kvErr *kverror.Error
if errors.As(err, &kvErr) {
clientMessage := kvErr.Message
if kvErr.Data != nil {
data, ok := kvErr.Data.(string)
if ok {
clientMessage = clientMessage + ", " + data
}
}
if kvErr.Loggable {
h.Logger.Error("kvstorehandler List service.List", "err", clientMessage)
}
}
h.JSON(
w,
http.StatusInternalServerError,
map[string]string{"error": err.Error()},
)
return
}
var handlerResponse ListResponse
for _, item := range *serviceResponse {
handlerResponse = append(handlerResponse, ItemResponse{
Key: item.Key,
Value: item.Value,
})
}
if len(handlerResponse) == 0 {
h.JSON(
w,
http.StatusNotFound,
map[string]string{"error": "nothing found"},
)
return
}
h.JSON(
w,
http.StatusOK,
handlerResponse,
)
}
src/internal/transport/http/kvstorehandler/requests.go
package kvstorehandler
// SetRequest is an input payload for creating new k/v item.
type SetRequest struct {
Key string `json:"key"`
Value any `json:"value"`
}
// UpdateRequest is an input payload for updating existing k/v item.
type UpdateRequest struct {
Key string `json:"key"`
Value any `json:"value"`
}
src/internal/transport/http/kvstorehandler/responses.go
package kvstorehandler
// ItemResponse represents k/v item.
type ItemResponse struct {
Key string `json:"key"`
Value any `json:"value"`
}
// ListResponse represents collection of ItemResponse.
type ListResponse []ItemResponse
src/internal/transport/http/kvstorehandler/set.go
package kvstorehandler
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"github.com/<GITHUB-KULLANICI-ADINIZ>/kvstore/src/internal/kverror"
"github.com/<GITHUB-KULLANICI-ADINIZ>/kvstore/src/internal/service/kvstoreservice"
)
func (h *kvstoreHandler) Set(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
h.JSON(
w,
http.StatusMethodNotAllowed,
map[string]string{"error": "method " + r.Method + " not allowed"},
)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
h.JSON(
w,
http.StatusBadRequest,
map[string]string{"error": err.Error()},
)
return
}
if len(body) == 0 {
h.JSON(
w,
http.StatusBadRequest,
map[string]string{"error": "empty body/payload"},
)
return
}
var handlerRequest SetRequest
if err = json.Unmarshal(body, &handlerRequest); err != nil {
h.JSON(
w,
http.StatusInternalServerError,
map[string]string{"error": err.Error()},
)
return
}
if handlerRequest.Key == "" {
h.JSON(
w,
http.StatusBadRequest,
map[string]string{"error": "key is empty"},
)
return
}
if handlerRequest.Value == nil {
h.JSON(
w,
http.StatusBadRequest,
map[string]string{"error": "value is empty"},
)
return
}
ctx, cancel := context.WithTimeout(r.Context(), h.CancelTimeout)
defer cancel()
existingItem, err := h.service.Get(ctx, handlerRequest.Key)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
h.JSON(
w,
http.StatusGatewayTimeout,
map[string]string{"error": err.Error()},
)
return
}
var kvErr *kverror.Error
if errors.As(err, &kvErr) {
clientMessage := kvErr.Message
if kvErr.Data != nil {
data, ok := kvErr.Data.(string)
if ok {
clientMessage = clientMessage + ", " + data
}
}
if kvErr.Loggable {
h.Logger.Error("kvstorehandler Set service.Get", "err", clientMessage)
}
if kvErr != kverror.ErrKeyNotFound {
h.JSON(
w,
http.StatusBadRequest,
map[string]string{"error": clientMessage},
)
return
}
}
}
// this should be nil. means, key does not exist
if existingItem != nil {
h.JSON(
w,
http.StatusConflict,
map[string]string{"error": "can not set, '" + handlerRequest.Key + "' already exists"},
)
return
}
serviceRequest := kvstoreservice.SetRequest{
Key: handlerRequest.Key,
Value: handlerRequest.Value,
}
serviceResponse, err := h.service.Set(ctx, &serviceRequest)
if err != nil {
var kvErr *kverror.Error
if errors.As(err, &kvErr) {
clientMessage := kvErr.Message
if kvErr.Data != nil {
data, ok := kvErr.Data.(string)
if ok {
clientMessage = clientMessage + ", " + data
}
}
if kvErr.Loggable {
h.Logger.Error("kvstorehandler Set service.Set", "err", clientMessage)
}
if kvErr == kverror.ErrKeyExists {
h.JSON(w, http.StatusConflict, map[string]string{"error": clientMessage})
return
}
}
h.JSON(
w,
http.StatusInternalServerError,
map[string]string{"error": err.Error()},
)
return
}
handlerResponse := ItemResponse{
Key: serviceResponse.Key,
Value: serviceResponse.Value,
}
h.JSON(
w,
http.StatusCreated,
handlerResponse,
)
}
src/internal/transport/http/kvstorehandler/update.go
package kvstorehandler
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"github.com/<GITHUB-KULLANICI-ADINIZ>/kvstore/src/internal/kverror"
"github.com/<GITHUB-KULLANICI-ADINIZ>/kvstore/src/internal/service/kvstoreservice"
)
func (h *kvstoreHandler) Update(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut {
h.JSON(
w,
http.StatusMethodNotAllowed,
map[string]string{"error": "method " + r.Method + " not allowed"},
)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
h.JSON(
w,
http.StatusBadRequest,
map[string]string{"error": err.Error()},
)
return
}
if len(body) == 0 {
h.JSON(
w,
http.StatusBadRequest,
map[string]string{"error": "empty body/payload"},
)
return
}
var handlerRequest UpdateRequest
if err = json.Unmarshal(body, &handlerRequest); err != nil {
h.JSON(
w,
http.StatusInternalServerError,
map[string]string{"error": err.Error()},
)
return
}
if handlerRequest.Key == "" {
h.JSON(
w,
http.StatusBadRequest,
map[string]string{"error": "key is empty"},
)
return
}
if handlerRequest.Value == nil {
h.JSON(
w,
http.StatusBadRequest,
map[string]string{"error": "value is empty"},
)
return
}
ctx, cancel := context.WithTimeout(r.Context(), h.CancelTimeout)
defer cancel()
serviceRequest := kvstoreservice.UpdateRequest{
Key: handlerRequest.Key,
Value: handlerRequest.Value,
}
serviceResponse, err := h.service.Update(ctx, &serviceRequest)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
h.JSON(
w,
http.StatusGatewayTimeout,
map[string]string{"error": err.Error()},
)
return
}
var kvErr *kverror.Error
if errors.As(err, &kvErr) {
clientMessage := kvErr.Message
if kvErr.Data != nil {
data, ok := kvErr.Data.(string)
if ok {
clientMessage = clientMessage + ", " + data
}
}
if kvErr.Loggable {
h.Logger.Error("kvstorehandler Update service.Update", "err", clientMessage)
}
if kvErr == kverror.ErrKeyNotFound {
h.JSON(w, http.StatusNotFound, map[string]string{"error": clientMessage})
return
}
}
h.JSON(
w,
http.StatusInternalServerError,
map[string]string{"error": err.Error()},
)
return
}
handlerResponse := ItemResponse{
Key: serviceResponse.Key,
Value: serviceResponse.Value,
}
h.JSON(
w,
http.StatusOK,
handlerResponse,
)
}
sonra;
$ git add .
$ git commit -m 'implement http handlers'
releaseinfo paketi
$ mkdir -p src/releaseinfo
$ touch src/releaseinfo/releaseinfo.go
src/releaseinfo/releaseinfo.go
package releaseinfo
// Version is the current version of service.
const Version string = "0.0.0"
// BuildInformation holds current build information.
var BuildInformation string
sonra;
$ git add src/releaseinfo/releaseinfo.go
$ git commit -m 'add release information package'
apiserver paketi
$ mkdir -p src/apiserver
$ touch src/apiserver/{apiserver,middlewares}.go
$ tree .
.
├── go.mod
└── src
├── apiserver
│ ├── apiserver.go
│ └── middlewares.go
├── internal
│ ├── kverror
│ │ └── kverror.go
│ ├── service
│ │ └── kvstoreservice
│ │ ├── base.go
│ │ ├── delete.go
│ │ ├── get.go
│ │ ├── list.go
│ │ ├── requests.go
│ │ ├── responses.go
│ │ ├── set.go
│ │ └── update.go
│ ├── storage
│ │ └── memory
│ │ └── kvstorage
│ │ ├── base.go
│ │ ├── delete.go
│ │ ├── get.go
│ │ ├── list.go
│ │ ├── set.go
│ │ └── update.go
│ └── transport
│ └── http
│ ├── basehttphandler
│ │ └── basehttphandler.go
│ └── kvstorehandler
│ ├── base.go
│ ├── delete.go
│ ├── get.go
│ ├── list.go
│ ├── set.go
│ └── update.go
└── releaseinfo
└── releaseinfo.go
src/apiserver/apiserver.go
package apiserver
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/<GITHUB-KULLANICI-ADINIZ>/kvstore/src/internal/service/kvstoreservice"
"github.com/<GITHUB-KULLANICI-ADINIZ>/kvstore/src/internal/storage/memory/kvstorage"
"github.com/<GITHUB-KULLANICI-ADINIZ>/kvstore/src/internal/transport/http/kvstorehandler"
"github.com/<GITHUB-KULLANICI-ADINIZ>/kvstore/src/releaseinfo"
)
// constants.
const (
ContextCancelTimeout = 5 * time.Second
ShutdownTimeout = 10 * time.Second
ServerReadTimeout = 10 * time.Second
ServerWriteTimeout = 10 * time.Second
ServerIdleTimeout = 60 * time.Second
apiV1Prefix = "/api/v1"
)
type apiServer struct {
db kvstorage.MemoryDB
logLevel slog.Level
logger *slog.Logger
serverEnv string
}
// Option represents api server option type.
type Option func(*apiServer)
// WithLogger sets logger option.
func WithLogger(l *slog.Logger) Option {
return func(s *apiServer) {
s.logger = l
}
}
// WithServerEnv sets serverEnv option.
func WithServerEnv(env string) Option {
return func(s *apiServer) {
s.serverEnv = env
}
}
// WithLogLevel sets logLevel option.
func WithLogLevel(level string) Option {
return func(s *apiServer) {
var logLevel slog.Level
switch level {
case "DEBUG":
logLevel = slog.LevelDebug
case "WARN":
logLevel = slog.LevelWarn
case "ERROR":
logLevel = slog.LevelError
default:
logLevel = slog.LevelInfo
}
s.logLevel = logLevel
}
}
// New instantiates new server instance.
func New(options ...Option) error {
apisrvr := &apiServer{
db: kvstorage.MemoryDB(make(map[string]any)), // default db
logLevel: slog.LevelInfo,
}
for _, o := range options {
o(apisrvr)
}
// default logging options if logger not present.
if apisrvr.logger == nil {
logHandlerOpts := &slog.HandlerOptions{Level: apisrvr.logLevel}
logHandler := slog.NewJSONHandler(os.Stdout, logHandlerOpts)
apisrvr.logger = slog.New(logHandler)
}
slog.SetDefault(apisrvr.logger)
if apisrvr.serverEnv == "" {
apisrvr.serverEnv = "production" // default server environment
}
logger := apisrvr.logger
storage := kvstorage.New(
kvstorage.WithMemoryDB(apisrvr.db),
)
service := kvstoreservice.New(
kvstoreservice.WithStorage(storage),
)
kvStoreHandler := kvstorehandler.New(
kvstorehandler.WithService(service),
kvstorehandler.WithContextTimeout(ContextCancelTimeout),
kvstorehandler.WithServerEnv(apisrvr.serverEnv),
kvstorehandler.WithLogger(logger),
)
mux := http.NewServeMux()
mux.HandleFunc("/healthz/live/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
j, _ := json.Marshal(map[string]any{
"server": apisrvr.serverEnv,
"version": releaseinfo.Version,
"build_information": releaseinfo.BuildInformation,
"message": "liveness is OK!, server is ready to accept connections",
})
_, _ = w.Write(j)
})
mux.HandleFunc("/healthz/ready/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
j, _ := json.Marshal(map[string]any{
"server": apisrvr.serverEnv,
"version": releaseinfo.Version,
"build_information": releaseinfo.BuildInformation,
"message": "readiness is OK!, server is ready to accept connections",
})
_, _ = w.Write(j)
})
mux.HandleFunc(apiV1Prefix+"/set/", kvStoreHandler.Set)
mux.HandleFunc(apiV1Prefix+"/get/", kvStoreHandler.Get)
mux.HandleFunc(apiV1Prefix+"/update/", kvStoreHandler.Update)
mux.HandleFunc(apiV1Prefix+"/delete/", kvStoreHandler.Delete)
mux.HandleFunc(apiV1Prefix+"/list/", kvStoreHandler.List)
api := &http.Server{
Addr: ":8000",
Handler: appendSlashMiddleware(httpLoggingMiddleware(logger, mux)),
ReadTimeout: ServerReadTimeout,
WriteTimeout: ServerWriteTimeout,
IdleTimeout: ServerIdleTimeout,
}
shutdown := make(chan os.Signal, 1)
apiError := make(chan error, 1)
signal.Notify(shutdown, syscall.SIGINT, syscall.SIGTERM)
go func() {
logger.Info("starting api server", "listening", api.Addr, "env", apisrvr.serverEnv)
apiError <- api.ListenAndServe()
}()
select {
case err := <-apiError:
return fmt.Errorf("listen and server err: %w", err)
case sig := <-shutdown:
logger.Info("starting shutdown", "pid", sig)
defer logger.Info("shutdown completed", "pid", sig)
ctx, cancel := context.WithTimeout(context.Background(), ShutdownTimeout)
defer cancel()
if err := api.Shutdown(ctx); err != nil {
if errr := api.Close(); errr != nil {
logger.Error("api close", "err", errr)
}
return fmt.Errorf("could not stop server gracefully: %w", err)
}
}
return nil
}
src/apiserver/middlewares.go
package apiserver
import (
"log/slog"
"net/http"
"strings"
)
func httpLoggingMiddleware(l *slog.Logger, h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
h.ServeHTTP(w, r)
uri := r.URL.String()
method := r.Method
l.Info("http request", "method", method, "uri", uri)
}
return http.HandlerFunc(fn)
}
func appendSlashMiddleware(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" && !strings.HasSuffix(r.URL.Path, "/") {
redirectURL := r.URL.Path + "/"
if r.URL.RawQuery != "" {
redirectURL += "?" + r.URL.RawQuery
}
http.Redirect(w, r, redirectURL, http.StatusPermanentRedirect)
return
}
h.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
sonra;
$ git add .
$ git commit -m 'add apiserver'
Artık esas sunucuyu çalıştıracak kısma geldik;
$ mkdir -p cmd/server
$ touch cmd/server/main.go
cmd/server/main.go
package main
import (
"log"
"os"
"github.com/<GITHUB-KULLANICI-ADINIZ>/kvstore/src/apiserver"
)
func main() {
if err := apiserver.New(
apiserver.WithServerEnv(os.Getenv("SERVER_ENV")),
apiserver.WithLogLevel(os.Getenv("LOG_LEVEL")),
); err != nil {
log.Fatal(err)
}
}
sonra;
$ git add .
$ git commit -m 'add server'
Evet, şimdi kodumuzu linter’dan geçirelim;
$ golangci-lint version # v1.54.1
$ golangci-lint run
eğer her şey OK ise;
$ go run -race cmd/server/main.go
İstekleri Yapalım
Evet, şu an sunucumuz çalışıyor. İster curl
ister httpie
ile denemelere
başlayalım:
curl
örnekleri:
# add new key/value
$ curl -L -s -X POST -H "Content-Type: application/json" -d '{"key": "success", "value": true}' "http://localhost:8000/api/v1/set" | jq
{
"key": "success",
"value": true
}
$ curl -L -s -X POST -H "Content-Type: application/json" -d '{"key": "server_env", "value": "production"}' "http://localhost:8000/api/v1/set" | jq
{
"key": "server_env",
"value": "production"
}
$ curl -L -s -H "Content-Type: application/json" "http://localhost:8000/api/v1/list" | jq
[
{
"key": "success",
"value": true
},
{
"key": "server_env",
"value": "production"
}
]
$ curl -L -s -X PUT -H "Content-Type: application/json" -d '{"key": "success", "value": false}' "http://localhost:8000/api/v1/update" | jq
{
"key": "success",
"value": false
}
$ curl -L -s -H "Content-Type: application/json" "http://localhost:8000/api/v1/list" | jq
[
{
"key": "success",
"value": false
},
{
"key": "server_env",
"value": "production"
}
]
$ curl -L -s -H "Content-Type: application/json" "http://localhost:8000/api/v1/get?key=success" | jq
{
"key": "success",
"value": false
}
$ curl -L -s -X DELETE -H "Content-Type: application/json" -o /dev/null -w '%{http_code}\n' "http://localhost:8000/api/v1/delete?key=success"
204
$ curl -L -s -H "Content-Type: application/json" "http://localhost:8000/api/v1/list" | jq
[
{
"key": "server_env",
"value": "production"
}
]
httpie
örnekleri:
$ http POST "http://localhost:8000/api/v1/set" key="success" value:=true
$ http POST "http://localhost:8000/api/v1/set" key="server_env" value="production"
$ http "http://localhost:8000/api/v1/list"
$ http PUT "http://localhost:8000/api/v1/update" key="success" value:=false
$ http "http://localhost:8000/api/v1/get?key=success"
$ http DELETE "http://localhost:8000/api/v1/delete?key=success"