A Study of Real-World Data Races in Golang

The concurrent programming literature is rich with tools and techniques for data race detection. Less, however, has been known about real-world, industry-scale deployment, experience, and insights about data races. Golang (Go for short) is a modern programming language that makes concurrency a first-class citizen. Go offers both message passing and shared memory for communicating among concurrent threads. Go is gaining popularity in modern microservice-based systems. Data races in Go stand in the face of its emerging popularity. In this paper, using our industrial codebase as an example, we demonstrate that Go developers embrace concurrency and show how the abundance of concurrency alongside language idioms and nuances make Go programs highly susceptible to data races. Google's Go distribution ships with a built-in dynamic data race detector based on ThreadSanitizer. However, dynamic race detectors pose scalability and flakiness challenges; we discuss various software engineering trade-offs to make this detector work effectively at scale. We have deployed this detector in Uber's 46 million lines of Go codebase hosting 2100 distinct microservices, found over 2000 data races, and fixed over 1000 data races, spanning 790 distinct code patches submitted by 210 unique developers over a six-month period. Based on a detailed investigation of these data race patterns in Go, we make seven high-level observations relating to the complex interplay between the Go language paradigm and data races.


Introduction
Uber * caters to hundreds of millions of daily customers who depend on its real-time transportation and delivery services. Uber's back-end software is a microservice [70] architecture running on several million CPU cores in many data centers. Different microservices talk to one another via remoteprocedure calls (RPC). Individual services are written in various programming languages such as Go, Java, Python, and NodeJS. Uber has extensively adopted Go as a primary programming language for developing these microservices. Uber's Go monorepo [65] comprises about 46 million lines of code and contains approximately 2100 unique Go services.
Go is one of the de facto languages for implementing microservices. There are several benefits of Go [32,40,46,50,60,82]. Concurrency is a first-class citizen in Go. Go requires minimal garbage collection tuning effort, unlike other languages such as Java. Go is compiled to native binary, making it performant. Go embraces a minimalist approach with Python-like syntax making it easy to learn and well-suited for rapid code development. Finally, Go has rich tooling, library support, and a thriving open-source community.
Go makes it easy to express asynchronous tasks in programs. Prefixing a function call with the go keyword runs the call asynchronously in the address space of the calling process. These asynchronous function calls in Go are called goroutines [15]. Developers hide latency (e.g., IO or RPC calls to other services) by creating goroutines within Go programs. Goroutines are considered "lightweight", and the Go runtime context switches them on the operating-system (OS) threads. Go programmers use goroutines liberally both for symmetric and asymmetric tasks. Two or more goroutines can communicate data via message passing (channels [13]) or shared memory. Shared memory is the most commonly used means of data communication in Go [83].
Go's memory model [81] is loosely defined and not standardized; it is claimed to be "like DRF-SC" [34]. Go memory model defines a happens-before-based partial ordering among the events in different goroutines [35]. Synchronization events establish a happens-before partial order between two or more participating goroutines. For example, a send event on a channel by a goroutine is considered to happen before the corresponding receive event on the same channel in another goroutine; an unlock event on a mutex by a goroutine is considered to happen before the subsequent lock event in another goroutine on the same mutex object. A data race occurs in Go when two or more goroutines access the same datum, at least one of them is a write, and there is no ordering between them [29,35,36].
Outages caused by data races in Go programs are a recurring and painful problem in our microservices. These issues have brought down our critical customer-facing services for hours together, causing inconvenience to our customers and impacting our revenue. Our developers have also found it hard to debug data races and sometimes resorted to fixing them by conservative strategies such as eliminating the concurrency altogether in suspicious code regions.
In this paper, we discuss deploying Go's default dynamic race detector to continuously detect data races in Uber's Go development environment. We elaborate on our learning from this deployment by providing a detailed overview of the common patterns for introducing data races in Go programs and the facilities inherent in the language infrastructure that eases the introduction of races into production code. Our analysis is based on more than one thousand data races fixed by more than two hundred engineers.
While there have been many algorithms to perform dynamic race detection, there is a significant gap in the knowhow on deploying dynamic analysis in a real-world setting. Given the intrinsic non-determinism associated with dynamic race detection, deploying it as part of continuous integration [63] is impractical. On the other hand, deploying it as a post-facto detection process introduces complexities in reporting races without duplication and challenges in determining the correct owner. We elaborate on the design choices for our deployment.
We use more than 100K Go unit tests to exercise the code and detect data races in our repository. In about six months, our continuous monitoring system has detected over 2000 data races in the repository; during this time 210 unique developers fixed more than 1000 data races (corresponding to 790 unique patches). The remaining (∼1000) data races are being actively worked upon. The system detects about five new data races every day in newly introduced code.
Our analysis of the detected and fixed races from this period reveals a surprising result that suggests that there are aspects unique to Go, beyond common concurrency bug type patterns, that introduce these races. We find that several Go language design nuances such as t ransparent capture-byreference of free variables [25], named return variables [8], deferred functions [14], ability to mix shared memory with message passing [13], indistinguishable value vs. pointer semantics [16,25], extensive use of thread-unsafe built-in map [7], flexible group synchronization [37], and confusing slice [9] semantics when combined with the simplicity of introducing concurrency via goroutines make Go highly susceptible to data races.

Technical Contributions
We make the following technical contributions in this paper.
1. Discuss the concurrency characteristics of Go programs in production to motivate the need for effective solutions to detect and eliminate data races in Go. 2. Identify technical gaps with deploying dynamic race detection in a production system to continuously detect races and scale to support thousands of developers. 3. Perform an elaborate, first-of-a-kind study of various data race patterns found in our Go programs. This study is conducted by analyzing over 1000 data races fixed by 210 unique developers over a six-month period.

