这个Go代码线程安全吗?还是需要互斥锁?

Suppose I have the following function, doWork, that starts some work in a goroutine and returns a Result to check for completion and error:

func doWork() *Result {
    r := Result{doneCh: make(chan struct{})}
    go func() {
        var err error
        defer func() {
            r.err = err
            close(r.doneCh)
        }()
        // do some work
    }()
    return &r
}

where Result is the following struct:

type Result struct {
    doneCh      chan struct{}
    err         error
}
// doneCh returns a closed chan when the work is done.
func (r *Result) Done() <-chan struct{} {
    return r.doneCh
}
// Err returns a non-nil err if the work failed.
// Don't call Err until Done returns a closed chan.
func (r *Result) Err() error {
    return r.err
}

is this code thread safe if I set err before closing doneCh:

defer func() {
    r.err = err
    close(r.doneCh)
}()

or is the compiler free to order the r.err = err and close(r.doneCh) instructions as it likes, in which case I'd need a mutex to prevent concurrent read/writes on error.

The compiler may not reorder the assignment and close statement, so you do not need a mutex if callers are well-behaved and do as instructed by your docs.

This is explained in The Go Memory Model, Channel Communication.

It is thread-safe only if your comments are obeyed and Err() is never called until a read from Done() returns.

You could simply make Err() blocking though by re-implementing it as:

func (r *Result) Err() error {
    <-r.doneCh
    return r.err
}

Which would guarantee that Err() only returns after done is complete. Given that err will be nil until the work errors, you have no way of telling if Err() is returning successfully because work was finished or because it hasn't completed or errored yet unless you block on Done() first, in which case why not just make Err() blocking?

Have you tried using chan error and testing if the channel is opened or closed on reception?

package main

import (
    "errors"
    "fmt"
)

func delegate(work func(ch chan error)) {
    ch := make(chan error)

    go work(ch)

    for {
        err, opened := <- ch
        if !opened {
            break
        }
        // Handle errors
        fmt.Println(err)
    }
}

func main() {
    // Example: error
    delegate(func(ch chan error) {
        defer close(ch)
        // Do some work
        fmt.Println("Something went wrong.")
        ch <- errors.New("Eyyyyy")
    })

    // Example: success
    delegate(func(ch chan error) {
        defer close(ch)
        // Do some work
        fmt.Println("Everything went fine.")
    })

    // Example: error
    delegate(func(ch chan error) {
        defer close(ch)
        // Do some work
        fmt.Println("Something went wrong more than once.")
        ch <- errors.New("Eyyyyy 1")
        ch <- errors.New("Eyyyyy 2")
        ch <- errors.New("Eyyyyy 3")
    })
}