When you look at your go code and see the go keyword, your first thought is like ok this is non-blocking because it’s prefixed with the go keyword. But sometimes this assumption may be wrong, let’s explore why.

Let say we have simple ChatBot struct with a say method:

1
2
3
4
5
6
7
type ChatBot struct {
	Name string
}

func (p *ChatBot) Say(word string) {
	fmt.Printf("%s: %s\n", p.Name, word)
}

Ok, next we tasked to make our chat bot to say something:

1
2
3
4
func main() {
	bot := &ChatBot{Name: "Max"}
	bot.Say("Hello!")
}

At some point we noticed that our chat bot is blocking real users, and decided to make it non-blocking. Solution was pretty straightforward as adding go keyword in front of the method call.

1
2
3
4
5
func main() {
	bot := &ChatBot{name: "Max"}
	go bot.Say("Hello!")
	// below is imaginary code dealing with real users
}

Everybody was happy, until we received a new task to make our bot to say something other than hello and behave a human like. So next we implemented GeneratePhrase method:

1
2
3
4
5
6
7
func (p *ChatBot) GeneratePhrase() string {
	// our human like behavior, think before saying
	// though not all humans following this rule :lol
	time.Sleep(time.Second)
	// some sophisticated code to generate a random phrase
	return "I see"
}

Now it’s time to integrate, and it looks like we can now replace hardcoded "Hello!" with our new method! Done with it and it looks cool!

1
2
3
4
5
func main() {
	bot := &ChatBot{Name: "Max"}
	go bot.Say(bot.GeneratePhrase())
	// below is imaginary code dealing with real users
}

Deployed to prod and everybody was happy again. Well happiness is a volatile substance and indeed we noticed that our bot is blocking real users again. But wait theres is a go keyword in front of the bot’s Say method! Doesn’t it mean that the entire statement bot.Say(bot.GeneratePhrase()) should be async and non-blocking? Well let’s check.

main.goplayground
1
2
3
4
5
6
7
8
9
10
11
func main() {
	bot := &ChatBot{Name: "Max"}
	start := time.Now()
	go bot.Say(bot.GeneratePhrase())
	fmt.Println(time.Since(start))
	// below is imaginary code dealing with real users

	// it's more idiomatic to use sync.WaitGroup to wait,
	// but for simplicity sake, we just Sleep one second here.
	time.Sleep(time.Second)
}

Running this contrived example reveals, that entire go bot.Say(bot.GeneratePhrase()) statement takes 1s to run and there is nothing wrong with the go keyword. The go keyword applies to the bot.Say method only, but we are feeding the method’s parameter with another calculation bot.GeneratePhrase() which takes place in the main’s goroutine. It turns out that above code is equivalent to the following:

1
2
3
4
5
6
func main() {
	bot := &ChatBot{Name: "Max"}
	phrase := bot.GeneratePhrase()
	go bot.Say(phrase)
	// below is imaginary code dealing with real users
}

Now it’s clear, why our bot was blocking while saying its phrase. To fix this we should wrap bot.Say(bot.GeneratePhrase()) statement with anonymous goroutine.

1
2
3
4
5
6
7
func main() {
	bot := &ChatBot{Name: "Max"}
	go func() {
		bot.Say(bot.GeneratePhrase())
	}()
	// below is imaginary code dealing with real users
}

Conclusion: it’s very tempting to prefix a single line method with the go keyword to make it async, however in that case we should pay attention to its parameters and watch how its arguments are populated. On the other hand we could avoid this bug from the beginning if wrapping the method with anonymous goroutine.