Golang program to implement a circular buffer


Circular buffers, a data structure that efficiently manages and cycles through data, provide a valuable solution. In this article, we will implement a circular buffer in go, showcasing its utility and practicality. The examples below demonstrate the operations like initialization, insertion and demonstration of a circular buffer.

Explanation

A circular buffer (also circular queue or ring buffer) is a fixed-size buffer operating as if the end and the beginning were connected, forming a loop. This ingenious data structure efficiently manages a continuous flow of data, making it an ideal choice for applications requiring data cycling and reuse.

This is the representation of a circular buffer.

+---+---+---+---+---+
| 0 | 1 | 2 | 3 | 4 |
+---+---+---+---+---+
   ^                ^
   |                |
  Head            Tail

Circular buffer has a fixed size, head is the pointer from where the next element is read, and tail is the pointer indicating the position from where the last element is added. The elements are added and removed so the head and tail pointer move around the buffer.

Syntax

func (c *CircularBuffer) push(data interface{})

The syntax represents a method called push is a method defined to add elements to the circular buffer, taking one parameter of type interface.

Algorithm

  • Start by initializing the buffer with a fixed size.

  • Maintain two pointers - readIndex and writeIndex.

  • Implement functions to write data, advancing writeIndex cyclically.

  • Implement functions to read data, advancing readIndex cyclically.

  • Handle buffer overflow and underflow conditions effectively.

Example 1

In this example, we see how to implement a circular buffer in go by increasing the write pointer and wrapping it around the bufer size. A strut named CircularBuffer is defined as contaning a slice buffer for storing data, a size value representing the buffer's capacity, and a writePointer for keeping trck of the next loction to write data. The size of a new circular bufer may be set using the NewCircularBuffer(size int) *CircularBuffer function. To prevent overflows, the push(data interface) function repeatedly increments the writePointer.

package main
import "fmt"
type CircularBuffer struct {
    buffer []interface{}
    size int
	writePointer int
}
func NewCircularBuffer(size int) *CircularBuffer {
    return &CircularBuffer{
    	buffer: make([]interface{}, size),
    	size: size,
    	writePointer: 0,
	}
}
func (c *CircularBuffer) push(data interface{}) {
	c.buffer[c.writePointer] = data
	c.writePointer = (c.writePointer + 1) % c.size
}
func main() {
	cb := NewCircularBuffer(5)
	cb.push(10)
	fmt.Println("Circular Buffer Contents:", cb.buffer)
	cb.push(20)
	fmt.Println("Circular Buffer Contents:", cb.buffer)
	cb.push(30)
	fmt.Println("Circular Buffer Contents:", cb.buffer)
	cb.push(40)
	fmt.Println("Circular Buffer Contents:", cb.buffer)
	cb.push(50)
	fmt.Println("Circular Buffer Contents:", cb.buffer)
	cb.push(60)
    fmt.Println("Circular Buffer Contents:", cb.buffer)
}

Output

Circular Buffer Contents: [10 <nil> <nil> <nil> <nil>]
Circular Buffer Contents: [10 20 <nil> <nil> <nil>]
Circular Buffer Contents: [10 20 30 <nil> <nil>]
Circular Buffer Contents: [10 20 30 40 <nil>]
Circular Buffer Contents: [10 20 30 40 50]
Circular Buffer Contents: [60 20 30 40 50]

Example 2

In this example, when pushing an element, the write pointer is incremented and the read pointer and count are updated as necessary since the buffer is full. The properties buffer, size, readPointer, writePointer, and count are used in conjunction with a CircularBuffer struct. The buffer is initialized by the NewCircularBuffer function, and further data entry and overflow are managed by the push method.

package main
import "fmt"
type CircularBuffer struct {
	buffer []interface{}
	size int
	readPointer  int
	writePointer int
	count int
}
func NewCircularBuffer(size int) *CircularBuffer {
	return &CircularBuffer{
    	buffer:   	make([]interface{}, size),
    	size:     	size,
    	readPointer:  0,
    	writePointer: 0,
        count:    	0,
	}
}
func (c *CircularBuffer) push(data interface{}) {
	if c.count == c.size {
    	c.readPointer = (c.readPointer + 1) % c.size
    } else {
    	c.count++
	}
	c.buffer[c.writePointer] = data
	c.writePointer = (c.writePointer + 1) % c.size
}
func main() {
	cb := NewCircularBuffer(5)
	cb.push(10)
	fmt.Println("Circular Buffer Contents:", cb.buffer) 
    cb.push(20)
    fmt.Println("Circular Buffer Contents:", cb.buffer)
	cb.push(30)
	fmt.Println("Circular Buffer Contents:", cb.buffer) 
    cb.push(40)
	fmt.Println("Circular Buffer Contents:", cb.buffer)
	cb.push(50)
	fmt.Println("Circular Buffer Contents:", cb.buffer) 
    cb.push(60)
    fmt.Println("Circular Buffer Contents:", cb.buffer) 
}

Output

Circular Buffer Contents: [10 <nil> <nil> <nil> <nil>]
Circular Buffer Contents: [10 20 <nil> <nil> <nil>]
Circular Buffer Contents: [10 20 30 <nil> <nil>]
Circular Buffer Contents: [10 20 30 40 <nil>]
Circular Buffer Contents: [10 20 30 40 50]
Circular Buffer Contents: [60 20 30 40 50]

Real Life Implementation

  • Audio Processing Systems: Circular buffers are often employed in audio processing systems, particularly for real-time audio playing and recording. Audio samples are reliably and continuously delivered into and retrieved from circular buffers in these systems. This allows for more efficient data storage and retrieval, as well as the seamless execution of repeated processes and real-time acoustic data modification.

  • Printer Spooling: Circular buffers are used in printer spooling systems. It is typical practice in the printing industry to hold many print jobs in a circular buffer. By processing print jobs in the order in which they were received, the printer is able to efficiently manage the printing process and avoid the loss of data.

Conclusion

Circular buffers specialize in managing data streams in constrained memory environments by preserving read/write pointers and accommodating overflow/underflow conditions. In this article, we looked at 2 ways to implement a circular buffer in go. The first approach, utilizing slices, is a straightforward and fast way to handle a circular buffer since it makes use of Golang's native capabilities with minimum overhead. The second approach uses a data structure to provide finer-grained access to the buffer's contents and enable features like fullness checks. We Have pushed values to the circular buffer content in both the examples.

Updated on: 18-Oct-2023

192 Views

Kickstart Your Career

Get certified by completing the course

Get Started
Advertisements