Queue

Implementing a Simple Queue in Go: A Step-by-Step Guide

When working with data structures in Go, understanding how to implement a basic queue is a crucial skill. A queue operates on a First In, First Out (FIFO) principle, meaning the first element added is the first one to be removed. In this post, we’ll walk through how to implement and use a simple queue in Go, covering enqueue (adding elements), dequeue (removing elements), and handling edge cases like attempting to dequeue from an empty queue.

What is a Queue?

A queue is a linear data structure that supports two primary operations:

  • Enqueue: Add an item to the end of the queue.
  • Dequeue: Remove an item from the front of the queue.

This kind of data structure is useful in many real-world scenarios, such as task scheduling, breadth-first search algorithms, and handling asynchronous jobs.

Go’s Built-in Slice: Perfect for a Queue

In Go, we can leverage the built-in slice data structure to create a queue. Slices provide dynamic resizing, allowing us to add or remove elements efficiently.

Golang code for Queue

Here’s the final code that implements a basic queue with enqueue, dequeue, and empty-check functionality:

package main

import (
	"errors"
	"fmt"
)

type queue struct {
	q []int
}

func (mq *queue) enQueue(item int) {
	mq.q = append(mq.q, item)
	return
}

func (mq *queue) deQueue() (item int, err error) {
	if mq.isEmpty() {
		return -1, errors.New("Queue is empty")
	}
	item = mq.q[0]
	mq.q = mq.q[1:]
	return item, nil
}

func (mq *queue) isEmpty() bool {
	return len(mq.q) == 0
}

func createQueue() (mq *queue) {
	return &queue{q: []int{}}
}

func main() {

	mq := createQueue()
	// Enqueuing multiple items one after another
	mq.enQueue(1)
	fmt.Printf("After enqueue 1 isEmpty()=%t, len=%d, queue=%#v\n", mq.isEmpty(), len(mq.q), mq.q)
	mq.enQueue(2)
	fmt.Printf("After enqueue 2 isEmpty()=%t, len=%d, queue=%#v\n", mq.isEmpty(), len(mq.q), mq.q)
	mq.enQueue(3)
	fmt.Printf("After enqueue 3 isEmpty()=%t, len=%d, queue=%#v\n", mq.isEmpty(), len(mq.q), mq.q)

	// Dequeue operations
	var item int
	var err error

	item, err = mq.deQueue()
	if err != nil {
		fmt.Printf("Error during deQueue operation: %s\n", err)
	} else {
		fmt.Printf("After Dequeue: item=%d, isEmpty()=%t, len=%d, queue=%#v\n", item, mq.isEmpty(), len(mq.q), mq.q)
	}

	item, err = mq.deQueue()
	if err != nil {
		fmt.Printf("Error during dequeue: %s\n", err)
	} else {
		fmt.Printf("After Dequeue: item=%d, isEmpty()=%t, len=%d, queue=%#v\n", item, mq.isEmpty(), len(mq.q), mq.q)
	}

	item, err = mq.deQueue()
	if err != nil {
		fmt.Printf("Error during dequeue: %s\n", err)
	} else {
		fmt.Printf("After Dequeue: item=%d, isEmpty()=%t, len=%d, queue=%#v\n", item, mq.isEmpty(), len(mq.q), mq.q)
	}

	item, err = mq.deQueue()
	if err != nil {
		fmt.Printf("Error during dequeue: %s\n", err)
	} else {
		fmt.Printf("After Dequeue: item=%d, isEmpty()=%t, len=%d, queue=%#v\n", item, mq.isEmpty(), len(mq.q), mq.q)
	}
}

Example Output

Running this code would produce the following output

After enqueue 1 isEmpty()=false, len=1, queue=[]int{1}
After enqueue 2 isEmpty()=false, len=2, queue=[]int{1, 2}
After enqueue 3 isEmpty()=false, len=3, queue=[]int{1, 2, 3}
After Dequeue: item=1, isEmpty()=false, len=2, queue=[]int{2, 3}
After Dequeue: item=2, isEmpty()=false, len=1, queue=[]int{3}
After Dequeue: item=3, isEmpty()=true, len=0, queue=[]int{}
Error during dequeue: Queue is empty

Program exited.

How It Works

1. Enqueue Operation

The enQueue function appends a new item to the end of the queue (which is a slice in Go). The slice’s dynamic nature makes it easy to add new elements with minimal overhead.

func (mq *queue) enQueue(item int) {
	mq.q = append(mq.q, item)
	return
}

2. Dequeue Operation

The dequeue function checks whether the queue is empty. If not, it removes the first item by slicing off the first element of the slice. If the queue is empty, it returns an error.

func (mq *queue) deQueue() (item int, err error) {
	if mq.isEmpty() {
		return -1, errors.New("Queue is empty")
	}
	item = mq.q[0]
	mq.q = mq.q[1:]
	return item, nil
}

By returning a tuple of the item, the modified queue, and an error, we handle both the success and failure of the dequeue operation gracefully.

3. isEmpty Function

The isEmpty function simply checks if the length of the queue slice is zero, returning a boolean.

func (mq *queue) isEmpty() bool {
	return len(mq.q) == 0
}

Edge Case: Empty Queue

One of the most important aspects of this implementation is handling the scenario where you attempt to dequeue from an empty queue. In the dequeue function, this case is checked first, and an error is returned to prevent unexpected behaviour.

if mq.isEmpty() {
    return -1, errors.New("Queue is empty")
}

As you can see:

  • We successfully enqueue three elements.
  • Each dequeue operation removes the front item from the queue.
  • When the queue is empty, attempting another dequeue triggers an error.

Conclusion

By leveraging Go’s slice operations, we’ve built a simple yet functional queue that handles edge cases and manages elements efficiently. The key takeaway is the importance of managing empty queue states and returning appropriate errors to avoid unexpected crashes.

This implementation can easily be extended for more advanced use cases, such as limiting the queue size or prioritizing elements.

Whether you’re preparing for coding interviews or building a real-world system, understanding the inner workings of simple data structures like queues will give you a solid foundation for more complex algorithms and systems.

Visit https: https://codeandalgo.com for more such contents

Leave a Reply

Your email address will not be published. Required fields are marked *