2

How can a file be opened such that only one process can write to it at a time in Golang?

I am new to Golang and was trying to learn it by writing something. Was attempting to create a secret storage tool which stores, say, api keys to a file; during which I came across an issue - if two instance of same program runs together (hypothetically) a race condition can occur and damage the file.

So, I was checking for a way to lock the file so that only one process can write to it at a time. Reading simultaneously is ok.


Found this answer : How to get an exclusive lock on a file in go on stackoverflow. but in the comments of the same, it's mentioned

your package uses flock() for locking on Unix so doesn't provide OS level mandatory locking


Tried os.ModeExclusive.

f, err := os.OpenFile(".secrets", os.O_RDWR|os.O_CREATE, os.ModeExclusive)

But it prevents the file from accessing the next time program runs. Can you please explain what this mode does. Not able to understand it from the docs.


Also tried:

f, err := os.OpenFile(".secrets", os.O_RDWR|os.O_CREATE, 0744)

On using the above, anyone can read/write when the file is opened.


James Z
  • 12,209
  • 10
  • 24
  • 44
Tony
  • 130
  • 1
  • 1
  • 12
  • 1
    "But it prevents the file from accessing the next time program runs." Did you remember to close the file, preferably using a `defer` statement? Also, what platform are you on? Filesystem locking semantics are quite different between different operating systems. – Thomas Aug 28 '21 at 08:18
  • 1
    In any case, locking only prevents one possible way of file corruption. Another is when your program or computer crashes partway through writing the file. A more robust solution (at least on Unix) is to write the data to a temporary file in the same directory first, close it, and then rename it to the final name. – Thomas Aug 28 '21 at 08:24
  • I am on Windows and yes.. file is defer closed – Tony Aug 28 '21 at 08:25
  • ok.. let me they that renaming method.. thank you.. – Tony Aug 28 '21 at 08:25
  • On Windows your concern about `flock` is unfounded, because `flock` is not used on Windows. – Thomas Aug 28 '21 at 08:26
  • oh ok. Thanks for that info. Can you please explain what os.ModExclusive does? – Tony Aug 28 '21 at 08:36
  • @Tony why not build a pipeline using a channel? You can have a goroutine which reads from a channel and writes to a file. Your other goroutines can just publish to a channel. – Ayush Gupta Aug 28 '21 at 12:54

2 Answers2

1

I'll try to explain the os.ModeExclusive bit of your question:

Tried os.ModeExclusive.

f, err := os.OpenFile(".secrets", os.O_RDWR|os.O_CREATE, os.ModeExclusive)

But it prevents the file from accessing the next time program runs. Can you please explain what this mode does. Not able to understand it from the docs.

OpenFile() with os.ModeExclusive basically creates a file where not even the file owner has read and write permissions on it (at least this is the behavior I observed on unix). that is why it "doesn't work" the next time when it tries to open the file again:

$ cat prog.go
package main

import (
        "fmt"
        "log"
        "os"
)

func main() {
        f, err := os.OpenFile(".secrets", os.O_RDWR|os.O_CREATE, os.ModeExclusive)
        if err != nil {
                log.Fatal(err)
        }
        defer f.Close()

        fmt.Fprintf(f, "this is my secret\n")
}
$ go build prog.go
$ ./prog
$ ls -al
total 2.1M
drwxr-xr-x 2 marco marco 4.0K Aug 28 13:53 ./
drwxr-xr-x 4 marco marco 4.0K Aug 28 13:22 ../
-rwxr-xr-x 1 marco marco 2.1M Aug 28 13:53 prog*
-rw-r--r-- 1 marco marco  230 Aug 28 13:49 prog.go
---------- 1 marco marco   17 Aug 28 13:53 .secrets    <-- HERE IT IS
$ ./prog
2021/08/28 13:54:27 open .secrets: permission denied
$ cat .secrets
cat: .secrets: Permission denied
$ chmod u+rw .secrets
$ cat .secrets
this is my secret
$

os.ModeExclusive can also be used with Chmod() of course on already existing files.

MarcoLucidi
  • 2,007
  • 1
  • 5
  • 8
  • @Tony glad to have helped you :) my answer doesn't fully answer your question though, so I'll totally understand if you want to un-accept it and wait for a more complete answer from other users. maybe leave an upvote :) – MarcoLucidi Aug 28 '21 at 12:38
  • Hei @MarcoLucidi. Thanks. Done as you mentioned. :) – Tony Aug 29 '21 at 04:27
0

The convention (at least for the standard library) is the following: No function/method is safe for concurrent use unless explicitly stated (or obvious from the context). It is not safe to write concurrently to an os.File via Write() without external synchronization.

source: https://stackoverflow.com/a/30746629/13067552

You can use RMutex from sync package to avoid data race. *sync.RWMutex

package main

import (
    "fmt"
    "os"
    "sync"
)

type MyFile struct {
    l    *sync.RWMutex
    file *os.File
}

func (f *MyFile) Write(b []byte) (int, error) {
    f.l.Lock()
    defer f.l.Unlock()

    return f.file.Write(b)
}

func (f *MyFile) Read(b []byte) (int, error) {
    f.l.RLock()
    defer f.l.RUnlock()

    return f.file.Read(b)
}

func main() {
    f, err := os.Open("tmp")
    if err != nil {
        // handle here
    }
    defer f.Close()

    myfile := &MyFile{
        l:    &sync.RWMutex{},
        file: f,
    }

    myfile.Write([]byte("secret"))

    b1 := make([]byte, 5)
    myfile.Read(b1)

    fmt.Println(string(b1))
}
twiny
  • 284
  • 5
  • 11
  • 3
    mutex locks can be used to avoid critical section access within same program; here what is needed is some method to avoid concurrent write to same file by two different processes. Thanks for your answer. – Tony Aug 28 '21 at 12:21
  • @Tony, I got you - sorry misunderstood the question. Have you tried this pkg: https://github.com/nightlyone/lockfile – twiny Aug 28 '21 at 13:36