Concurrency in Go Services
In this section, we provide a few key static and dynamic concurrency-related characteristics of our Go programs and compare them with programs in other languages to highlight some of Go's distinguishing features. Most of our Go programs reside in a single repository (monorepo [65]) of about 46 million lines of Go code (MLoC) hosting 2100 unique microservices. In contrast, our Java monorepo constitutes 19 MLoC and hosts 857 microservices. As a rough approximation of the use of concurrency, we counted the number of concurrency creation constructs and synchronization constructs in these repositories and made the following observation: Observation 1. Developers using Go employ significantly more concurrency and synchronization constructs than in Java.  Table 1 shows the number of concurrency creation, pointto-point synchronization, and group communication constructs found in our Go and Java monorepos. For concurrency creation, we looked for the .start() construct in Java and the go construct in Go † . For point-to-point synchronization, we searched for constructs related to lock, unlock, acquire, release, and synchronized in Java and searched for Lock, Unlock, RLock, RUnlock, and the channel (<-) constructs in Go. For group communication, we looked for the instances of CyclicBarrier, CountDownLatch, and Phaser in Java and looked for the instances of WaitGroup [37] in Go. While this look-up is coarse-grained and imperfect, it sheds light on significant quantitative differences in the use of concurrency constructs in the two languages. Java code has 219 thread creation constructs per MLoC, whereas the same for Go is 250 per MLoC, which is not significantly different. However, the Java code has 203 point-to-point synchronization constructs per MLoC, whereas the same for Go is 754.2 per MLoC, which is 3.7× higher. Similarly, the Java code has 55.9 group synchronization constructs per MLoC, whereas the same for Go is 104.2 per MLoC, which is 1.9× higher.
Observation 2. Developers using Go for programming microservices expose significantly more runtime concurrency than other languages such as Java, Python, and NodeJS used for the same purpose.
To substantiate this fact, we scanned our data centers and counted the number of threads in the service instances (processes) running on each machine. For Go, we used the pprof [47] profiler to count the number of goroutines in each process. Our scan included 130K Go processes, 39.5K Java processes, 19K Python processes, and 7K NodeJS processes. Go programs are most widely deployed, and the next one is Java. Figure 1 shows the fraction of processes at different concurrency levels for each language, plotted as a cumulative frequency distribution graph. The x-axis plots † the exact regular expressions are more involved. the concurrency level on a log scale. The y-axis is the cumulative fraction of processes of each language. The x value(s), where the line rises sharply, represents the common concurrency level for the specific language. A line rising more on the higher value of x has a higher average concurrency. From the figure, we note that NodeJS typically has 16 threads. Python typically has less than 16-32 threads. The second most used language in our fleet, Java, often has between 128-1024 threads; about 10% of cases have 4096 threads, and 7% have 8192 threads. In contrast, typically, Go processes have 1024-4096 goroutines; about 6% of processes contain 8102 goroutines. The max reaches at about 130K goroutines with contributions to the cumulative sum along the intermediate steps. Put another way, the 50% percentile of the number of threads is 16 in NodeJS, 16 in Python, 256 in Java, and 2048 in Go. This data empirically demonstrates that Go programs, at runtime, expose a lot of concurrency -about 8× more than Java.
Since higher concurrency can introduce more concurrency bugs, it is only natural to expect more data races in Go, especially when the language does not have a built-in mechanism to avoid data races, unlike languages such as Rust [38,68].

Deploying Dynamic Data Race Detector
In this section, we provide a brief background on dynamic data race detection, discuss the design challenges of its deployment in an industrial setup, elaborate on our deployment design, and finally conclude with the impact of rolling this out to the engineering community internally.

Dynamic Data Race Detection
A data race happens when a shared memory location is accessed concurrently by more than one thread and at least one of the accesses is a write access. The absence of a happensbefore (HB) relation between the accesses results in the accesses being considered concurrent.
Executing a program and analyzing its execution, by instrumenting the shared memory accesses and synchronization primitives, is used to detect underlying races. The execution of unit tests in Go that spawn multiple goroutines is a good starting point for dynamic race detection. Go has a built-in race detector that can be used to instrument the code at compile time and detect the races during their execution.
Internally, the Go race detector uses the ThreadSanitizer [79] runtime library. ThreadSanitizer uses a combination of lock-sets [76] and HB [44,66] based algorithms to report races. The lock-sets algorithm tracks the set of locks held before every memory access. If the set intersection of lock-sets associated with the different accesses of a shared memory location across goroutines is empty, it might indicate a data race. The lock-sets algorithm does not track the HB relations and it may include races that may never manifest in practice.
For higher precision, the Go race detector also uses vector clocks [51] to track the HB relation between shared memory accesses. Vector clocks are expensive both in space and time since they track the relation between the timestamps of accesses. We defer the reader to the vast literature that discusses these approaches elaborately. Although the scheduling of goroutines on underlying OS-threads is nondeterministic in Go, since the race detector maintains a vector clock per goroutine, the data race detector correctly reports a data race whether or not the goroutines ran on the same OS thread in a given execution.
There are three attributes one needs to be aware of with respect to dynamic race detection: 1. It may not report all races in the source code as it is dependent on the analyzed executions. 2. The detected set of races depend on the thread interleavings and can vary across multiple runs, even though the input to the program remains unchanged. 3. A reported race can be "benign" [62] and need not lead to a failing execution.

