sudopower

go_sync_once_do

GO: Sync.Once.Do() Why not just check a global variable ?

Consider this block of code

var globalAdService *adService = nil
var globalAdServiceInitOnce sync.Once

func GetAdService() AdService {
	if globalAdService == nil {
		globalAdServiceInitOnce.Do(func() { globalAdService = newAdService() })
	}
	return globalAdService
}

type adService struct{}

func newAdService() *adService {
	return &adService{}
}

This is an example of singleton pattern, to only initialise and object once and reuse it. But can’t we just check if a GLOBAL level object is already initialised and then just use it directly ?

func GetAdService() AdService {
	if globalAdService == nil {
		globalAdService = newAdService()
	}
	return globalAdService
}

Why even use Sync.Once.Do() if we check if struct is nil anyway ?

First I thought concurrent Go routines may access it at the same time and both see them as nil and cause multiple instances. This is fair point but at startup there’s got to be a “first” request that initialises it, for example a serial test suite or health check request. Which can make serial requests one after the other and hence at startup no go routines will access it in parallel.

I found some info to shoot down my assumptions.

  1. Let’s say we have a health check request which check this service, if an orchestrator makes this health check and doesn’t receive response it decides to restart the app / pod. The orchestrator tries “N” health check requests before deciding to do this. In case of a network delay it is possible two health check requests arrive at the same time. Thus causing two go routines trying to access in parallel. It may be rare but it is a possible RACE condition.
    • go run -race” Should be able to detect this
  2. If a serial test suite makes these requests it is prone to same issues. What is this test runner crashes and upon restart sends same request, it is possible due to network delays these arrive in parallel.
  3. In a complex system, it is difficult to guarantee this, considering there may be other paths / routes that can use this service in parallel. This causes potential danger of writing bad code.

How about in language like NodeJS ? It has a single event loop !

Enter NodeJS which has a single event loop (including worker threads which have their own event loop). In this case two callbacks using the same service will never try to create two instances of this object since callbacks are processed serially in the event loop. In case of worker threads, yes they will create their own instances of a singleton class, but that should be ok since they are independent OS threads. But if we were to share this service across them we would need to employ similar methods.

So we know why we need Sync.Once.Do why the nil check then, why not directly run it ?

Like so

func GetAdService() AdService {
	globalAdServiceInitOnce.Do(func() { globalAdService = newAdService() })
	return globalAdService
}

That is to avoid the extra work to run the Do function unnecessarily !

Leave a Reply

Your email address will not be published. Required fields are marked *

Related Blogs