What is a WaitGroup

WaitGroup when we were in concurrent introduced before, it is a form of concurrent control, it’s this way is to control more than one goroutine completed at the same time.

func main(a) {
	var wg sync.WaitGroup

	wg.Add(2)
	go func(a) {
		time.Sleep(2*time.Second)
		fmt.Println("Completed by the 1st.")
		wg.Done()
	}()
	go func(a) {
		time.Sleep(2*time.Second)
		fmt.Println("Completed by the 2nd.")
		wg.Done()
	}()
	wg.Wait()
	fmt.Println("All right, everybody's done. Let's go.")}Copy the code

A very simple example is that both goroutines must be completed at the same time to be considered complete. The goroutines must wait for the others to be completed.

This is a way of controlling concurrency, and this is especially true when you have multiple Goroutines working together on something, because each goroutine is doing a part of it, and it’s not done until all of the Goroutines are done, and this is waiting.

In a real business, we might have a scenario where we would actively notify a goroutine of closure. Let’s say we start a background Goroutine that’s always doing something, like monitoring, and now we don’t need it anymore, so we need to tell that monitor goroutine to stop, otherwise it’ll just keep running and it’ll leak.

Chan notice

We all know that once a Goroutine starts, we can’t control it. Most of the time, we just wait for it to end. What if the goroutine is a background Goroutine that doesn’t end on its own? Things like monitoring, things like that, will always be up and running.

In this case, the foolproof solution is the global variable, which is modified elsewhere to complete the termination notification, and then the background Goroutine keeps checking the variable and terminates itself if it finds that the notification is closed.

This is fine, but first we need to ensure that this variable is safe in multiple threads. For this reason, there is a better way: chan + select.

func main(a) {
	stop := make(chan bool)

	go func(a) {
		for {
			select {
			case <-stop:
				fmt.Println("Surveillance is off, it's stopped...")
				return
			default:
				fmt.Println("Goroutine monitoring...")
				time.Sleep(2 * time.Second)
			}
		}
	}()

	time.Sleep(10 * time.Second)
	fmt.Println("That's it. Tell the surveillance to stop.")
	stop<- true
	// To check whether the monitor has stopped, if there is no monitor output, it is stopped
	time.Sleep(5 * time.Second)

}
Copy the code

In this example we define a stop chan and tell him to end the background goroutine. Implementation is also very simple, in the background of goroutine, use select to determine whether stop can receive a value, if it can receive, it means that you can exit the stop; If no, the system performs the monitoring logic in default and continues monitoring until a stop notification is received.

Given the logic above, we can send values to Stop Chan in other goroutine types, in this case in main Goroutine, and control the monitoring of the goroutine to end.

After sending stop< -true, I use time.sleep (5 * time.second) to deliberately pause for 5 seconds to check if we finished monitoring goroutine successfully. If successful, there will be no more Goroutine monitoring… Output of; If not, monitor Goroutine will continue to print goroutine monitor… The output.

Chan +select is an elegant way to end a goroutine, but it has limitations. What if there are too many Goroutines that need to be closed? What if these Goroutines spawn more goroutines? What about endless layers of Goroutine? This is very complicated, and even if we define a lot of Chans, it would be difficult to solve this problem because goroutine’s chain of relationships makes this scenario very complicated.

I met the Context

The above scenario exists, such as a network Request Request, each Request needs to open a Goroutine to do something, and these Goroutines may open other Goroutines. So we need a way to track goroutines in order to control them, and this is the Context that the Go language gives us. It’s very appropriate to call a Context, which is the Context of a Goroutine.

Let’s rewrite the above example using the Go Context.

