Lansana Camara
Lansana Camara

Reputation: 9873

Golang reading from a file - is it safe from locking?

I have a function that will be called on every single HTTP GET request. The function reads a file, does some stuff to the contents of that file, and returns a slice of bytes of those contents. That slice of bytes of then written as the response body to the HTTP response writer.

Do I need to use a mutex for any of the steps in this function to prevent locking in the event of multiple HTTP requests trying to read the same file? And if so, would a simple RWMutex locking the reading of the file suffice, since I am not actually writing to it but am creating a copy of its contents?

Here is the function:

// prepareIndex will grab index.html and add a nonce to the script tags for the CSP header compliance.
func prepareIndex(nonce string) []byte {
    // Load index.html.
    file, err := os.Open("./client/dist/index.html")
    if err != nil {
        log.Fatal(err)
    }

    // Convert to goquery document.
    doc, err := goquery.NewDocumentFromReader(file)
    if err != nil {
        fmt.Println(err)
    }

    // Find all script tags and set nonce.
    doc.Find("body > script").SetAttr("nonce", nonce)

    // Grab the HTML string.
    html, err := doc.Html()
    if err != nil {
        fmt.Println(err)
    }

    return []byte(html)
}

I also thought about just loading the file once when main starts, but I was having a problem where only the first request could see the data and the subsequent requests saw nothing. Probably an error in the way I was reading the file. But I actually prefer my current approach because if there are any changes to index.html, I want them to be persisted to the user immediately without having to restart the executable.

Upvotes: 0

Views: 2918

Answers (2)

yazgazan
yazgazan

Reputation: 378

Using RWMutex won't protect you from the file being modified by another program. The best option here would be to load your file in a []byte at startup, and instantiate "bytes".Buffer whenever you use goquery.NewDocumentFromReader. In order for the changes to be propagated to the user, you can use fsnotify[1] to detect changes to your file, and update your cached file ([]byte) when necessary (you will need RWMutex for that operation).

For example:

type CachedFile struct {
    sync.RWMutex
    FileName string
    Content  []byte
    watcher  *fsnotify.Watcher
}

func (c *CachedFile) Buffer() *bytes.Buffer {
    c.RLock()
    defer c.RUnlock()
    return bytes.NewBuffer(c.Content)
}

func (c *CachedFile) Load() error {
    c.Lock()
    content, err := ioutil.ReadAll(c.FileName)
    if err != nil {
        c.Unlock()
        return err
    }
    c.Content = content
    c.Unlock()
}

func (c *CachedFile) Watch() error {
    var err error

    c.watcher, err = fsnotify.NewWatcher()
    if err != nil {
        return err
    }

    go func() {
        for ev := range c.watcher.Events {
            if ev.Op != fsnotify.Write {
                continue
            }
            err := c.Load()
            if err != nil {
                log.Printf("loading %q: %s", c.FileName, err)
            }
        }
    }()

    err = c.watcher.Add(c.FileName)
    if err != nil {
        c.watcher.Close()
        return err
    }

    return nil
}

func (c *CachedFile) Close() error {
    return c.watcher.Close()
}

[1] https://godoc.org/github.com/fsnotify/fsnotify

Upvotes: 2

Drathier
Drathier

Reputation: 14539

If you're modifying the file, you need a mutex. RWMutex should work fine. It looks like you're just reading it, and in that case you should not see any locking behavior or corruption.

The reason you didn't get any data the second time you read from the same file handle is that you're already at the end of the file when you start reading from it the second time. You need to seek back to offset 0 if you want to read the contents again.

Upvotes: 1

Related Questions