signal
A one-shot error that others can wait on, and a channel that costs nothing until someone needs it.
Every long-lived component ends up with the same pair of fields: an error recording why it stopped, and a channel closed when it did. Written by hand that is a mutex, a sync.Once, a nil-check on the channel and a race between the two. Signal is that pair, done once.
go
import "gitlab.internal/tuan.quang.tran/aioz-common/signal"Usage
go
type Server struct {
stopped signal.Signal
}
func (s *Server) Run(ctx context.Context) {
err := s.serve(ctx)
s.stopped.Set(err) // first caller wins; later ones are no-ops
}
func (s *Server) Wait() error {
s.stopped.Wait()
return s.stopped.Err()
}
// Or select on it alongside anything else.
select {
case <-s.stopped.Signal():
case <-ctx.Done():
}Err alone cannot tell you it fired
go
sig.Set(nil) // a clean shutdown is still a shutdown
sig.Err() // nil
sig.IsSet() // true
err, ok := sig.Get()A Signal can legitimately hold a nil error, so a nil Err() does not mean the signal is unset. Anywhere the distinction matters, use IsSet or Get. Err() != nil implies it was set, but not the other way round.
Chan
Chan is the lazy half on its own: a chan struct{} that is not allocated until the first use, for a struct that has many of them and uses few.
go
var c signal.Chan
c.Send() // allocates on demand
c.Recv()
c.Close() // idempotentAPI
Signal:Set(err) bool,Err() error,Get() (error, bool),IsSet() bool,Wait(),Signal() chan struct{}.Chan:Get() chan struct{},Make(cap uint),Send(),Recv(),Close(),Full() bool.