func main(a) {
	ctx, cancel := context.WithCancel(context.Background())
	go func(ctx context.Context) {
		for {
			select {
			case <-ctx.Done():
				fmt.Println("Surveillance is off, it's stopped...")
				return
			default:
				fmt.Println("Goroutine monitoring...")
				time.Sleep(2 * time.Second)
			}
		}
	}(ctx)

	time.Sleep(10 * time.Second)
	fmt.Println("That's it. Tell the surveillance to stop.")
	cancel()
	// To check whether the monitor has stopped, if there is no monitor output, it is stopped
	time.Sleep(5 * time.Second)

}
Copy the code

It is easy to rewrite the chan stop to Context, and use Context to track goroutine for control, such as termination, etc.

Context.background () returns an empty context, which is used at the root of the entire context tree. We then use context.withcancel (parent) to create a cancelable child context and pass it to the Goroutine as an argument so that the child context can be used to track the Goroutine.

In goroutine, the select call < -ctx.done () is used to determine whether to terminate the goroutine. If the value is received, the end of the Goroutine is returned. If not, monitoring continues.

So how do you send the end command? This is the cancel function in our example, which is returned when we call context.withcancel (parent) to generate the child context. The second return value is the cancel function, which is of type CancelFunc. We call it to issue a cancel instruction, and then our monitor Goroutine will get a signal and return to end.

Context controls multiple Goroutines

Using the Context to control a goroutine example, as shown above, is very simple. Now let’s look at controlling multiple Goroutines, which is actually quite simple.

func main(a) {
	ctx, cancel := context.WithCancel(context.Background())
	go watch(ctx,"[Monitoring 1]")
	go watch(ctx,"[Monitoring 2]")
	go watch(ctx,"[Monitoring 3]")

	time.Sleep(10 * time.Second)
	fmt.Println("That's it. Tell the surveillance to stop.")
	cancel()
	// To check whether the monitor has stopped, if there is no monitor output, it is stopped
	time.Sleep(5 * time.Second)
}

func watch(ctx context.Context, name string) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println(name,"Surveillance is off, it's stopped...")
			return
		default:
			fmt.Println(name,"Goroutine monitoring...")
			time.Sleep(2 * time.Second)
		}
	}
}
Copy the code

In the example, three monitoring goroutines are started for continuous monitoring, each of which is tracked using Context, and all three goroutines are terminated when we cancel with the cancel function. So that’s the control of the Context, it’s like a controller, and when you press a switch, all of the other contexts that are based on that Context or derived from that Context are notified, and then you can clean up, and finally release the Goroutine, and that’s an elegant solution to the problem of not being able to control the Goroutine when it starts.

The Context interface

The interface of the Context is pretty neat, so let’s look at the methods of the interface.

type Context interface {
	Deadline() (deadline time.Time, ok bool)

	Done() <-chan struct{}

	Err() error

	Value(key interface{}) interface{}}Copy the code

There are four methods in this interface, and it is important to understand what these methods mean so that we can use them better.

The Deadline method gets the set Deadline. The first callback is the Deadline, at which point the Context will automatically initiate a cancellation request. The second return value, ok==false, indicates that no cutoff time is set, and that cancelling is required.

The Done method returns a read-only chan of type struct{}. In goroutine, if the chan can be read from the parent context, it means that the parent context has issued a cancellation request. Then exit Goroutine, freeing resources.

The Err method returns the cause of the cancellation error because the Context was canceled.

The Value method gets the Value bound to the Context, which is a key-value pair, so you need a Key to get the corresponding Value, which is generally thread-safe.

If the Context is cancelled, we can get a closed chan. The closed chan can be read, so if the Context is read, it means that the Context is cancelled.

func Stream(ctx context.Context, out chan<- Value) error {
  	for {
  		v, err := DoSomething(ctx)
  		iferr ! =nil {
  			return err
  		}
  		select {
  		case <-ctx.Done():
  			return ctx.Err()
  		case out <- v:
  		}
  	}
  }
Copy the code

The Context interface does not need to be implemented. The Go built-in has already implemented two of them for us. We start our code with these two built-in partent contexts as the top layer and derive more sub-contexts.

var (
	background = new(emptyCtx)
	todo       = new(emptyCtx)
)