Deployment Challenges
We now discuss the challenges associated with deploying the dynamic race detector in a large codebase quickly and ensure that the appropriate developers fix these races.
3.2.1 When to deploy? When code changes are sent for code review (e.g., a pull request (PR) [53]), it is common practice to run many low-cost static analysis checks [71] along with executing relevant unit tests [55]. This approach ensures that the problematic code changes are detected early and are not merged into the main branch of the source. This process has the following benefits: 1. Defects are fixed before they are pushed to production, 2. Defect ownership is straightforward as the author of the PR becomes responsible for fixing the bug, and 3. The time taken to fix the issue is comparatively lower as the author has sufficient context on the changes.
We explore the following three possibilities on when to deploy a dynamic data race detector. We ultimately adopt the last one -generating bug reports based on data race detection in our workflow also referred to as "post-facto data race bug reporting".
[Option I] Block the PR based on dynamic race detection: An obvious question is whether a dynamic race detector can also be used as part of this workflow, referred to as Continuous Integration (CI) [63]. In other words, when the PR is sent for review, should dynamic race detection also be performed to prevent potential data races from being introduced into the code? We observe that dynamic race detection is a misfit during this phase for the following reasons: 1. Dynamic race detection is non-deterministic [78] as the paths followed by various executions may be different due to the thread interleavings. The flakiness may result in races being dormant in the PR where the bug is introduced and being detected in a later, unrelated PR and impacting a different author. 2. High overheads (2×-20× [36]) of dynamic data race detection impact the turnaround time for landing changes into the main branch. Reducing the overheads by instrumenting only the modified libraries will not be helpful as this affects overall analysis precision (e.g., impacts the correctness of HB relations that may happen in another library or the runtime). 3. The presence of pre-existing races complicates the deployment as it can overwhelm a developer who may not have been responsible for the data races.
A low overhead dynamic data race detector [56,85] is not a panacea for this situation; the non-determinism of detected races prevents us from adopting it at a PR time.
[Option II] Add warnings to a PR based on dynamic race detection: Given the challenges of blocking a PR based on the non-determinism of a dynamic data race detector, we considered making data races as non-blocking PR-time warnings. However, we did not pursue this direction for the following reasons: 1. Thousands of pre-existing data races make the warning report noisy, 2. Developers ignore warnings and become immune to it, and 3. The overhead of the dynamic race detector violates build-time SLAs.
[Option III] Generate bug reports based on dynamic race detection: Based on these considerations, our deployment strategy for dynamic race detection is to perform it periodically on a snapshot of code, post-facto, instead of incorporating it as part of the CI workflow. Meaning, we do not prevent data races from being introduced into our codebase; we actively find and fix them. This design assigns a detected data race as a defect to its owner using a heuristic. While incorrectly blocking a PR makes the build system flaky and hurts developer sentiments, accidentally assigning a data race defect to an incorrect owner is not a problem of the same magnitude. This is so because defects get triaged and eventually get reassigned to appropriate owners in a large software development environment.
Despite our current design choice, our experience with this deployment (and other non-CI-based analyses) shows that algorithms enabling the deployment of dynamic race detection during the CI workflow are more impactful, as they increase the likelihood of races being fixed.

Post-facto Data Race Bug Reporting
Deploying dynamic race detection periodically, post-facto, will broadly involve the following steps -(a) clone the latest version of the repository, (b) perform dynamic race detection by executing all the unit tests in the repository, and (c) report all outstanding races by filing tasks to the appropriate bug owner. A detected race report contains the following details: 1. The conflicting memory address, 2. Two call chains (aka calling contexts or stack traces) of the two conflicting accesses, and 3. The memory access types (read or a write) associated with each access. This process exposes interesting design choices with building a practical developer workflow due to the challenges associated with the code constantly evolving, underlying non-determinism of dynamic race detection, and the frequent churn in the organization. We discuss these details below.
3.3.1 Ensuring unique race reports. Each calling context comprises a list of function names and line numbers. Hashing all this information can help in unique identification of the race but can result in duplicated race reports, just because either a) the source line numbers change because of unrelated changes to the source file, or b) the order of two conflicting accesses is reversed.
To avoid such duplication and to ensure consistency in reporting across source code revisions, we devised a simple hashing technique that is relatively resilient to these common concerns. We first ignore the source line numbers in both call chains, which takes care of unrelated code modifications within a function. Second, we order the two call stacks lexicographically; meaning two call chains P()->Q()->R() and A()->B()->C() are always ordered as A()->B()->C() and P()->Q()->R(), irrespective of the order in which the execution happened.
The flip side of ignoring the line numbers is that it suppresses different data races sharing exactly the same two call chains but differing only in line numbers if at least one of them is already reported. This approach is not a significant limitation because we suppress a defect iff there is an active one with the same hash that is already open in our bug database. As soon as the open defect with the same hash is fixed, our system files another defect with the same hash (sharing the call chains), if it finds one.
On this topic, it must also be emphasized that the uniqueness of reporting is only about the manifested races and not the underlying root causes. For example, the same underlying root cause may result in different pairs of conflicting memory accesses (e.g., absence of a lock causing multiple shared data structures to race). See statistics in Section 3.5. Automatically triaging the root cause and reporting them uniquely is an interesting area of research worth exploring but is outside the scope of our current effort.
3.3.2 Determining assignee for a race. After detecting a race, our system needs to determine the developer who will be responsible for fixing the race. In the absence of the root cause, the potential list of assignees is restricted to the authors of the root and leaf nodes of the two call stacks. Even though the leaf nodes contain the race-inducing memory accesses, we choose to report it to the owner of the root nodes of the call stacks. We chose this option because the developers associated with the root nodes correspond to the author of code higher up in the call stack; these developers have a stake in the functional correctness of their code and are hence incentivized to eliminate a race and drive the issue to closure even if it is in a downstream library.
Due to the churn in the organization and the presence of mass refactorings to source code, reporting a defect to the latest modifier of the code may result in an inaccurate assignment. Therefore, we consider a few additional statistics corresponding to a) the author(s) who frequently modify the associated source code, b) any metadata attached to the source describing the owning team, and c) the presence of the developer and their manager in the organization, etc. We found in our experience that attaching a log of how our algorithm arrived at the choice of the assignee and enumerating the potential set of assignees was useful to the developers, rather than simply assigning without explaining why the tool thinks the assignee should fix the data race.
Based on our experiences, we make the following remarks: Remark 1. Design algorithms to enable dynamic race detection during Continuous Integration.
Remark 2. Design algorithms to automatically root cause and identify appropriate ownership for data races. Figure 2 presents the architecture diagram of our deployment. Periodically (daily), the workflow executes all the unit tests with race enabled; this step involves compiling the entire program with instrumentation followed by executing tests.   The built-in dynamic race detector records encountered data races in each test to the standard error, which we capture for further processing. The detected races are de-duplicated, and tasks are generated for the resulting unique races and filed to the appropriate developer(s). The task contains the details on the source version on which the race was detected, stack traces of two conflict accesses, and the necessary instructions to help the developer reproduce the underlying race. The fixes to the races follow standard development procedures, and the resultant changes are merged into the main branch of the source.

