GoCache: LRU Cache Implementation in Go

I got to know about Golang a year back and did write some toy programs in it while learning but then I gave up as I was not really enjoying despite liking Go language. It is very much like Python but with better performance because it’s compiled.

Recently I against wished to do something in Go. This time I did not want to go back to practice topic by topic. I rather thought to do some project and will learn whatever the stuff I need to get the thing done.

I have used Memcached in the past in PHP and really liked it so I thought to come up with a cache mechanism in Go. Upon learning I found out that Memcached has used the LRU cache technique. I had a couple of challenges:

  • Learning Go to do my stuff. Especially about structs, pointers, and maps.
  • Understanding LRU implementation.

It was not easy, but I pushed anyway to progress. I found a few implementations in Python and Java. I did not want to see any Go implementation as it could kill the entire purpose. After a month or so I was able to come up with an implementation consist of a couple of files.

/*
	- Set: Add the item both in queue and HashMap. If they capacity is full, it removes the least recently used
	element

	- Get: Returns the item requested via Key. On querying the item it comes to forward of the queue

*/

package main

import (
	"container/list"
	"errors"
	"fmt"
)

var queue = list.New()
var m = make(map[string]string)

/*
	Cache struct
*/
type Cache struct {
	capacity int
}

/*
	CacheItem Struct
*/
type CacheItem struct {
	Name  string
	Value string
}

// Move the list item to front
func moveListElement(k string) {
	for e := queue.Front(); e != nil; e = e.Next() {
		if e.Value.(CacheItem).Name == k {
			queue.MoveToFront(e)
			break
		}
	}
}

func (cache *Cache) print() {

	fmt.Println("Printing Queue Items")
	// Iterate through list and print its contents.
	for e := queue.Front(); e != nil; e = e.Next() {
		fmt.Println(e.Value.(CacheItem).Name)
	}
}

func (cache *Cache) set(key string, val string) string {
	//Search the key in map
	_, found := m[key]

	if !found {
		//Check the capacity
		if len(m) == cache.capacity { // Time to evict
			// Get the least use item from the queue
			e := queue.Back()
			queue.Remove(e) // Dequeue
			keyName := e.Value.(CacheItem).Name
			// Delete from the map
			delete(m, keyName)
		} else {
			//There is still some room
			item := CacheItem{Name: key, Value: val}
			queue.PushFront(item)
			m[key] = val
		}
	}
	return "1"
}

func (cache *Cache) get(k string) (string, error) {
	//Search the key in map
	v, found := m[k]
	if found {
		v := m[k]
		//fmt.Println(v)
		moveListElement(v)
		return v, nil
	}
	v = "-1"
	return v, errors.New("Key not found")
}

and…

package main

import (
	"bufio"
	"flag"
	"fmt"
	"net"
	"regexp"
	"strings"
)

/*
	This function will process the command and execute relevant LRU methods
*/
func processCommand(c string, gc Cache) string {
	var result string
	var er error
	var pattern = ""

	if strings.Index(strings.ToLower(c), "get") > -1 {
		pattern = `(?i)GET\s+(\w+)`
	} else if strings.Index(strings.ToLower(c), "set") > -1 {
		pattern = `(?i)SET\s(\w+)\s(\w+)`
	}
	// TODO: Based on results call Cache Methods

	var re = regexp.MustCompile(pattern)
	matches := re.FindAllStringSubmatch(c, -1)

	if len(matches) > 0 {
		// For GET command
		if len(matches[0]) == 2 {
			key := matches[0][1]
			result, er = gc.get(key)
			if er != nil {
				result = er.Error()
			}

		} else if len(matches[0]) == 3 {
			key := matches[0][1]
			val := matches[0][2]
			result = gc.set(key, val)
			//gc.print()
		}
	}
	return result
}

func validateCommand(cmd string) bool {
	var msg bool
	//Split the command to make sure it is not more than 2 or 3
	cmdArray := strings.Split(cmd, " ")

	if len(cmdArray) > 3 {
		msg = false

	} else if (len(cmdArray) == 2) && (strings.TrimSpace(strings.ToLower(cmdArray[0])) != "get") {
		msg = false

	} else if len(cmdArray) == 3 && strings.ToLower(cmdArray[0]) != "set" {
		msg = false

	} else if len(cmdArray) == 3 && strings.ToLower(cmdArray[0]) == "set" {
		msg = true

	} else if len(cmdArray) == 2 && strings.ToLower(cmdArray[0]) == "get" {
		msg = true

	}

	return msg
}

func handleConnection(c net.Conn, gc Cache) {
	fmt.Printf("Serving %s\n", c.RemoteAddr().String())

	for {
		netData, err := bufio.NewReader(c).ReadString('\n')
		if err != nil {
			fmt.Println(err)
			return
		}

		temp := strings.TrimSpace(string(netData))
		if validateCommand(temp) {
			outcome := processCommand(temp, gc)
			c.Write([]byte(string(outcome + "\n")))

		} else {
			c.Write([]byte(string("Invalid Command\n")))
		}
		//fmt.Println(z)

		if temp == "\\q" {
			break
		}
	}
	c.Close()
}

func main() {

	portArg := flag.String("port", "9000", "GoCached Server Port")
	capacityArg := flag.Int("capacity", 5, "Capacity of the cache")
	flag.Parse()

	PORT := ":" + *portArg
	fmt.Printf("Launching server at port %s with the capacity %d \n", *portArg, *capacityArg)
	//os.Exit(0)
	var gCache = Cache{*capacityArg}

	// listen on all interfaces
	connection, err := net.Listen("tcp4", PORT)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer connection.Close()

	for {
		c, err := connection.Accept()
		if err != nil {
			fmt.Println(err)
			return
		}
		go handleConnection(c, gCache)
	}
}

lru.go consist of main implementation while gocached.go calls the stuff from lru.go along with initialization a socket server.

The code is pushed on Github. Some kind guy did a code review and came up with his own professional implementation which I merged in master.(The beautify of Opensource) therefore I dumped the original crappy code here. You are free to fork and play/improve it.

I am still a newbie but definitely, a great journey to learn something new, especially when we all are locked down.

 

If you like this post then you should subscribe to my blog for future updates.

* indicates required