Austin Millan
Jun 12 · 3 min read

Reused Variables in Go

In general it’s a bad idea to reuse/recycle variables (see: Software Design/Don’t reuse a variable). It makes code less readable, debugging more difficult, and can lead to bad designs.

The following sections will highlight some issues with reused/recycling variables in Go.

range

range is a Go built-in keyword provided to iterate over arrays and slices. The syntax is shorter than C-style for loops ForStmt = "for" [ Condition | ForClause | RangeClause ] Block ., but the way it works may be confusing at first.

Check out the following code snippet:

Output:

Notice that the address of a is the same between iterations and the underlying values in input don’t change. This is because:

When ranging over a slice, two values are returned for each iteration. The first is the index, and the second is a copy of the element at that index. https://tour.golang.org/moretypes/16

Between each iteration there’s only one variable being modified — and it’s a copy, not the underlying element.

Decoding

While it might be tempting to reuse structures to decode data (e.g. JSON, XML, Database rows), I’m going to point out an issue with this approach especially when decoding partial or invalid data.

Valid JSON w/ Omitted Fields

The issue here is there’s a side-effect when handling omitted data. When reusing a variable to decode data more than once, some fields from the first leak/bleed into the second which can be problematic.

The essence of the problem is demonstrated in the short code snippet below:

Output:

Notice that valB is omitted from the second JSON string. There are many situations you could have omitted JSON like this -- maybe it's from a (unsafe) HTTP PATCH request where the API requires only the fields the client wants updated. Or maybe the field was unset from a previous JSON serialization and the omitempty tag instructed the encoder to leave out unset fields.

Whatever the case is, we have the JSON so let’s see what happens when we decode it.

The first unmarshal operation results in each field being set to 1 ({"valA":1,"valB":1}), and the second JSON string overwrites valA to 0 ({"valA":0,"valB":1}). Notice valB was left unmodified from the first decoding step.

This probably isn’t a surprise — you’re reusing memory and overwriting some of it with new data.

Valid DB Row w/ Omitted Fields

Okay, let’s look at a database example that might be a bit more subtle:

Output:

This example is basically the same as the last, except instead of decoding JSON with omitted fields it’s decoding a database row with omitted values.

Failed Decoding

So far I have provided examples where incoming data (JSON/DB rows) are valid, but contain omitted data. But there are situations where incoming data may be in the wrong format or partially incomplete (e.g. streamed JSON), or maybe you’re just running a test with bad input data. Whatever the case, re-using a structure here also also produces problems.

The following example contains 2 valid JSON strings at the start and end of dataSequence array, the other contains an invalid field and are not added to the As array.

Output:

This is an interesting situation. The second unmarshal will (partially) fail due to the invalid type for the valA field, but the decoder continues on and successfully sets valB. While the unmarshal operation returns an error and the decoded object isn't put into A[1] index of the array, values still bleed between iterations of the loop, just like before.

Concurrency Problems (Race Condition)

Let’s suppose this is the general design you choose for your HTTP PATCH handler, with requests handled concurrently for a single resource, and you store this resource to be reused somewhere in memory. (I hope you already see the problem with running concurrent code in which multiple threads share a resource, and if not, review the output below the code snippet)

Output:

There are couple solutions off the top of my head to avoid this data race:

  1. Copy the variable into different addresses, and pass copies to each thread.
  2. Implement a locking mechanism ("sync") for the shared resource.
  3. Don’t reuse and share this structure across threads.

Conclusion

The conclusion is that in general it’s better to avoid reusing/recycling structures, and it should especially be avoided while decoding data.

Geek Culture

Proud to geek out. Follow to join our 1M monthly readers.