Experiences
We rolled out this deployment in April 2021 and have collected data for about six months. Our approach has helped detect ∼2000 data races in our monorepo with hundreds of daily commits by hundreds of Go developers. Out of the reported races, 1011 races are fixed by 210 different engineers.
In addition, we observe that there were 790 unique patches to fix these races, suggesting ∼78% unique root causes. We also collected the statistics for the total outstanding races over the last six months and report this data in Figure 3. In the initial phase (2-3 months) of the rollout, we shepherded the assignees to fix the data races. The drop in the outstanding races is noticeable during this phase. Subsequently, as the shepherding was minimized, we notice a gradual increase in the total outstanding races. The figure also shows the fluctuations in the outstanding count, which is due to fixes to races, the introduction of new races, enabling and disabling of tests by developers, and the underlying nondeterminism of dynamic race detection. After reporting all the pre-existing races, we also observe that the workflow creates about five new race reports, on average, every day.  There is a slow rise in reported races from April to June because we did not release all existing data races in a single day. Instead, we slowly ramped up the number of data races we reported as we were fine-tuning our workflow to best suit our developers. The sudden surge in July is a result of finally opening the flood gates, which also coincided with fixing a lot of bugs due to the authors a) driving the developers to fix the data races and b) themselves fixing several critical data races. The initial phase resulted in a significant resolution of the created tasks. Subsequently, we observe that the gradient for the task creation is higher than that of task resolution because the authors disengaged from shepherding.
We believe that the presence of race detection as part of a CI workflow will help address this problem by preventing new races from being introduced, apart from reducing the outstanding race count to zero.
In terms of the overhead of running our offline data race detector, we noticed that the 95th percentile of the running time of all tests without data race detection is 25 minutes, whereas it increases by 4× to about 100 minutes with data race enabled. Furthermore, in a survey taken by tens of engineers, about six months after rolling out the system, 52% of developers found the system useful, 40% of developers were not involved with the system, and 8% of developers did not find it useful.

Observations of Data Races in Go
In this section, we detail several common patterns of data races we found in our investigation of over 1000 data races that were fixed. We have chosen, as our examples, the ones that either happen frequently or are unique and subtle to root cause. In the following examples, we have changed the function and variable names, reduced the code, and retained only the basic structure needed to understand the root cause of races.

Essential Background on Go Syntax
As a precursor to the rest of this section, we provide some essential background to the Go syntax [24]. Go is garbage collected, type-safe, and statically compiled.
In Go, the variable name precedes its type in a declaration, e.g., var a int . The syntax, x := 10 , both declares x as an integer via type inference and defines its value to be 10 Structs, like C++, are aggregate types in Go. Go does not have classes. However, one can define methods on types. A method is a function with a special "receiver" argumentthe object it is invoked on. func (m *myStruct) Foo() declares Foo as a method on myStruct type. Here, m is called the "receiver". Notice that the receiver is a pointer (*) to myStruct in this example.
While C++, Java, and Python pass this/self as a pointer to the method, in Go, it can be passed either as a pointer or a value depending on the type of the receiver. One can write a method on a value. For example, func (m myStruct) Foo() declares Foo as a method on myStruct value type, and the only difference is that the receiver here is a value type. The method invocation in either case uses the same obj.Foo() syntax.
Go has pointers, and they can be accessed by the standard address-of ( & ) operator on a value. Go uses the same dot ( . ) operator to access fields of a struct or invoke a method, whether on a value type (e.g., myValue.Foo()) or a pointer type (e.g., myPtr.Foo()). The compiler infers whether a variable is a pointer or value and transparently arranges for dereferencing it or taking its address, as appropriate.
Go allows nesting functions (aka closures). Nested functions are anonymous and capture all the free variables byreference. One can launch any function asynchronously by prefixing the call with the go keyword. Any function call with the prefix defer delays its execution till the function's return.
We now discuss the various data race patterns by making an observation at the beginning of each subsection and explain the observation elaborately with an example. All the example data races discussed in this section are available as an open-source artifact at https://zenodo.org/record/6330164.

Races due to Transparent Capture-by-Reference
Observation 3. Transparent capture-by-reference of free variables in goroutines is a recipe for data races.
Nested functions, aka closures, in Go transparently capture all free variables [5] by-reference. The programmer does not explicitly specify which free variables are captured in the closure syntax. This mode of usage is different from, say, C++, where the programmer must explicitly specify every captured free variable as well as specify whether it is captured by-value or by-reference [12]. In Java, all free variables are captured by-value [23].
Developers are often unaware that a variable used inside a closure is a free variable and captured by-reference, especially when the closure is large. More often than not, Go developers use closures as goroutines. As a result of captureby-reference and goroutine concurrency, Go programs end up potentially having unordered access to free variables, unless explicit synchronization is performed. The capture in a closure manifests itself in different forms, which we show in the following subsections.

