C is one of the ancestors of Go, and Go inherits a lot of C syntax and expressions, including global variables, although Go does not directly give the definition of global variables in its syntax specification, but those who have already started Go know that the package exported variable in Go plays the role of a global variable. exported variables are similar to C global variables in terms of advantages, disadvantages, and usage.

I’m a C programmer, so I’m not a stranger to global variables, so I didn’t have much Gap when learning Go global variables, but people from other languages (like Java) may feel awkward when learning Go global variables, and they may not be able to understand how to use global variables for a long time.

In this post, let’s talk about global variables in Golang and understand them systematically with you.

I. Global Variables in Go

A global variable is a variable that can be accessed and modified throughout the program, regardless of where it is defined. Different programming languages have different ways of declaring and using global variables.

In Python, you can declare a global variable anywhere in the module. Like the globvar in the example below, but if you want to reassign it, you need to use the global keyword in the function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
globvar = 0

def set_globvar_to_one():
  global globvar # To assign a value to the global variable globvar
  globvar = 1

def print_globvar():
  print(globvar) # Read global variables globvar without the global keyword

set_globvar_to_one()
print_globvar() # 1

There is no concept of global variables in Java, but you can use a public static variable of a class to simulate the effect of a global variable, because such a public class static variable can be accessed by any other class anywhere. For example, the following Java code for globalVar:

1
2
3
4
5
6
7
8
9
public class GlobalExample {

  // Global Variables
  public static int globalVar = 10;

  // Global constants
  public static final String GLOBAL_CONST = "Hello";

}

In Go, a global variable is an exported variable declared at the top level of a package with the first letter capitalized so that the variable can be accessed and modified anywhere in the entire Go program, such as the variable Global in the foo package in the following sample code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package foo

var Global = "myvalue" // Go Global Variables

package bar

import "foo"

func F1() {
    println(foo.Global)
    foo.Global = "another value"
}

foo.Global can be read and modified by any other package that imports the foo package, just like those operations on it in code F1 above.

Even for global variables, the scope of the above global variables is package block, not universe block, according to the Go syntax specification.

Since Go exported variables act as global variables in Go, it has the same advantages and disadvantages as global variables in other languages. Let’s look at the advantages and disadvantages of global variables next.

II. Advantages and disadvantages of global variables

As the saying goes, if it exists, it exists for a reason! Let’s not discuss whether “existence is justified” is philosophically correct, but let’s first look at what benefits global variables can bring.

1. Advantages of global variables

  • First, global variables are easy to access.

    The definition of a global variable dictates that it can be accessed anywhere in the program. Whether it is inside a function, a method, a loop, or a deeply indented conditional block, global variables can be accessed directly. This provides some “convenience” in reducing the number of arguments to a function, as well as eliminating the “hassle” of determining argument types and implementing argument passing.

    Global variables can easily be accidentally modified or obscured by local variables, which can lead to unexpected problems.

  • Second, global variables are easy to share data with.

    Due to their easy access, global variables are often used to share data between different parts of the program, such as configuration item data, command line flags, etc. Since the life cycle of global variables is the same as the whole life cycle of the program, they are not destroyed at the end of function calls and are not GC’d, but always exist and maintain their values. Therefore, when global variables are used as shared data, developers do not have the mental burden of worrying that the memory where the global variables are located has been “recycled”.

    Concurrent goroutines need to consider the “data race” problem when accessing the same global variable.

  • Finally, global variables make the code look cleaner.

    Go global variables only need to be declared once at the top level of the package, after which they can be accessed and modified from anywhere in the program. For the maintainer of the package in which the global variable is declared, this code couldn’t be more concise!

    Code that accesses and modifies global variables in multiple places creates direct data coupling with global variables, reducing maintainability and extensibility.

In the above description, I wrote a “reverse” viewpoint for each of the advantages of global variables. These reverse arguments are clustered together to form the set of disadvantages of global variables, so let’s continue to look at them.

2. Disadvantages of global variables

  • First, global variables are easily modified accidentally or obscured by local variables.

    As mentioned earlier, global variables are easily accessible, which means that all places may access or modify global variables directly. Changing a global variable in any one location may affect another function that uses it in an unexpected way. This makes testing against these functions more difficult. The presence of global variables makes isolation between tests poor, and if a global variable is modified during test case execution, it may be necessary to restore the global variable to its previous state before test execution ends to ensure the least possible interference with other test cases, as shown in the following example.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
    var globalVar int
    
    func F1() {
        globalVar = 5
    }
    
    func F2() {
        globalVar = 6
    }
    
    func TestF1(t *testing) {
        old := globalVar
        F1()
        // assert the result
        ... ...
        globalVar = old // Recover globalVar
    }
    
    func TestF2(t *testing) {
        old := globalVar
        F2()
        // assert the result
        ... ...
        globalVar = old // Recover globalVar
    }
    

    In addition, global variables can easily be shadowed by local variables of the same name in functions, methods, and loops, leading to some strange and difficult debugging problems, especially when used in conjunction with Go’s short variable declaration syntax**.

    go vet supports static analysis of code, but variable masking checks need to be installed additionally.

    1
    2
    
    $go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest
    $go vet -vettool=$(which shadow)
    
  • Second, there is a “data race” problem for accessing global variables under concurrency conditions.

    If your program has multiple goroutines reading and writing global variables concurrently, then the “data contention” problem is inevitable. You need to protect global variables with additional synchronization means, such as mutual exclusion locks, read/write locks, atomic operations, etc.

    By the same token, global variables that are not protected by synchronization means also limit the ability to execute unit tests in parallel (-paralell).

  • Finally, while global variables bring code simplicity, more than anything else, they bring coupling that is detrimental to extension and reuse!

    A global variable makes all code in the program that accesses and modifies it data-coupled to it, and small changes to the global variable will have an impact on that code. Thus, it would become very difficult to reuse or extend this code that depends on global variables. For example, if you want to parallelize their execution, you need to consider whether the global variables they are coupled to support synchronization means. To reuse the logic of this code in other programs, it may also be necessary to create a new global variable in the new program.

    As we can see, Go global variables have advantages and a bunch of disadvantages, so how exactly should we treat global variables in the actual production coding process? Let’s move on to the next section.

