├── .circleci └── config.yml ├── .gitignore ├── LICENSE ├── README.md ├── gofmt.sh ├── hooks ├── Makefile └── pre-commit ├── httpd ├── service.go └── service_test.go ├── main.go └── store ├── store.go └── store_test.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/go-httpd/src/github.com/otoolep/go-httpd 5 | docker: 6 | - image: circleci/golang:1.10 7 | 8 | steps: 9 | - checkout 10 | - run: go tool vet . 11 | - run: go get -t -d -v ./... 12 | - run: go test -timeout 60s -v ./... 13 | - run: 14 | command: go test -race -timeout 120s -v ./... 15 | environment: 16 | GORACE: "halt_on_error=1" 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries 2 | go-httpd 3 | 4 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 5 | *.o 6 | *.a 7 | *.so 8 | 9 | # Folders 10 | _obj 11 | _test 12 | 13 | # Architecture specific extensions/prefixes 14 | *.[568vq] 15 | [568vq].out 16 | 17 | *.cgo1.go 18 | *.cgo2.c 19 | _cgo_defun.c 20 | _cgo_gotypes.go 21 | _cgo_export.* 22 | 23 | _testmain.go 24 | 25 | *.exe 26 | *.test 27 | *.prof 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2018 Philip O'Toole http://www.philipotoole.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-httpd [![Circle CI](https://circleci.com/gh/otoolep/go-httpd/tree/master.svg?style=svg)](https://circleci.com/gh/otoolep/go-httpd/tree/master) [![GoDoc](https://godoc.org/github.com/otoolep/go-httpd?status.png)](https://godoc.org/github.com/otoolep/go-httpd) [![Go Report Card](https://goreportcard.com/badge/github.com/otoolep/go-httpd)](https://goreportcard.com/report/github.com/otoolep/go-httpd) 2 | ====== 3 | 4 | go-httpd is an example [Go](http://golang.org) project, showing you how to organise that most basic of systems -- a HTTP server that allows you to set and get values in a key-value store. Trivial _Hello, World_ programs don't demonstrate how to organise a real program, but go-httpd does. It also exhibits many other important principles of a good Go project. 5 | 6 | ## Project layout 7 | `main.go`, within the _main_ package, is the entry point for the program, and this source file is at the top level of the tree. 8 | 9 | There are other possible paths for this main package, depending on your requirements. One other suggested place is in `cmd/go-httpd/`. This can be convenient if your project builds more than one binary -- a CLI for example. Then the CLI can be located in `cmd/go-http-cli`. Two examples of projects using this structure are [rqlite](http://github.com/rqlite/rqlite) and [InfluxDB](https://github.com/influxdata/influxdb). 10 | 11 | Otherwise the remainder of the project is contained in two sub-packages -- _http_ and _store_. 12 | 13 | ### Project name 14 | It's best not to call your project `go-`. Just call it ``. I called this project `go-httpd` to make the connection with Go clear. 15 | 16 | ## Running go-httpd 17 | *Building go-httpd requires Go 1.5 or later. [gvm](https://github.com/moovweb/gvm) is a great tool for installing and managing your versions of Go.* 18 | 19 | Starting and running go-httpd is easy. Download and build it like so: 20 | ``` 21 | mkdir go-httpd # Or any directory of your choice 22 | cd go-httpd/ 23 | export GOPATH=$PWD 24 | go get github.com/otoolep/go-httpd 25 | ``` 26 | Some people consider using a distinct `GOPATH` environment variable for each project _doing it wrong_. In practise I, and many other Go programmers, find this actually most convenient. 27 | 28 | Run it like so: 29 | ``` 30 | $GOPATH/bin/go-httpd 31 | ``` 32 | 33 | You can now set a key and read its value back as follows: 34 | ``` 35 | curl -XPOST localhost:8080/key -d '{"user1": "batman"}' 36 | curl -XGET localhost:8080/key/user1 37 | ``` 38 | 39 | ### Building and rebuilding 40 | Once you've downloaded the code from GitHub, you'll probably want to change the code and rebuild it. The easiest way to do this is to execute the following commands: 41 | ``` 42 | cd $GOPATH/src/github.com/otoolep/go-httpd 43 | go build 44 | ./go-httpd 45 | ``` 46 | To build __and__ install the binary in `$GOPATH/bin`, instead execute `go install`. 47 | 48 | ### Development environments 49 | vim has good support for Go, via a [plugin](https://github.com/fatih/vim-go). Go support for Sublime 3 is also available via [GoSublime](https://github.com/DisposaBoy/GoSublime). 50 | 51 | ## Use the standard library 52 | Go comes with a high-quality standard library, with support for much important functionality such as networking, compression, JSON serialization, encryption, IO, concurrency, and synchronization. It is better to use the standard library even if it means having to write a little bit of extra code, than to import a third-party library. You can learn more about this philosophy [here](https://blog.gopheracademy.com/advent-2014/case-against-3pl/). 53 | 54 | You can see this principle at work in go-httpd. It uses the standard _logging_ package, _testing_ package, and does its own HTTP routing, even though there are literally hundreds of non-standard packages that claim to do a better job in each area. In my experience just stick with the standard library if at all possible. 55 | 56 | ## Logging 57 | Each package prefixes its log output with an identifiable string, and sends the output to `stderr`. Many operators find this most convenient, as **they** can then decide where to send the log output of your program. 58 | 59 | ## Testing 60 | Each package is tested via the framework that comes with the standard library, using simple test cases. A full test suite would involve many more tests. 61 | 62 | To run the test suite execute the following command: 63 | ``` 64 | cd $GOPATH/src/github.com/otoolep/go-httpd 65 | go test -v ./... 66 | ``` 67 | 68 | ### Interfaces 69 | Interfaces are a key concept within Go, and allow components to interact in a very natural way. The [_io.Reader_](https://golang.org/pkg/io/#Reader) and [_io.Writer_](https://golang.org/pkg/io/#Writer) interfaces are the most important examples in the standard library. 70 | 71 | Interfaces are also very useful for testing. The HTTP service within go-httpd does not import the Store directly, but instead specifies the interface any Store must support. This makes it very easy to pass a mock store to the HTTP service, as part of this testing. 72 | 73 | ## Documentation 74 | The _GoDoc_ standard is very easy to follow and results in nice, easy to read, documentation. 75 | 76 | go-httpd is documented in the GoDoc style. The GoDoc link at the top of this file is automatically generated from the comments in the source. Check the source of the README file itself to see how to add these badges to your own projects. 77 | 78 | ## Pre-commit hooks 79 | Within the `hooks` directory is a git _pre-commit hook_. Installing this hook means that your code is checked for [_go fmt_](https://golang.org/cmd/gofmt/) and _go vet_ errors before it can even be committed. While these checks can be run manually at any time, installing them as hooks ensures you don't forget. 80 | 81 | To install the hook execute `make install` from within the `hooks directory`. 82 | 83 | ## CircleCI integration 84 | [CircleCI](http://www.circleci.com) supports basic Go testing without any extra work on your part, but that testing can be more sophisticated. The example `yml` file included in this repository shows how to instruct CircleCI (version 1.0) to perform extra testing such as checking for formatting and linting issues. It also instructs CircleCI to run your code through Go's [race detection](https://blog.golang.org/race-detector) system. 85 | 86 | ## Go Programming References 87 | Be sure to check out the following references: 88 | * The [standard docs](https://golang.org/pkg/). You really don't need much else. 89 | * [Effective Go](https://golang.org/doc/effective_go.html). You should read this when you first start programming in Go, and then read it again 3 months later. And then read it again 6 months after that. 90 | * [How to Write Go](https://golang.org/doc/code.html). 91 | * [The Go Programming Language](http://www.amazon.com/Programming-Language-Addison-Wesley-Professional-Computing/dp/0134190440). 92 | * The [Go Playground](https://play.golang.org/) -- a useful place to share snippets of Go code. 93 | * [rqlite](http://github.com/rqlite/rqlite) -- a distributed database, with SQLite as its storage engine. rqlite is a much more sophisticated example of production-quality Go code, which strives to follow all the principles outlined above. 94 | * [400 Days of Go](http://www.philipotoole.com/400-days-of-go/) -- a blog post I wrote on why I like Go. 95 | -------------------------------------------------------------------------------- /gofmt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | fmtcount=`git ls-files | grep '.go$' | xargs gofmt -l 2>&1 | wc -l` 3 | if [ $fmtcount -gt 0 ]; then 4 | echo "run 'go fmt ./...' to format your source code." 5 | exit 1 6 | fi 7 | -------------------------------------------------------------------------------- /hooks/Makefile: -------------------------------------------------------------------------------- 1 | GIT_ROOT=`git rev-parse --show-toplevel` 2 | 3 | install: 4 | cp pre-commit $(GIT_ROOT)/.git/hooks/pre-commit 5 | chmod +x $(GIT_ROOT)/.git/hooks/pre-commit 6 | -------------------------------------------------------------------------------- /hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | fmtcount=`git ls-files | grep '.go$' | xargs gofmt -l 2>&1 | wc -l` 4 | if [ $fmtcount -gt 0 ]; then 5 | echo "Some files aren't formatted, please run 'go fmt ./...' to format your source code before committing" 6 | exit 1 7 | fi 8 | 9 | vetcount=`go tool vet -composites=true ./ 2>&1 | wc -l` 10 | if [ $vetcount -gt 0 ]; then 11 | echo "Some files aren't passing vet heuristics, please run 'go vet ./...' to see the errors it flags and correct your source code before committing" 12 | exit 1 13 | fi 14 | exit 0 15 | -------------------------------------------------------------------------------- /httpd/service.go: -------------------------------------------------------------------------------- 1 | // Package httpd provides the HTTP server for accessing the key-value store. 2 | package httpd 3 | 4 | import ( 5 | "encoding/json" 6 | "io" 7 | "io/ioutil" 8 | "log" 9 | "net" 10 | "net/http" 11 | "os" 12 | "strings" 13 | ) 14 | 15 | // Store is the interface the key-value stores must implement. 16 | type Store interface { 17 | // Get returns the value for the given key. 18 | Get(key string) (string, error) 19 | 20 | // Set sets the value for the given key. 21 | Set(key, value string) error 22 | 23 | // Delete removes the given key. 24 | Delete(key string) error 25 | } 26 | 27 | // Service provides HTTP service. 28 | type Service struct { 29 | addr string 30 | ln net.Listener 31 | logger *log.Logger 32 | 33 | store Store 34 | } 35 | 36 | // New returns an uninitialized HTTP service. 37 | func New(addr string, store Store) *Service { 38 | return &Service{ 39 | addr: addr, 40 | store: store, 41 | logger: log.New(os.Stderr, "[http] ", log.LstdFlags), 42 | } 43 | } 44 | 45 | // Start starts the service. 46 | func (s *Service) Start() error { 47 | server := http.Server{ 48 | Handler: s, 49 | } 50 | 51 | ln, err := net.Listen("tcp", s.addr) 52 | if err != nil { 53 | return err 54 | } 55 | s.ln = ln 56 | 57 | http.Handle("/", s) 58 | 59 | go func() { 60 | err := server.Serve(s.ln) 61 | if err != nil { 62 | s.logger.Fatalf("HTTP serve: %s", err) 63 | } 64 | }() 65 | 66 | return nil 67 | } 68 | 69 | // Close closes the service. 70 | func (s *Service) Close() error { 71 | s.ln.Close() 72 | return nil 73 | } 74 | 75 | // ServeHTTP allows Service to serve HTTP requests. 76 | func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { 77 | s.logger.Println(r.Method, r.URL) 78 | if strings.HasPrefix(r.URL.Path, "/key") { 79 | s.handleKeyRequest(w, r) 80 | } else { 81 | w.WriteHeader(http.StatusNotFound) 82 | } 83 | } 84 | 85 | func (s *Service) handleKeyRequest(w http.ResponseWriter, r *http.Request) { 86 | getKey := func() string { 87 | parts := strings.Split(r.URL.Path, "/") 88 | if len(parts) != 3 { 89 | return "" 90 | } 91 | return parts[2] 92 | } 93 | 94 | switch r.Method { 95 | case "GET": 96 | k := getKey() 97 | if k == "" { 98 | w.WriteHeader(http.StatusBadRequest) 99 | } 100 | v, err := s.store.Get(k) 101 | if err != nil { 102 | w.WriteHeader(http.StatusInternalServerError) 103 | return 104 | } 105 | 106 | b, err := json.Marshal(map[string]string{k: v}) 107 | if err != nil { 108 | w.WriteHeader(http.StatusInternalServerError) 109 | return 110 | } 111 | 112 | io.WriteString(w, string(b)) 113 | 114 | case "POST": 115 | // Read the value from the POST body. 116 | b, err := ioutil.ReadAll(r.Body) 117 | if err != nil { 118 | w.WriteHeader(http.StatusBadRequest) 119 | return 120 | } 121 | m := map[string]string{} 122 | if err := json.Unmarshal(b, &m); err != nil { 123 | w.WriteHeader(http.StatusBadRequest) 124 | } 125 | for k, v := range m { 126 | if err := s.store.Set(k, v); err != nil { 127 | w.WriteHeader(http.StatusInternalServerError) 128 | return 129 | } 130 | } 131 | 132 | case "DELETE": 133 | k := getKey() 134 | if k == "" { 135 | w.WriteHeader(http.StatusBadRequest) 136 | return 137 | } 138 | if err := s.store.Delete(k); err != nil { 139 | w.WriteHeader(http.StatusInternalServerError) 140 | return 141 | } 142 | s.store.Delete(k) 143 | 144 | default: 145 | w.WriteHeader(http.StatusMethodNotAllowed) 146 | } 147 | return 148 | } 149 | 150 | // Addr returns the address on which the Service is listening 151 | func (s *Service) Addr() net.Addr { 152 | return s.ln.Addr() 153 | } 154 | -------------------------------------------------------------------------------- /httpd/service_test.go: -------------------------------------------------------------------------------- 1 | package httpd 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | // Test_NewServer performs basic testing of the HTTP service. 15 | func Test_NewServer(t *testing.T) { 16 | store := newTestStore() 17 | s := &testServer{New(":0", store)} 18 | if s == nil { 19 | t.Fatal("failed to create HTTP service") 20 | } 21 | 22 | if err := s.Start(); err != nil { 23 | t.Fatalf("failed to start HTTP service: %s", err) 24 | } 25 | 26 | b := doGet(t, s.URL(), "k1") 27 | if string(b) != `{"k1":""}` { 28 | t.Fatalf("wrong value received for key k1: %s", string(b)) 29 | } 30 | 31 | doPost(t, s.URL(), "k1", "v1") 32 | 33 | b = doGet(t, s.URL(), "k1") 34 | if string(b) != `{"k1":"v1"}` { 35 | t.Fatalf("wrong value received for key k1: %s", string(b)) 36 | } 37 | 38 | store.m["k2"] = "v2" 39 | b = doGet(t, s.URL(), "k2") 40 | if string(b) != `{"k2":"v2"}` { 41 | t.Fatalf("wrong value received for key k2: %s", string(b)) 42 | } 43 | 44 | doDelete(t, s.URL(), "k2") 45 | b = doGet(t, s.URL(), "k2") 46 | if string(b) != `{"k2":""}` { 47 | t.Fatalf("wrong value received for key k2: %s", string(b)) 48 | } 49 | 50 | } 51 | 52 | // testServer represents a service under test. 53 | type testServer struct { 54 | *Service 55 | } 56 | 57 | // URL returns the URL of the service. 58 | func (t *testServer) URL() string { 59 | port := strings.TrimLeft(t.Addr().String(), "[::]:") 60 | return fmt.Sprintf("http://127.0.0.1:%s", port) 61 | } 62 | 63 | // testStore represents a mock store, demonstrating the use of interfaces 64 | // to mock out a real store. 65 | type testStore struct { 66 | m map[string]string 67 | } 68 | 69 | // newTestStore returns an initialized mock store. 70 | func newTestStore() *testStore { 71 | return &testStore{ 72 | m: make(map[string]string), 73 | } 74 | } 75 | 76 | // Get gets the requested key. 77 | func (t *testStore) Get(key string) (string, error) { 78 | return t.m[key], nil 79 | } 80 | 81 | // Set sets the given key to given value. 82 | func (t *testStore) Set(key, value string) error { 83 | t.m[key] = value 84 | return nil 85 | } 86 | 87 | // Delete delets the given key. 88 | func (t *testStore) Delete(key string) error { 89 | delete(t.m, key) 90 | return nil 91 | } 92 | 93 | func doGet(t *testing.T, url, key string) string { 94 | resp, err := http.Get(fmt.Sprintf("%s/key/%s", url, key)) 95 | if err != nil { 96 | t.Fatalf("failed to GET key: %s", err) 97 | } 98 | defer resp.Body.Close() 99 | body, err := ioutil.ReadAll(resp.Body) 100 | if err != nil { 101 | t.Fatalf("failed to read response: %s", err) 102 | } 103 | return string(body) 104 | } 105 | 106 | func doPost(t *testing.T, url, key, value string) { 107 | b, err := json.Marshal(map[string]string{key: value}) 108 | if err != nil { 109 | t.Fatalf("failed to encode key and value for POST: %s", err) 110 | } 111 | resp, err := http.Post(fmt.Sprintf("%s/key", url), "application-type/json", bytes.NewReader(b)) 112 | if err != nil { 113 | t.Fatalf("POST request failed: %s", err) 114 | } 115 | defer resp.Body.Close() 116 | } 117 | 118 | func doDelete(t *testing.T, u, key string) { 119 | ru, err := url.Parse(fmt.Sprintf("%s/key/%s", u, key)) 120 | if err != nil { 121 | t.Fatalf("failed to parse URL for delete: %s", err) 122 | } 123 | req := &http.Request{ 124 | Method: "DELETE", 125 | URL: ru, 126 | } 127 | 128 | client := http.Client{} 129 | resp, err := client.Do(req) 130 | if err != nil { 131 | t.Fatalf("failed to GET key: %s", err) 132 | } 133 | defer resp.Body.Close() 134 | } 135 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | 11 | "github.com/otoolep/go-httpd/httpd" 12 | "github.com/otoolep/go-httpd/store" 13 | ) 14 | 15 | // DefaultHTTPAddr is the default HTTP bind address. 16 | const DefaultHTTPAddr = ":8080" 17 | 18 | // Parameters 19 | var httpAddr string 20 | 21 | // init initializes this package. 22 | func init() { 23 | flag.StringVar(&httpAddr, "addr", DefaultHTTPAddr, "Set the HTTP bind address") 24 | flag.Usage = func() { 25 | fmt.Fprintf(os.Stderr, "Usage: %s [options]\n", os.Args[0]) 26 | flag.PrintDefaults() 27 | } 28 | } 29 | 30 | // main is the entry point for the service. 31 | func main() { 32 | flag.Parse() 33 | 34 | s := store.New() 35 | if err := s.Open(); err != nil { 36 | log.Fatalf("failed to open store: %s", err.Error()) 37 | } 38 | 39 | h := httpd.New(httpAddr, s) 40 | if err := h.Start(); err != nil { 41 | log.Fatalf("failed to start HTTP service: %s", err.Error()) 42 | } 43 | 44 | log.Println("httpd started successfully") 45 | 46 | signalCh := make(chan os.Signal, 1) 47 | signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM) 48 | 49 | // Block until one of the signals above is received 50 | select { 51 | case <-signalCh: 52 | log.Println("signal received, shutting down...") 53 | if err := s.Close(); err != nil { 54 | log.Println("failed to close store:", err.Error()) 55 | } 56 | if err := h.Close(); err != nil { 57 | log.Println("failed to close HTTP service:", err.Error()) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /store/store.go: -------------------------------------------------------------------------------- 1 | // Package store provides a simple key-value store. 2 | package store 3 | 4 | import ( 5 | "log" 6 | "os" 7 | "sync" 8 | ) 9 | 10 | // Store is a simple key-value store. 11 | type Store struct { 12 | mu sync.RWMutex 13 | m map[string]string // The key-value store for the system. 14 | 15 | logger *log.Logger 16 | } 17 | 18 | // New returns a new Store. 19 | func New() *Store { 20 | return &Store{ 21 | m: make(map[string]string), 22 | logger: log.New(os.Stderr, "[store] ", log.LstdFlags), 23 | } 24 | } 25 | 26 | // Open opens the store. 27 | func (s *Store) Open() error { 28 | s.logger.Println("store opened") 29 | return nil 30 | } 31 | 32 | // Close closes the store. 33 | func (s *Store) Close() error { 34 | return nil 35 | } 36 | 37 | // Get returns the value for the given key. 38 | func (s *Store) Get(key string) (string, error) { 39 | s.mu.RLock() 40 | defer s.mu.RUnlock() 41 | return s.m[key], nil 42 | } 43 | 44 | // Set sets the value for the given key. 45 | func (s *Store) Set(key, value string) error { 46 | s.mu.Lock() 47 | defer s.mu.Unlock() 48 | s.m[key] = value 49 | return nil 50 | } 51 | 52 | // Delete deletes the given key. 53 | func (s *Store) Delete(key string) error { 54 | s.mu.Lock() 55 | defer s.mu.Unlock() 56 | delete(s.m, key) 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /store/store_test.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // Test_StoreOpen tests that the store can be opened and closed. 8 | func Test_StoreOpen(t *testing.T) { 9 | s := New() 10 | if err := s.Open(); err != nil { 11 | t.Fatalf("failed to open store: %s", err) 12 | } 13 | if err := s.Close(); err != nil { 14 | t.Fatalf("failed to close store: %s", err) 15 | } 16 | } 17 | 18 | // Test_StoreOpenSingleNode tests reading and writing keys. 19 | func Test_StoreOpenSingleNode(t *testing.T) { 20 | s := New() 21 | if err := s.Open(); err != nil { 22 | t.Fatalf("failed to open store: %s", err) 23 | } 24 | 25 | if err := s.Set("foo", "bar"); err != nil { 26 | t.Fatalf("failed to set key: %s", err.Error()) 27 | } 28 | 29 | value, err := s.Get("foo") 30 | if err != nil { 31 | t.Fatalf("failed to get key: %s", err.Error()) 32 | } 33 | if value != "bar" { 34 | t.Fatalf("key has wrong value: %s", value) 35 | } 36 | 37 | if err := s.Delete("foo"); err != nil { 38 | t.Fatalf("failed to delete key: %s", err.Error()) 39 | } 40 | 41 | value, err = s.Get("foo") 42 | if err != nil { 43 | t.Fatalf("failed to get key: %s", err.Error()) 44 | } 45 | if value != "" { 46 | t.Fatalf("key has wrong value: %s", value) 47 | } 48 | } 49 | --------------------------------------------------------------------------------