4.2.1
Loop index variable capture. Listing 1 shows an example of iterating over a Go slice jobs and processing each element job via the ProcessJob function. Here the developer wraps the expensive ProcessJob in an anonymous goroutine that is launched one per item. However, the loop index variable job is captured by-reference inside the goroutine. When the goroutine launched for the first loop iteration is accessing the job variable on line 3, the for loop will be advancing through the slice and updating the same loop-index variable job (on line 1) to point to the second element in the slice. This situation causes a data race, which we have highlighted by the two black right triangles in the code. This type of data race happens for value and reference types; slices, array, and maps; and read-only and write accesses in the loop body. Go recommends a coding idiom to hide and privatize [17] the loop index variable in the loop body, which developers do not always follow.

4.2.2
Idiomatic err variable capture. Go allows and advocates multiple return values [19] from functions. It is common to return the actual return value(s) and an error object to indicate if there was an error. The actual return value is considered meaningful if and only if the error value is nil. In Listing 2, Line 1 shows an example invocation of a function Foo, which returns an integer and an error object. It is a common practice to assign the returned error object to a variable named err, followed by checking for its nilness as shown in lines 2-4. Since multiple error-returning functions can be called inside a function body, there will be several assignments to the err variable followed by the nilness check each time, as shown on lines 8 and 15. Developers do not create a new error variable each time.
When developers mix this idiom with a goroutine, as shown in lines 6-12, the err variable gets captured byreference in the closure. As a result, the accesses (both read and write) to err in the goroutine run concurrently with subsequent reads and writes to the same err variable in the enclosing function (or multiple instances of the goroutine), which causes a data race. In the example in Listing 2, one write to err on line 8 and another write to the same err on line 15 cause a data race.

Named return variable capture.
Go introduces a syntactic sugar called "named" return values [8]. The named return variables are treated as variables defined at the top of the function, whose scope outlives the body of the function. A return statement without arguments, known as a "naked" return, returns the named return values. Go recommends using naked returns only in short functions because they can harm readability in longer functions. However, developers use named returns in larger functions also. In the presence of a closure, mixing normal (non-naked) returns with named returns or using a deferred [14] return in a function with a named return is a risky proposition as we show below.

Normal return in a function with a named return:
Listing 3 shows mixing a normal return in a function with a named return variable in the presence of a goroutine. The function NamedReturnCallee returns an integer, and the return variable is named as result on line 1. The rest of the function body can read and write to result without declaring it because of this syntax. If the function returns at line 4, which is a naked return, due to the assignment result=10 on line 2, the caller at line 13 would see the return value of 10. The compiler arranges for copying result to retVal.
A named return function can also use the standard return syntax, as shown on line 9. This syntax makes the compiler copy the return value, 20 in the return statement, to be assigned to the named return variable result.
Line 6 creates a goroutine, which captures the named return variable result. In setting up this goroutine, even a concurrency expert might believe that the read from result on line 7 is safe because there is no other write to the same variable; the statement return 20 on line 9 is, after all, a constant return and does not seem to touch the named return variable result. The code generation, however, turns the return 20 statement into a write to result, as previously mentioned. Now suddenly, we have a concurrent read and a write to the shared result variable, a case of a data race.
Deferred functions in a named return: Yet another flavor of data races happens when a named return is present alongside Go's defer construct. The anonymous function on line 2 in Listing 4 is prefixed with the defer keyword. As a result, the anonymous function will execute after the return on line 10. In this case, the developer has written the deferred function such that the two named return variables resp and err are correctly populated on any return patha defensive programming technique.
Unfortunately, the developer also uses a goroutine on line 7, which captures the named return variable err on line 8. When the execution returns on line 10, the write to the named return variable err on line 3 executes after the return. In the meantime, the read to the same err variable in the goroutine on line 8 races with this write, causing a subtle data race. In the code where we found this situation, the enclosing function was large and complex. The developer could not foresee the data race while developing the code because there is no visible concurrent access to err in the code after the launch of the goroutine. Because of the complexity of 1  Listing 5. Data race in slices even after using locks. the data race, the developer could not even understand the defect when our tool reported the issue.

Data Races due to Slices
Observation 4. Slices are highly confusing types that create subtle and hard to diagnose data races.
Slices are dynamic arrays and reference types. Internally, a slice contains a pointer to the underlying array, its current length, and the maximum capacity to which the underlying array can expand. We refer to these variables as meta fields of a slice for ease of discussion. A common operation on a slice is to grow it via the append operation, as shown on line 6 in Listing 5. The slice is declared on line 2. When the size reaches the capacity, a new allocation (e.g., double the current size) is made, and the meta fields are updated. When goroutines concurrently access a slice, it is natural to protect accesses to it by a mutex, as shown by the Lock/Unlock operations surrounding it. In this code, the developer wraps the slice append operation into a standalone closure safeAppend and uses a mutex in the closure to ensure mutual exclusion; the closure correctly captures the free mutex variable.
A subtle bug happens because the developer accidentally passes the slice myResults as an argument as well to the goroutine, as shown on line 14. This style of invocation causes the meta fields of the slice to be copied from the callsite to the callee. Notice that this copying is not lock protected. While one iteration of the loop on line 14 is making a copy of the meta fields of myResults, a goroutine invoked on some previous iteration could be modifying the meta fields of the captured myResults on line 6. The append to this slice, which may modify the three meta fields of the slice, races with the read of the same meta variables.
This data race is not because of the goroutine capturing myResults by-reference, but because of a leftover passing of the slice. Given a slice is a reference type, one would not imagine its passing (copying) to a callee to cause a data race. However, a slice is not the same as a pointer type, hence the subtle data race. A better way to refactor this code is to a) pass a pointer to myResults and b) not capture myResults in 1  the safeAppend closure, and instead arrange to dereference the incoming pointer to myResults while holding the lock.