III. Usage conventions and alternatives for Go global variables

How exactly does the Go language treat global variables? I looked through the standard library to see how the official Go team treats global variables, and I came to the conclusion that they should be used as little as possible.

There are “quite a few” global variables in the Go standard library, but most of them are global “sentinel” error variables, such as

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// $GOROOT/src/io/io.go
var ErrShortWrite = errors.New("short write")

// ErrShortBuffer means that a read required a longer buffer than was provided.
var ErrShortBuffer = errors.New("short buffer")

// EOF is the error returned by Read when no more input is available.
// (Read must return EOF itself, not an error wrapping EOF,
// because callers will test for EOF using ==.)
// Functions should return EOF only to signal a graceful end of input.
// If the EOF occurs unexpectedly in a structured data stream,
// the appropriate error is either ErrUnexpectedEOF or some other error
// giving more detail.
var EOF = errors.New("EOF")

// ErrUnexpectedEOF means that EOF was encountered in the
// middle of reading a fixed-size block or data structure.
var ErrUnexpectedEOF = errors.New("unexpected EOF")
... ...

These ErrXXX global variables are defined as “Variables (Var)”, but since Go has been open source for a long time, there is a tacit agreement that these ErrXXX variables are only “read-only” and no one will modify them in any way. Here some beginners may ask: Why not define them as constants? That’s because the only types of constants in Go are boolean constants, rune constants, integer constants, floating point constants, complex constants, and string constants.

No other types can be defined as constants. And the dynamic type returned by errors.New is a pointer to the errors.errorString structure type, which is also clearly outside the scope of constant types.

Apart from global variables like ErrXXX, there are very few other global variables in the Go standard library. A typical global variable is http.DefaultServeMux:

1
2
3
4
5
6
7
8
9
// $GOROOT/src/net/http/server.go

// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux

var defaultServeMux ServeMux

// NewServeMux allocates and returns a new ServeMux.
func NewServeMux() *ServeMux { return new(ServeMux) }

The http package is a highly used package carried by Go since its early days. I guess the global variable DefaultServeMux was defined in the early implementation for some reason, and it may have been retained later for compatibility reasons, but in terms of code logic, removing it would not have any effect.

The logic of DefaultServeMux, defaultServeMux and NewServeMux in the http package also shows that the Go language uses an alternative to global variables, which is “encapsulation”. ServeMux for example (let’s assume that the global variable DefaultServeMux is removed and replaced by the package level unexported variable defaultServeMux).

The http package defines the ServeMux type and the corresponding methods for handling multiplexing HTTP requests, but instead of directly defining a global variable for ServerMux (we assume that the DefaultServeMux variable is removed), the http package defines a package level unexported variable defaultServeMux as the default Mux.

The http package exports only two functions Handle and HandleFunc for the caller to register the http request path with the corresponding handler (DefaultServeMux can be replaced with defaultServeMux in the following code):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// $GOROOT/src/net/http/server.go

// Handle registers the handler for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) }

// HandleFunc registers the handler function for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

This way http does not need to expose the details of the Mux implementation at all, and the caller does not need to rely on a global variable, a solution that converts the original data coupling to global variables into behavioral coupling to the http package.

A similar approach can be seen in the standard library log package, which defines the package variable std as the default logger, but only exposes a series of print functions such as Printf, whose implementation uses the corresponding methods of the package variable std:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// $GOROOT/src/log/log.go

// Print calls Output to print to the standard logger.
// Arguments are handled in the manner of fmt.Print.
func Print(v ...any) {
    if std.isDiscard.Load() {
        return
    }
    std.Output(2, fmt.Sprint(v...))
}

// Printf calls Output to print to the standard logger.
// Arguments are handled in the manner of fmt.Printf.
func Printf(format string, v ...any) {
    if std.isDiscard.Load() {
        return
    }
    std.Output(2, fmt.Sprintf(format, v...))
}

// Println calls Output to print to the standard logger.
// Arguments are handled in the manner of fmt.Println.
func Println(v ...any) {
    if std.isDiscard.Load() {
        return
    }
    std.Output(2, fmt.Sprintln(v...))
}
... ...

Note: Other languages may have some other alternatives to global variables, such as Java’s dependency injection.

IV. Summary

In summary, although global variables have the advantages of being easy to access, easy to share, and clean code, Go developers have chosen the best practice of “using global variables sparingly” compared to the disadvantages of accidental modification, concurrent data competition, and higher coupling.

In addition, the most common alternative to global variables in Go is encapsulation, which you can learn by reading the typical source code of the standard library.

V. Ref

  • https://tonybai.com/2023/03/22/global-variable-in-go/