Millet Porridge

English version of https://corvo.myseu.cn

0%

A Small Pitfall with defer in Golang

Where the Problem Came From

People who come from C++ have very likely written code like this.

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
30
31
32
33
void set() {

{
// C++ has a feature: variables in a block are destructed when leaving the block
// Here an l variable is defined; l is constructed here and destructed when the current block ends.
// With it you never have to worry about forgetting to call mutex.unlock.
Lock l(&s->delMtx);

// do whatever
}

return ;
}

class Lock
{
public:
explicit Lock(pthread_mutex_t *mx) : mutex(mx)
{
int err = pthread_mutex_lock(mutex); // lock on construction
printf("[LOCKER]: lock %d\n", err);
}
~Lock()
{
int err = pthread_mutex_unlock(mutex); // unlock on destruction
printf("[LOCKER]: unlock %d\n", err);
mutex = nullptr;
}

private:
Lock(const Lock &l);
pthread_mutex_t *mutex;
};

And Then

Today I used this pattern once in Go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func newTTLMap() (m *TTLMap) {
m = &TTLMap{m: make(map[int]*item)}

go func() {
for ;; {

{ // To shrink the critical section, I deliberately locked only in this small block
log.Info("map lock")
m.mtx.Lock()
defer m.mtx.Unlock()
// for k, v := range m.m — traverse this map
}
// Do something
}
}()
return m
}

Then calling the program still deadlocked. The reason: the mtx above called Lock(), but Unlock() was never called — the second lock attempt of course deadlocks.

Later On

Really, not understanding the documentation properly is exhausting. The documentation says:

Go’s defer statement schedules a function call (the deferred function) to be run immediately before the function executing the defer returns. It’s an unusual but effective way to deal with situations such as resources that must be released regardless of which path a function takes to return. The canonical examples are unlocking a mutex or closing a file.

defer schedules a function call, not a code block; it executes immediately before the function returns.

It has nothing to do with code blocks — a defer called inside a code block still executes before the function ends.

What to Do?

Discovering this problem, I was in despair too — my previous C++ experience might as well not exist.

After thinking it over, there are two solutions.

The first (no more defer; I have a good memory and I’m not afraid):

1
2
3
4
5
6
7
8

{ // To shrink the critical section, I deliberately lock only in this small block
m.mtx.Lock()

// for k, v := range m.m — traverse this map

m.mtx.Unlock()
}

The second (force defer by turning it into an anonymous function):

1
2
3
4
5
6
func() {// To shrink the critical section, I deliberately lock only in this small block
m.mtx.Lock()
defer m.mtx.Unlock()

// for k, v := range m.m — traverse this map
}()

I don’t know which one you think is better. Although the critical section can’t be very large, forgetting Unlock() is still quite hard to do. But I think what can be left to the machine shouldn’t be left to people — I prefer the second one, though either is fine.

A Parting Word

First, I admit I didn’t understand Golang properly — that’s the root cause.

How was this problem discovered: recently I decided I couldn’t just write code carelessly, so today while writing I also wrote a few unit tests myself. I found the program wouldn’t end by itself, and further found the lock was never released. If this had been discovered only after everything was integrated, debugging would have been a disaster.

I hope everyone takes this as a warning: spending some time on unit tests brings great benefits. Test early, enjoy early.