Data Races on Thread-Unsafe Map
Observation 5. Built-in maps in Go make them commonly used. The array-style syntax of map accesses provides a false illusion of disjoint accesses of elements. However, map implementation is thread-unsafe in Go causing frequent data races.
Hash table (map) is a built-in language feature in Go. However, the built-in hash table in Go is not thread-safe. Data race ensues if multiple goroutines simultaneously access the same hash table with at least one of them trying to modify the hash table (insert or delete an item).
Line 2 in Listing 6 shows the syntax for creating a hash table with a string key and an error value type. The function processOrders receives a slice of uuids. It iterates through each item in uuids (line 3) and launches a goroutine per item to process each item asynchronously. The GetOrder API invoked on each uuid may return a failure in which case the error string for the corresponding uuid is recorded in the errMap hash table. The developer intends to accumulate all errors (if any) in errMap and later return a combined error on line 12. A write-write data race happens on line 7 during concurrent writes to the hash table even though each uuid is intended to be a separate entry in the hash table.
In this case, the developer has full visibility into the concurrent code and the hash table (which is in the same enclosing function scope) and yet makes a fundamental but common assumption (and a mistake) that the different entries in the hash table can be concurrently accessed. This assumption stems from developers viewing the errMap[uuid] syntax used for map accesses and misinterpreting them to be accessing disjoint elements. However, a map (hash table), unlike an array or a slice, is a sparse data structure, and accessing one element might result in accessing another element; if during the same process another insertion/deletion happens, it will modify the sparse data structure and cause a data race.
We found more complex concurrent map access data races (not shown) resulting from the same hash table being passed to deep call paths and developers losing track of the fact that these call paths mutate the hash table via asynchronous goroutines. While the hash table leading to data races is not unique to Go, the following reasons make map-based data races common in Go.
Maps are more frequently used in Go: Since map is a built-in language construct, it is liberally used in Go. We have 83392 map-related constructs in our Java repository, whereas in Go, we have 273713 maps. This translates to 4389 map constructs per MLoC in Java, whereas the same for Go is 5950 per MLoC, which is 1.34× higher. Array syntax and error tolerance: The map access syntax is just like array-access syntax (unlike Java's get/put APIs), making it easy to use but often confused for a random access data structure. A non-existing map element can easily be queried with the table[key] syntax, which simply returns the default value without producing any error. This error tolerance can make one complacent when using Go maps.

Mistakes due to Pass-by-Value vs.
Pass-by-Reference in Go Observation 6. Pass-by-value semantics are recommended in Go because it simplifies escape analysis and gives variables a better chance to be allocated on the stack, which reduces pressure on the garbage collector. Developers often err on the side of pass-by-value (or methods over values), which can cause non-trivial data races.
Unlike Java, where all objects are reference types, in Go, an object can be a value type (struct) or a reference type (interface). There is no syntactic difference, leading to incorrect use of synchronization constructs such as sync.Mutex and sync.RWMutex, which are value types (structures) in Go.
In Listing 7, the function main creates a mutex structure on line 9 and passes it by value to the goroutine invocation on lines 11 and 12. The two concurrent executions of the function CriticalSection, now operate on two different mutex objects, which share no internal state. As a result, the Lock and Unlock operations on these two different mutexes do not ensure mutually exclusive access to the global variable a. A correct implementation should have passed the address of mutex (&mutex), and the signature of CriticalSection should have received a pointer (*sync.Mutex).
Since the Go syntax is the same for invoking a method over pointers or values, less attention is given by the developer to question that m.Lock is working on a copy of mutex instead of a pointer. Also, observe Listing 8 for how the Lock and Unlock are implemented. Even though they (correctly) operate on a pointer to Mutex, the caller can still invoke these APIs on a mutex value, and the compiler transparently arranges to pass the address of the value. Had this transparency not been there, the bug could have been detected as a compiler type-mismatch error.
A converse of this situation happens when developers accidentally implement a method where the receiver is a pointer to the structure instead of a value/copy of the structure. In these situations, multiple goroutines invoking the method accidentally share the same internal state of the structure, whereas the developer intended otherwise. Even in this case, the caller is unaware that the value type was transparently converted to a pointer type at the receiver.

Mixing Shared Memory with Message Passing
Observation 7. Mixed use of message passing (channels) and shared memory makes code complex and susceptible to data races.
Listing 9 shows an example of a generic Future [22] implementation by a developer using a channel for signaling and waiting. The Future can be started by calling the Start method, and one can block for the future's completion by calling the Wait method on the Future. The Start method creates a goroutine, which executes a function registered with the Future and records its return values (response and err). The goroutine signals the completion of the Future to the Wait method by sending a message on the channel ch as shown on line 6. Symmetrically, the Wait method blocks to fetch the message from the channel (line 11).
Contexts [18] in Go carry deadlines, cancelation signals, and other request-scoped values across API boundaries and between processes. This is a common pattern in microservices where timelines are set for tasks. Hence, Wait blocks on either the context being canceled (line 13) or the Future to complete (line 11). Furthermore, the wait employs a select statement [13] (line 10), which blocks until at least one of the select arms is ready. § If the context times out, the corresponding case records the err field of Future as ErrCancelled on line 14. This write to err races with the write to the same variable in the future on line 5. Also, the goroutine will block forever on line 6 when there is no receiver on the other side of the channel. Note that, if the context does not timeout, which is the common case, the channel send operation establishes a happens-before edge preventing a data race.

Incorrect Use of Flexible Group Synchronization
Observation 8. Go offers more leeway in its group synchronization construct sync.WaitGroup. The number of participants is dynamic. Incorrect placement of Add and Done methods of a sync.WaitGroup lead to data races.
The sync.WaitGroup structure is a group synchronization construct in Go. Unlike C++ barrier [6], pthread barrier [3], and Java barrier [4] or latch [2] constructs, the number of participants in a WaitGroup is not determined at the time of construction but is updated dynamically. Three operations are allowed on a WaitGroup object -Add(int), Done(), and Wait(). Add() increments the count of participants, and the Wait() blocks until Done() is called count number of times (typically once by each participant). WaitGroup is extensively used in Go. Table 1 shows that group synchronization is 1.9× higher in Go than in Java.
In Listing 10, the developer intends to create as many goroutines as the number of elements in the slice itemIds and process the items concurrently. Each goroutine records its success or failure status in results slice at different indices. The developer expects the parent function block at line 12 until all goroutines finish. The parent goroutine then accesses all elements of results to count the number of successful processings.
For this code to work correctly, when Wait is invoked on line 12, the number of registered participants must be already equal to the length of itemIds. This is possible only if wg.Add(1) is performed as many times as the length of itemIds prior to invoking Wait, which means wg.Add (1) should have been placed on line 5, prior to each goroutine invocation. However, the developer incorrectly places wg.Add(1) inside the body of the goroutines on line 7, which is not guaranteed to have been executed by the time the outer function WaitGrpExample invokes Wait. As a result, there can be fewer than the length of itemIds registered with § if both are ready, one is chosen non-deterministically. 1 func WaitGrpExample ( itemIds [] int ) int { 2 wg sync . WaitGroup 3 results := make ([] int , len ( itemIds ) ) 4 for i := 0; i < len ( itemIds ) ; i ++{ 5 6 go ( idx int ) { the WaitGroup when the Wait is invoked. For that reason, the Wait can unblock prematurely, and the WaitGrpExample function can start to read from the slice results (line 13) while some goroutines are concurrently writing to it.
We also found data races arising from a premature placement of the Done() call on a Waitgroup.

Parallel Testing Idiom
Observation 9. Running tests in parallel for Go's tabledriven test suite idiom can often cause data races, either in the product or test code.
Testing is a built-in feature in Go. Any function with the prefix Test in a file with suffix _test.go can be run as a test via the Go build system. If the test code calls an API testing.T.Parallel(), it will run concurrently with other such tests. We found a large class of data races happen due to such concurrent test executions. The root causes of these data races were sometimes in the test code and sometimes in the product code.
Additionally, Go developers often write many subtests and execute them via the Go-provided suite [20] package within a single Test-prefixed function. Go recommends a table-driven test suite idiom [33] to write and run a test suite. Our developers extensively write tens or hundreds of subtests in a test, which our systems run in parallel.
This idiom becomes a source of problem for a test suite where the developer either assumed serial test execution or lost track of using shared objects in a large complex test suite. Problems also arise when the product API(s) was written without thread safety (perhaps because it was not needed) but were invoked in parallel, violating the assumption.

Incorrect or Missing Mutual Exclusion
Observation 10. Incorrect use of mutual exclusion primitives leads to data races.
This class of data races due to logical mistakes while writing concurrent programs is not unique to Go. It is also one of the most frequent reasons for data races in our code. We discuss this class of races for completeness. Confusing pass-by-value vs. pass-by-reference 38 7 Mixing message passing with shared memory 25 8 Missing or incorrect use of group synchronization 24 9 Parallel test suite ( RWMutex, which allows concurrent readers to execute a critical section simultaneously. The RLock/RUnlock methods on a RWMutex hold the lock in a read-only mode. Sometimes developers accidentally put statements that may modify shared data in critical sections protected by RWMutex, while using RLock/RUnlock methods. Such a mistake could have happened due to code evolving to address some issues or new requirements. Listing 11 shows one such example where after acquiring an RLock, the code may modify a shared variable on line 6. Concurrent readers may modify the same data leading to a data race. Worse yet, on line 7, the data race violates idempotency, where a network IO operation is performed more than once.

Other forms for incorrect mutual exclusion.
There are numerous ways in which mutual exclusions can be incorrectly used. The subtle ones we found were partial mutual exclusion, where the developer used locks in one place and forgot to use it in another while accessing the same shared variable(s). In some cases, the developer used a lock but called unlock prematurely, leaving some shared variable access outside the critical section. We observed an analogous situation where the developer used sync.Atomic [21] partially -used for writing to a shared variable but forgot to use it to read from the same variable.

Summary of Findings
We studied each of the 1011 fixed data races and manually labeled their root cause(s). Table 2 shows the count of data races categorized under different language features and idioms discussed in this section. Table 3 shows the count of data races, which are language agnostic, and hence are not discussed in detail. These labelings are not mutually exclusive; sometimes, multiple labels were assigned to the same bug because they fell into multiple categories.
Overall, we see that missing or incorrect locking (Table 3) was the single largest reason for the data races. Concurrent accesses to slices and maps were also common. Capture-byreference in goroutines can be seen to be the next common cause. Mixing channels with shared memory, interchanging by-reference with by-value, and incorrect group communication are quite frequent causes of data races. The other subtle causes grouped under the miscellaneous category are less common. The last three lines in Table 3 pertain to those data races that were not root caused but instead addressed by refactoring the code: a) removing the concurrency, b) disabling tests, or c) changing the code/logic in a significant way.
Remark 3. Future programming language designers should carefully weigh different language features and coding idioms with their potential to create common or arcane concurrency bugs.

Threats to Validity
The discussion in this paper is based on our experiences with data races in Uber's Go monorepo and may have missed additional patterns of data races that may happen elsewhere. Also, as dynamic race detection does not detect all possible races due to code and interleaving coverage, our discussion may have missed a few patterns of races. Despite these threats to the universality of our results, the discussion on the patterns in this paper and the deployment experiences hold independently.

Related Work
Race detection is one of the most researched topics in the programming languages community with a vast body of literature. We discuss a few related works to help place our efforts in this context. Dynamic race detection: Dynamic analyses [39,43,44,48,58,66,69,77] instrument code to capture memory accesses and synchronization events and reasons about them at runtime to prove a conflict between a pair of accesses. Eraser [76], which is based on the lock-sets algorithm, checks for empty lock-set intersection before accessing a shared variable to detect a race. Vector clocks [44,51,66] are commonly used in dynamic data race detection in unstructured parallel programs; ThreadSanitizer [79], which is based on these algorithms and used for race detection, can introduce 2×-5× space and 2×-20× runtime overheads. Race detection based on causally-precedes (CP) relation [80] improves precision at the cost of increasing the overheads further. Many subsequent enhancements have used strategies to reduce additional instrumentation [27,57]. While structured fork-join style parallel programs [11,31,45] can significantly alleviate the overheads of runtime race detection [43,48,58,69], this facility is not available in unstructured parallel programs such as Go.
Our study dives deeper into the deployment of a variant of dynamic race detection and the practical issues surrounding it. While any of the online approaches can be employed in our setup, the key problems of scalability and determinism of detecting races persist with all these approaches. Exploring thread interleavings: Executing multithreaded tests and observing assertion failures is yet another approach to help detect data races. As manifesting a race is dependent on the thread interleaving, RaceFuzzer [78] fuzzes the thread schedules by inserting random thread yields at different synchronization points to ensure different interleavings are explored. In contrast, Chess [59] systematically explores various thread interleavings by performing a tree traversal on the interleaving tree. Finally, TSVD [52] uses lightweight monitoring of calling behavior associated with thread-unsafe methods to inject delays to expose races. While these strategies can also be used instead of dynamic analysis for our deployment, the problem of non-determinism with the detected races and the scale of the overall state space poses its own challenges for practical deployment. Automated test generation: A key requirement for dynamic analysis or testing-based approaches is the ability to analyze concurrent executions, which necessitates the availability of effective test suites. Samak et al. leverage sequential execution traces to generate tests to expose data races [74], other types of concurrency bugs [72,73], and assertion violations [75]. In [67], Pradel and Gross develop a technique for automatically generating executions with random inputs and thread interleavings to expose thread safety violations. Billes et al., [26] use a two-phase approach to identify potential conflicts by analyzing single client executions and synthesizing multi-client interactions. Our current deployment can benefit from all these approaches as they build the necessary test corpus for concurrent execution, which the dynamic race detector can analyze. Surveys on concurrency bugs: Tu et al., [83] analyze 171 reported concurrency-related bugs in six open-source Go applications. They classify the bugs into blocking and nonblocking categories and provide the root causes in each case. Qin et al., [68] studied memory and thread-safety practices in real-world Rust programs, indicating the potential for data races in Rust programs where compile time checks are circumvented. More than a decade ago, Lu et al., [54] studied four open-source C/C++ applications and characterized concurrency bug patterns in them. In contrast to these studies, our study a) focuses on data race bugs in Go, b) explores significantly more (2100) services inside a real industrial setting, c) investigates more than 1000 data races, d) bubble-up the interplay between language features and data races, and e) discusses the software engineering aspects of deploying a dynamic race detection for industrial scale.
While Lu et al., [54] found user-defined synchronization a common culprit in concurrency bugs, the same does not hold good for Go programs. Go developers rarely, if at all, use their own synchronization but liberally use Go's Mutex locks and condition variables. Static analysis based race detection: Static analysis for data race detection inspects source code to report conflicting accesses to shared variables. RacerD [28] performs abstract interpretation to report concurrency issues, including data races. Chord [61] uses various static analysis strategies for Java programs, including alias analysis, lock analysis, threadescape analysis, and call-graph construction, to reduce the possible pairs of memory accesses involved in a data race. Errorprone [71] employs AST analysis to detect possible data races. MC checker [42] also uses compiler extensions to perform abstract interpretation and statistical analysis to report locations of ineffective locking. We believe the bug patterns in Go presented in this paper can inspire further research in static race detection for Go.
Detecting races in event-driven applications: Petrov et al. [64] explore data races in JavaScript web applications by developing a definition of happen-before for asynchronous event-based web execution environments. Hsiao et al. [49] explore data races in Android apps and observe that traditional dynamic data race detectors introduce false ordering between events handled sequentially by the same thread. While Go has event-based asynchronous processing via channels, it is subsumed under the task-level parallelism of goroutines, where the traditional happens-before via vector clocks suffices. Further, unlike these efforts which explored novel race detection algorithms, the focus of this paper is on the deployment challenges and the interplay of language design with concurrency in Go.

Conclusions
In this paper, we discussed the characteristics of concurrency in Go programs and a deployment strategy for detecting races continuously using dynamic race detection. Based on observed (including fixed) data races, we elaborate on the Go language paradigms that make it easy to introduce races in Go programs. We hope that this work will inspire future work towards enabling dynamic race detection during continuous integration, developing program analyses for detecting Go races, and designing language features such that their interplay with concurrency does not easily introduce races.