File Locking In Go Part I

Go is a very powerful language. It’s a general purpose language that is used in many ways. From building a scalable web application to building reliable system tools.

In this article, we will be talking about File locks in go and how to use them so you can use this article as a reference.

What Is File Lock And Why We Need it?

It can be very cumbersome to deal with shared resources that are used by multiple systems concurrently.
This is different from controlling resources used by a single application where you can use something like mutex or semaphore. We’re talking here about sharing a file and accessing it by multiple systems where there can be a race between them.

Locking:

The purpose of locking is to secure, manage shared resources, and keep consistency between the shared resources.

Types of Locks:

Shared Lock :
A shared lock is used when there are multiple operations need to access a target simultaneously; those operations will not change the target’s value, e.g., read file content or list directories. Multiple processes can access the file at the same time.
It’s usually used for reading.

Exclusive Lock:
The exclusive lock will allow the resources to be accessed exclusively, which is opposed to a shared lock. Only a single process can access a file at any point in time.
It’s usually used for writing.

Use Case

Imagine that we have multiple applications emitting their logs to a single file; we need to organize this operation from all applications, so they don’t overwrite each other logs. They wait before they start writing their logs to the file.

We will first open the file, acquire a write lock if possible and block if we can’t.

file, err := os.OpenFile("./apps.log", os.O_RDWR|os.O_CREATE, 0666)
// handle err
defer file.Close()

We opened a file with write access. Note that we create the file if it doesn’t exist.

err := syscall.Flock(int(file.Fd()), syscall.LOCK_EX)

We have two options to lock the file, we can either use LOCK_EX, which will make an “Exclusive” lock, and this means only one application can access the file at a time, and the others will be blocked and waiting for it to finish.
There is another type called syscall.LOCK_SH, it’s the “shared” lock that we referred to earlier.

Calling exclusive lock will block until we acquire a lock. If you don’t want to wait for some reason, be it you will accumulate more locks and call it after some time, or you will have another plan, in this case, you can use the non-blocking flag combined with exclusive flag “syscall.LOCK_EX|syscall.LOCK_NB”.

func main() {
   if err := lockFile("./app.log"); err != nil {
      fmt.Println(err)
      os.Exit(1)
   }
}

func lockFile(fileName string) error {
   file, err := os.OpenFile(fileName, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0666)
   if err != nil {
      return err
   }
   defer file.Close()

   if err := syscall.Flock(int(file.Fd()), syscall.LOCK_EX); err != nil {
      return err
   }
   defer syscall.Flock(int(file.Fd()), syscall.LOCK_UN)

   fmt.Println("Trying to lock again")
   file2, err := os.OpenFile(fileName, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0666)
   err = syscall.Flock(int(file2.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
   if err == nil {
      return nil
   }

   if errors.Is(err, syscall.EAGAIN) {
      fmt.Println("File is already locked")
   }
   return err
}

In the code above, we opened the app.log file and acquired an exclusive lock for it. Then we tried to acquire another exclusive lock. Since we already have a lock, the system returned “syscall.EAGAIN”, which means the resource is not available now because another process acquired it.

In Part II, we will explain a more complex use case where we will download online resources on disk and share it between application instances.

References:
flock(2)

About the author

Ahmed Magdy

Software Developer

Add Comment

Ahmed Magdy

Software Developer

Get in touch

You can reach me out through of these channels.