Locks and buffer in Go

In Go, locking manages concurrent access to shared resources, while buffered channels enable non-blocking communication up to a set capacity.

user
Roshan Chaudhary

Wed, Jun 11, 2025

3 min read

thumbnail

Locks

Locks are used to prevent two or more processes from using the same resources or enter a critical section, which helps in concurrency to maintain a consistent state in a distributed environment. It also helps to prevent deadlocks or race conditions.

In Go , sync.Mutex is used as a mutual exclusion lock, which allows only one goroutine to access a piece of code or data(resources) at a time.

Let’s import sync and define a type:

package main
 
import (
	"fmt"
	"sync"
)
 
type syncedBuffer struct {
	lock   sync.Mutex
}

Buffer

Meanwhile buffer is used like a dynamic, resizable array of bytes which helps to store bytes or strings. It helps in efficient memory management while handling byte streams or constructing strings. It dynamically allocates memory for what you store and also may reserve extra capacity to avoid frequent resizing.

We can import it as bytes and also define it in the type:

package main
 
import (
	"bytes"
	"fmt"
	"sync"
)
 
type syncedBuffer struct {
	lock   sync.Mutex
	buffer bytes.Buffer
}

Let’s define the main function and the variables s of struct syncedBuffer and wg(waitgroup) :

func main() {
	var s syncedBuffer
	var wg sync.WaitGroup
 
	wg.Add(2)
	
	
			//logic code here
			
			
	wg.Wait()
 
}

wg is a waitgroup variable that runs asynchronously, which prevents the main function from completing before the goroutines. Here wg.Add(2) adds two goroutines in the waitgroup and wg.wait() waits until these goroutines are processed before moving further.

Now let’s add the go routines:

package main
 
import (
	"bytes"
	"fmt"
	"sync"
	"time"
)
 
type syncedBuffer struct {
	lock   sync.Mutex
	buffer bytes.Buffer
}
 
// locks and buffer in Go
 
func main() {
	var s syncedBuffer
	var wg sync.WaitGroup
 
	wg.Add(2)
 
	// first goRoutine: holds lock longer
	go func() {
		defer wg.Done()
		s.lock.Lock()
		fmt.Println("[goRoutien 1] acquired lock  , writing")
		s.buffer.WriteString("Hello from goRoutine 1!\n")
		time.Sleep(5 * time.Second) // Simulate long operation
		s.lock.Unlock()
		fmt.Println("[goRoutien 1] released lock")
	}()
 
	// delay second goRoutine slightly so it hits the lock while the first is holding the lock
	go func() {
		defer wg.Done()
		time.Sleep(1000 * time.Millisecond)
		fmt.Println("[goRoutien 2] trying to acquire lock...")
		s.lock.Lock()
		fmt.Println("[goRoutine 2] acquired lock , writing...")
		s.buffer.WriteString("hello from go routine 2 \n")
		time.Sleep(2 * time.Second)
		s.lock.Unlock()
		fmt.Println("[go routine 2 ] released lock")
	}()
 
	wg.Wait()
	time.Sleep(3 * time.Second)
	fmt.Println("final buffer content: \n" + s.buffer.String())
}
  • The first goroutine acquires the lock and starts writing in the buffer. It further holds the lock for 5s
  • Then the second goroutine tries to acquire the lock after a second, but still the lock is acquired by the first goroutine, so the 2nd has to wait.
  • After the lock is released by 1st goroutine after 5 seconds, the 2nd goroutine starts writing in the buffer.
  • After 2nd goroutine releases the lock, the content in the buffer is printed in the console.

output:

[goRoutien 1] acquired lock  , writing
[goRoutien 2] trying to acquire lock...
[goRoutien 1] released lock
[goRoutine 2] acquired lock , writing...
[go routine 2 ] released lock
final buffer content:   
Hello from goRoutine 1! 
hello from go routine 2

Thanks for reading till the end. The minions are proud of you! 😊

giphy.gif