This section illustrates Gobra’s specification language on simple examples and shows how we handle interfaces and concurrency.
Gobra uses a variant of separation logic  in order to reason about mutable heap data structures and concurrency. Separation logics associate an access permission with each heap location. Access permissions are held by method executions and transferred between methods upon call and return. A method may access a location only if it holds the associated permission. Permission to a shared location
is denoted in Gobra by
, which is analogous to separation logic’s
\(\mapsto \_\). Gobra provides an expressive permission model supporting fractional permissions  to allow concurrent read accesses while still ensuring exclusive writes, (recursive) predicates to denote access to unbounded data structures, and quantified permissions (also called iterated separating conjunction) to express permissions to random-access data structures such as arrays and slices.
The example in Fig. 1 illustrates the use of permissions. Method
increases all elements of a given slice
by an amount
. (Slices are data types that can intuitively be seen as shared arrays of variable length.) The method requires permission to all slice elements (via its precondition) and returns them to the caller (via its first postcondition).
Functional properties are expressed via standard assertions, which include side-effect free Go expressions (including calls to pure methods, as we explain below) as well as universal quantification and old-expressions to refer to the value an expression had in the pre-state of a method. In our example, the second postcondition uses these assertions to express the functional behavior of the method. The loop invariants are analogous to the method contracts and are needed for verification.
In Go, any memory location can either be shared or exclusive. Shared locations reside on the heap and can, thus, be accessed by multiple methods and threads; reasoning about shared locations requires permissions to ensure race freedom and to enable framing, i.e., preserving information across heap changes. On the other hand, exclusive locations are accessed exclusively by one method execution and may be allocated on the stack; they can be reasoned about as local variables. The Go compiler determines automatically whether a location is shared or exclusive, for instance by determining whether its address is taken at some point of the execution. To make verification independent of a particular compiler analysis, Gobra requires shared locations to be decorated with an extra annotation
at the declaration point, as illustrated by the following client of
The first line declares a Go array
of fixed length 4, with values 1, 2, 4, and 8. This array is sliced on line 2 using the syntax
, thereby omitting the first two elements of
from the created slice. Since
is used in a context in which it is sliced, it is a shared location, which is made explicit via the
annotation. Consequently, the array creation will produce permissions to the array elements, which are required by
’s precondition. Omitting the
annotation will cause a verification error.
Go supports polymorphism through interfaces, named sets of method signatures. Subtyping for interfaces is structural: a type implements an interface iff every method of the interface is implemented by the type. The subtype relationship is determined by the type checker, without any declarations from the programmerFootnote 1.
Calls on an interface value are dynamically dispatched. In settings with nominal subtyping, dynamic dispatch is handled by proving behavioral subtyping : each subtype declaration requires a proof that the specifications of subtype methods refine the specifications of the corresponding supertype methods. Since structural subtypes are not declared explicitly, we adapt this approach as follows.
Whenever a Go program assigns a value to a variable of an interface type, Gobra requires an implementation proof, that is, a proof that each method of the subtype satisfies the specification of the corresponding method in the interface. Implementation proofs are inferred automatically by Gobra in simple cases; user-provided implementation proofs are required especially when they include ghost operations, for instance, to manipulate predicates.
The example in Fig. 2 illustrates this approach. Interface
(lines 1–8) declares an interface with two methods,
. The latter may return values of an arbitrary type, which is denoted by an empty interface. Since interfaces do not contain an implementation, their specification must be fully abstract. To this end,
introduces an abstract predicate
, whose definition is provided by the subtypes of the interface. The functional behavior of interface methods can be expressed in terms of pure (that is, side-effect free) abstract methods, here,
, which will also be defined in subtypes.
Next, lines 10–16 show an implementation of the interface in the form of a counter. The counter has a current
value. As long as the maximum value is not reached,
will increase the current value. At line 16, an integer can be assigned to the empty interface since behavioral subtyping holds trivially. The specification at line 15 expresses that the returned interface value contains an integer with the old value of the
The counter implementation is completely independent of the
interface. Their connection is established only in the implementation proof (lines 18–24). This proof defines the
predicate from the
interface for receivers of type
(line 18). Moreover, an implementation proof verifies that the specification of each method implementation refines the specification of the corresponding interface method. This proof checks that, assuming the precondition of an interface method, a call to the implementation method with identical arguments establishes the postcondition of the interface method. This format is enforced syntactically and permits ghost operations before and after the call to manipulate predicates. For instance, the proof on line 21 for
temporarily unfolds the
predicate to obtain permission to
, which is required by the implementation method, and conversely after the call.
Implementation proofs can be written explicitly, imported from other packages, and also inferred automatically when no explicit proof exists in the current scope. Currently, Gobra does not infer ghost operations such as the
on line 21; our experiments suggest that already simple heuristics can deal with many cases occurring in practice. For instance, many implementation proofs we have encountered follow the same pattern: First, the interface predicate instances of the precondition are unfolded. Second, the implementation method is called. Lastly, the interface predicate instances of the postcondition are folded. This pattern can be generated automatically to alleviate the annotation burden.
Gobra’s implementation proofs enable one to reason about interfaces without enforcing subtype declarations in either the interface or the declaration, which would defeat the purpose of structural subtyping. This solution allows one to reason about dynamically-dispatched calls. For instance, the following code snippet verifies in Gobra:
In particular, Gobra is able to determine that
is equal to
, and the latter follows from the definition of
(line 12) and the initial value of
. This intuitive reasoning is enabled by an intricate underlying encoding, which is not exposed to users. Users do not have to know how interface predicates are encoded and can treat interface predicates the same as any other separation-logic predicate.
Go supports concurrency through goroutines, lightweight threads started by prefixing a method call with the
keyword. Go offers the usual synchronization primitives, but goroutines idiomatically synchronize via channels. Buffered channels provide asynchronous communication, where sending a message blocks only when the buffer is full. Unbuffered channels offer rendez-vouz communication.
Gobra enables verification of concurrent programs by associating Go’s synchronization primitives with predicates that do not only express properties of data but also express how permissions to shared memory get transferred between threads. For instance, lock invariants may include properties as well as permissions to the data protected by the lock, and channel invariants include properties and permissions of the data sent over a channel. These invariants are specified via ghost operations when the synchronization primitive is initialized.
Figure 3 illustrates Gobra’s concurrency support using an excerpt from a parallel search-and-replace algorithm (see the full paper  for the complete example). Method
spawns a series of worker threads and then sends each of them a chunk of the input slice to process. The worker threads are joined via a wait group
implements the worker threads.
Gobra associates channels (like
in the example) with a predicate to specify properties and permissions of the sent data. The call
on line 10 takes this predicate as an argument. As expressed on line 2, it includes permissions to the chunk a worker operates on. For synchronous channels, an additional predicate can specify permissions transferred in the opposite direction, from the receiver to the sender. Initializing a channel also creates send and receive permissions for the channel, which are used to control which threads may access it. In our example, we transfer a fraction of the receive permission to each worker (line 28).
The workers receive permission to the chunk they operate on via a message sent on line 24 and received on line 34. The transfer back is orchestrated through a wait group, which implements an abstract shared counter. Wait groups are used as follows: The main thread adds to the counter the number of units of work to be done in spawned goroutines (line 22). Each spawned goroutine decreases the counter each time a unit of work is done (via a call to
, line 37). The master can await the counter to reach 0 via a call to
(line 26). Gobra uses dedicated permissions to express the obligation of a thread to perform units of work before decreasing the counter; each time this happens, permissions are transferred to the wait group and, eventually to the main thread calling
. We omit the details here for brevity.
In our example, this mechanism allows the main thread to recover the permissions to the entire slice once the workers have terminated. The example in Fig. 3 illustrates only the permission aspect of the verification. Functional correctness can be verified easily based on the explained machinery, by specifying a stronger channel invariant that includes the work obligation for each worker. We omit the details here, but see the full paper  for the complete example.