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
-
New Slimbook EVO with Raw AMD Ryzen Power
If you're looking for serious power in a 14" ultrabook that is powered by Linux, Slimbook has just the thing for you.
-
The Gnome Foundation Struggling to Stay Afloat
The foundation behind the Gnome desktop environment is having to go through some serious belt-tightening due to continued financial problems.
-
Thousands of Linux Servers Infected with Stealth Malware Since 2021
Perfctl is capable of remaining undetected, which makes it dangerous and hard to mitigate.
-
Halcyon Creates Anti-Ransomware Protection for Linux
As more Linux systems are targeted by ransomware, Halcyon is stepping up its protection.
-
Valve and Arch Linux Announce Collaboration
Valve and Arch have come together for two projects that will have a serious impact on the Linux distribution.
-
Hacker Successfully Runs Linux on a CPU from the Early ‘70s
From the office of "Look what I can do," Dmitry Grinberg was able to get Linux running on a processor that was created in 1971.
-
OSI and LPI Form Strategic Alliance
With a goal of strengthening Linux and open source communities, this new alliance aims to nurture the growth of more highly skilled professionals.
-
Fedora 41 Beta Available with Some Interesting Additions
If you're a Fedora fan, you'll be excited to hear the beta version of the latest release is now available for testing and includes plenty of updates.
-
AlmaLinux Unveils New Hardware Certification Process
The AlmaLinux Hardware Certification Program run by the Certification Special Interest Group (SIG) aims to ensure seamless compatibility between AlmaLinux and a wide range of hardware configurations.
-
Wind River Introduces eLxr Pro Linux Solution
eLxr Pro offers an end-to-end Linux solution backed by expert commercial support.