Developing a key/value store in Go

In this section, you will learn how to develop an unsophisticated version of a key-value store in Go, which means that you will learn how to implement the core functionality of a key-value store without any additional bells and whistles. The idea behind a key-value store is modest: answer queries fast and, generally speaking, work as fast as possible. This translates into using simple algorithms and simple data structures.

The program presented now will basically implement the four fundamental tasks of a key-value store:

  1. Adding a new element
  2. Deleting an existing element from the key-value store based on a key
  3. Looking up for the value of a specific key in the store, and
  4. Changing the value of an existing key

These four functions allow you to have full control over the key-value store. The commands for these four functions will be named ADD, DELETE, LOOKUP, and CHANGE, respectively. This means that the program will only operate when its gets one of these four commands. Additionally, the program will stop when you enter the STOP word as input, and it will print the full contents of the key-value store when you enter the PRINT command.

The name of the program in this example will be keyValue.go, and it will be presented in five code segments.

The first code segment of keyValue.go follows next:

package main 
 
import ( 
    "bufio" 
    "fmt" 
    "os" 
    "strings" 
) 
 
type myElement struct { 
    Name    string 
    Surname string 
    Id      string 
} 
 
var DATA = make(map[string]myElement) 

The key-value store is stored in a native Go map because using a built-in Go structure is usually faster. The map variable is defined as a global variable, where its keys are string variables and its values are myElement variables. You can also see the definition of the myElement struct type here.

The second code segment of keyValue.go is as follows:

func ADD(k string, n myElement) bool { 
    if k == "" { 
        return false 
    } 
 
    if LOOKUP(k) == nil { 
        DATA[k] = n 
        return true 
    } 
    return false 
} 
 
func DELETE(k string) bool { 
    if LOOKUP(k) != nil { 
        delete(DATA, k) 
        return true 
    } 
    return false 
} 

This code contains the implementation of two functions that support the functionality of the ADD and DELETE commands. Note that if the user tries to add a new element to the store without giving enough values to populate the myElement struct, the ADD function will not fail. For this particular program, the missing fields of the myElement struct will be set to the empty string. However, if you try to add a key that already exists, you will get an error message instead of modifying the existing key-value.

Notice that I rarely use function names with all caps unless I want them to stand out in the code.

The third portion of keyValue.go contains the following code:

func LOOKUP(k string) *myElement { 
    _, ok := DATA[k] 
    if ok { 
        n := DATA[k] 
        return &n 
    } else { 
        return nil 
    } 
} 
 
func CHANGE(k string, n myElement) bool { 
    DATA[k] = n 
    return true 
} 
 
func PRINT() { 
    for k, d := range DATA { 
        fmt.Printf("key: %s value: %v
", k, d) 
    } 
} 

In this Go code segment, you see the implementation of the functions that support the functionality of the LOOKUP and CHANGE commands. If you try to change a key that does not exist, the program will add that key to the store without generating any error messages.

In this part, you can also see the implementation of the PRINT() function that prints the full contents of the key-value store.

The fourth part of keyValue.go is as follows:

func main() { 
    scanner := bufio.NewScanner(os.Stdin) 
    for scanner.Scan() { 
        text := scanner.Text() 
        text = strings.TrimSpace(text) 
        tokens := strings.Fields(text) 
 
        switch len(tokens) { 
        case 0: 
               continue 
        case 1: 
              tokens = append(tokens, "") 
              tokens = append(tokens, "") 
              tokens = append(tokens, "") 
              tokens = append(tokens, "") 
        case 2: 
              tokens = append(tokens, "") 
              tokens = append(tokens, "") 
              tokens = append(tokens, "") 
        case 3: 
              tokens = append(tokens, "") 
              tokens = append(tokens, "")
        case 4: 
              tokens = append(tokens, "") 
        } 

In this part of keyValue.go, you read the input from the user. First, the for loop makes sure that the program will keep running for as long as the user provides some input. Additionally, the program makes sure that the tokens slice has at least five elements, even though only the ADD command requires that number of elements. Thus, for an ADD operation to be complete and not to have any missing values, you will need an input that looks like ADD aKey Field1 Field2 Field3.

The last part of keyValue.go is shown in the following Go code:

        switch tokens[0] { 
        case "PRINT": 
              PRINT() 
        case "STOP": 
              return 
        case "DELETE": 
              if !DELETE(tokens[1]) { 
                    fmt.Println("Delete operation failed!") 
              } 
        case "ADD": 
              n := myElement{tokens[2], tokens[3], tokens[4]} 
              if !ADD(tokens[1], n) { 
                    fmt.Println("Add operation failed!") 
              } 
        case "LOOKUP": 
              n := LOOKUP(tokens[1]) 
              if n != nil { 
                    fmt.Printf("%v
", *n) 
              } 
        case "CHANGE": 
              n := myElement{tokens[2], tokens[3], tokens[4]} 
              if !CHANGE(tokens[1], n) { 
                    fmt.Println("Update operation failed!") 
              } 
        default: 
              fmt.Println("Unknown command - please try again!") 
        } 
    } 
} 

In this part of the program, you process the input from the user. The switch statement makes the design of the program very clean, and it saves you from having to use multiple if...else statements.

Executing and using keyValue.go will create the following output:

$ go run keyValue.go
UNKNOWN
Unknown command - please try again!
ADD 123 1 2 3
ADD 234 2 3 4
ADD 345
PRINT
key: 123 value: {1 2 3}
key: 234 value: {2 3 4}
key: 345 value: { }
ADD 345 3 4 5
Add operation failed!
PRINT
key: 123 value: {1 2 3}
key: 234 value: {2 3 4}
key: 345 value: { }
CHANGE 345 3 4 5
PRINT
key: 123 value: {1 2 3}
key: 234 value: {2 3 4}
key: 345 value: {3 4 5}
DELETE 345
PRINT
key: 123 value: {1 2 3}
key: 234 value: {2 3 4}
DELETE 345
Delete operation failed!
PRINT
key: 123 value: {1 2 3}
key: 234 value: {2 3 4}
ADD 345 3 4 5
ADD 567 -5 -6 -7
PRINT
key: 123 value: {1 2 3}
key: 234 value: {2 3 4}
key: 345 value: {3 4 5}
key: 567 value: {-5 -6 -7}
CHANGE 345
PRINT
key: 123 value: {1 2 3}
key: 234 value: {2 3 4}
key: 345 value: { }
key: 567 value: {-5 -6 -7}
STOP

You will have to wait until Chapter 8, Telling a Unix System What to Do, in order to learn how to add data persistence to the key-value store.

You can also improve keyValue.go by adding goroutines and channels to it. However, adding goroutines and channels to a single user application has no practical meaning. Nevertheless, if you make keyValue.go capable of operating over TCP/IP networks, then the use of goroutines and channels will allow it to accept multiple connections and serve multiple users. You will learn more about routines and channels in Chapter 9, Go Concurrency – Goroutines, Channels and Pipelines and Chapter 10, Go Concurrency – Advanced topics. You will also learn about creating network applications in Go in Chapter 12, The Foundations of Network Programming in Go, and in Chapter 13, Network Programming – Building Servers and Clients.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.116.27.178