Harder than scripting, but easier than programming in C

Viewed in Context

If you create a large number of goroutines, you have to precisely define the goroutines' life cycles. Otherwise, there will be uncontrolled growth, and resources that are not released will eventually paralyze the main program.

In Google's data centers, this problem arose with the web servers, which typically use goroutines to fetch data from various back-end services in order to fulfill user requests. If there is a delay and the web server loses patience, it has to inform all the goroutines that have been started in parallel that their services are no longer needed and that they should stop working immediately. The web server then looks to send an error message to the currently requesting web client to carry on with processing the next request.

This communication is handled by the context construct, which made its way into Go's standard library because of its importance. Using context.Background(), Listing 10 creates and initializes a channel from which any goroutine running in parallel will attempt to read permanently in a select statement. If the main program wants to drop the big snuffer on the goroutine's heads, it simply calls the context's Cancel() function. This takes down the internal channel, which in turn snaps all the listening goroutines out of their select statements at once. The routines can then quickly release their allocated resources and exit in an orderly fashion. From the main program's point of view, everything can be reliably cleaned up in a single action using a single instruction – convenience at its best.

Listing 10

ctx.go

01 package main
02
03 import (
04   "context"
05   "fmt"
06   "time"
07 )
08
09 func main() {
10   ctx, cancel := context.WithCancel(
11       context.Background())
12
13   for i := 0; i < 10; i++ {
14     bee(i, ctx)
15   }
16
17   time.Sleep(time.Second)
18   cancel()
19   fmt.Println("")
20 }
21
22 const tick = 200 * time.Millisecond
23
24 func bee(id int, ctx context.Context) {
25   go func() {
26     for {
27       fmt.Printf("%d", id)
28       select {
29       case <-ctx.Done():
30         return
31       case <-time.After(tick):
32       }
33     }
34   }()
35 }

Listing 10 fires off 10 concurrent goroutines in the for loop starting in line 13 to illustrate the mechanics. All of them jump to the worker bee() function starting in line 24 to output their integer values there in an infinite loop. They then wait for 200 milliseconds as instructed by time.After() in line 31, before going on to repeat themselves ad infinitum.

However, the select statement starting in line 28 does not just wait for the repeatedly expiring timer, it also waits for events in the ctx.Done() channel, which is the context's communication funnel. If the main program closes this channel, the corresponding case statement kicks in, and the goroutine says goodbye with return.

The program's output now looks like this:

097851234646392...

Then the program reliably terminates after about a second, when the main function timer expires in line 17 of the main program and the program calls the cancel() snuffer function previously created by context.WithCancel().

Attentive readers will note that functions in Go can return functions; they are first-order data types, and Go code uses this feature quite liberally, often to adopt a functional programming style.

Complains Unless Used

In other languages, unused variables and unnecessarily dragged-in header files often accumulate in the course of system development. Go has set out to get rid of this uncontrolled growth however possible. If you declare a variable, but don't use it, the compiler will knock it on the head; if you import an external package, but don't use a function from it anywhere, the compiler will refuse to do its work until the untidy code is cleaned up.

This is certainly a good idea for programs shortly before they are released, but it can be outright annoying during development. If something doesn't run as desired, the obvious thing to do is to include a Printf() statement in the code to print a variable's value, but this means importing the fmt package. If the Printf() statement subsequently disappears after the problem is fixed, the import section still says "fmt", and the compiler refuses to compile the source code until that line disappears, too.

Fortunately, there is a loophole by which the compiler does not complain about defined but unused functions. If you want to stash code snippets for later use, just wrap them in a new function that you never use. By the way, I have heard that some renegade Go coders ignore the error codes returned by called functions by assigning them to the _ (underscore) pseudo variable (see also Listing 4). However, this is a mean trick that should be banned.

Initialization with Pitfalls

Before you use a variable for the first time, Go insists on knowing its type. As a programmer, you can make this clear to the program either by explicitly declaring the variable, such as var text string, which declares the text variable of the string type.

But even the first assignment of a value to a variable can indirectly declare its type, if := is used instead of the = operator. If the code says foo := "", the compiler knows that the variable foo is of the string type. If you want a slightly more sophisticated example,

bar := map[string]int{"a": 1, "b": 2}

states that bar is a hash table (map) type, which maps integer values to strings and initializes the map by assigning a value of 1 to the "a" key and value of 2 to "b".

If you don't declare your variables in one of these ways, you will get a rebuke from the compiler. This also happens if you're using previously declared variables on the left side of the := operator, because then Go insists on a simple assignment with = instead, as there is nothing to declare.

Incidentally, the short declaration with := (as opposed to the verbose one with var) sometimes leads to misunderstandings. A piece of code such as the one in Listing 11, which accidentally sets a variable num already set outside the (always true) if block together with a new str on the left side of a declaration/assignment with :=, will probably not work as desired. Go interprets the assignment as defining two new variables inside the if block and only overwrites the local version of num with the value 2, while the variable outside the if block remains unchanged, and the Print() statement afterwards will print the unmodified old value.

Listing 11

var1.go

01 package main
02
03 import (
04   "fmt"
05 )
06
07 func main() {
08   num := 1
09
10   if true {
11     num, str := 2, "abc"
12     fmt.Printf("num=%d str=%s\n", num, str) // 2, "abc"
13   }
14
15   fmt.Printf("num=%d\n", num) // 1
16 }

If you actually intend to work with the outer definition of num and want to assign a new value to it within the if block, you must not use the := operator in this arrangement. Instead, you must use var to declare the new str variable inside the if block and use the plain assignment operator = instead of := to initialize it. With this, Go will only use one instance of num, both inside and outside the if block (Figure 3). In Listing 11, this would require changing line 11 to var str string and line 12 to num, str = 2, "abc".

Figure 3: The output from both versions of the program in Listing 11 at runtime.

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • Rat Rac

    If program parts running in parallel keep interfering with each other, you may have a race condition. Mike Schilli shows how to instruct the Go compiler to detect these conditions and how to avoid them in the first place.

  • Simultaneous Runners

    In the Go language, program parts that run simultaneously synchronize and communicate natively via channels. Mike Schilli whips up a parallel web fetcher to demonstrate the concept.

  • Fighting Chaos

    When functions generate legions of goroutines to do subtasks, the main program needs to keep track and retain control of ongoing activity. To do this, Mike Schilli recommends using a Context construct.

  • Motion Sensor

    Inotify lets applications subscribe to change notifications in the filesystem. Mike Schilli uses the cross-platform fsnotify library to instruct a Go program to detect what's happening.

  • Progress by Installments

    Desktop applications, websites, and even command-line tools routinely display progress bars to keep impatient users patient during time-consuming actions. Mike Schilli shows several programming approaches for handwritten tools.

comments powered by Disqus
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters

Support Our Work

Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.

Learn More

News