func Background() Context {
	return background
}

func TODO() Context {
	return todo
}
Copy the code

One is Background, which is used in main, initialization, and test code, as the top-level Context of the tree, the root Context.

One is TODO, which we don’t know yet, so we can use this if we don’t know what Context to use.

Both of them are essentially emptyCtx struct types, which are non-cancelable, have no expiration date, and carry no values.

type emptyCtx int

func (*emptyCtx) Deadline(a) (deadline time.Time, ok bool) {
	return
}

func (*emptyCtx) Done(a) <-chan struct{} {
	return nil
}

func (*emptyCtx) Err(a) error {
	return nil
}

func (*emptyCtx) Value(key interface{}) interface{} {
	return nil
}
Copy the code

So that’s how emptyCtx implements the Context interface, and as you can see, these methods don’t do anything, they return nil or zero.

Context descends

So with the root Context above, how do you derive more child contexts? This depends on the With series of functions provided by the context package.

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context
Copy the code

Each of the four With functions takes a partent parameter, which is the parent Context, and we create the meaning of the child Context based on the parent Context, which can be understood as an inheritance of the child Context from the parent Context, or as a derivation from the parent Context.

Using these functions, you create a Context tree, where each node can have as many children as you want, and the node hierarchy can be as many.

The WithCancel function, which passes a parent Context as an argument, returns a child Context, and a cancel function to cancel the Context. The WithDeadline function, which is similar to WithCancel, it’s going to pass in an extra deadline, which means that at this point in time, the Context is going to cancel automatically, but we can also wait until that point, we can cancel ahead of time with the cancel function.

So WithTimeout is basically the same thing as WithDeadline, so this means automatically cancels out, how much time after the Context is automatically cancelled.

The WithValue function has nothing to do with canceling the Context. It generates a Context that is bound to a key-value pair of data that can be accessed through the context. Value method, which we’ll talk about later.

You may have noticed that each of the first three functions returns a CancelFunc, a function type that is very simple to define.

type CancelFunc func(a)
Copy the code

That’s the type of cancel function, which cancels a Context, and all the contexts under that node’s Context, no matter how many levels there are.

WithValue passes metadata

We can also pass some necessary metadata through the Context, which will be attached to the Context for use.

var key string="name"

func main(a) {
	ctx, cancel := context.WithCancel(context.Background())
	/ / added value
	valueCtx:=context.WithValue(ctx,key,"[Monitoring 1]")
	go watch(valueCtx)
	time.Sleep(10 * time.Second)
	fmt.Println("That's it. Tell the surveillance to stop.")
	cancel()
	// To check whether the monitor has stopped, if there is no monitor output, it is stopped
	time.Sleep(5 * time.Second)
}

func watch(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			/ / remove the value
			fmt.Println(ctx.Value(key),"Surveillance is off, it's stopped...")
			return
		default:
			/ / remove the value
			fmt.Println(ctx.Value(key),"Goroutine monitoring...")
			time.Sleep(2 * time.Second)
		}
	}
}
Copy the code

In the previous example, we passed the value of name to the monitor function by passing an argument. In this case, we’re doing the same thing, but using the Value of the Context.

We can append a k-V key-value pair using context.WithValue, where the Key must be equivalent, that is, comparable; Value specifies a thread-safe Value.

This generates a new Context with the key-value pair that, when used, reads ctx.value (key) through the Value method.

Remember, pass WithValue, usually a required value, don’t pass everything.

Context Usage rules

  1. Don’t put the Context in a structure, pass it as a parameter
  2. Function methods that take Context as an argument should take Context as their first argument first.
  3. When you pass Context to a function method, don’t pass nil, if you don’t know what to pass, use context.todo
  4. The Value related methods of the Context should pass the necessary data, don’t use this pass for everything
  5. Context is thread-safe and can be passed safely across multiple Goroutines

Refer to the article

  • Snow ruthless blog