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"
.
Buy this article as PDF
(incl. VAT)
Buy Linux Magazine
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.
News
-
Fedora 40 Beta Released Soon
With the official release of Fedora 40 coming in April, it's almost time to download the beta and see what's new.
-
New Pentesting Distribution to Compete with Kali Linux
SnoopGod is now available for your testing needs
-
Juno Computers Launches Another Linux Laptop
If you're looking for a powerhouse laptop that runs Ubuntu, the Juno Computers Neptune 17 v6 should be on your radar.
-
ZorinOS 17.1 Released, Includes Improved Windows App Support
If you need or desire to run Windows applications on Linux, there's one distribution intent on making that easier for you and its new release further improves that feature.
-
Linux Market Share Surpasses 4% for the First Time
Look out Windows and macOS, Linux is on the rise and has even topped ChromeOS to become the fourth most widely used OS around the globe.
-
KDE’s Plasma 6 Officially Available
KDE’s Plasma 6.0 "Megarelease" has happened, and it's brimming with new features, polish, and performance.
-
Latest Version of Tails Unleashed
Tails 6.0 is based on Debian 12 and includes GNOME 43.
-
KDE Announces New Slimbook V with Plenty of Power and KDE’s Plasma 6
If you're a fan of KDE Plasma, you'll be thrilled to hear they've announced a new Slimbook with an AMD CPU and the latest version of KDE Plasma desktop.
-
Monthly Sponsorship Includes Early Access to elementary OS 8
If you want to get a glimpse of what's in the pipeline for elementary OS 8, just set up a monthly sponsorship to help fund its continued existence.
-
DebConf24 to be Held in South Korea
Busan will be the location of the latest DebConf running July 28 through August 4