Callbacks

Now that we know how to use WaitGroups, we can also introduce the concept of callbacks. If you have ever worked with languages like JavaScript that use them extensively, this section will be familiar to you. A callback is an anonymous function that will be executed within the context of a different function.

For example, we want to write a function to convert a string to uppercase, as well as making it asynchronous. How do we write this function so that we can work with callbacks? There's a little trick-we can have have a function that takes a string and returns a string:

func toUpperSync(word string) string { 
  //Code will go here 
} 

So take the returning type of this function (a string) and put it as the second parameter in an anonymous function, as shown here:

func toUpperSync(word string, f func(string)) { 
  //Code will go here 
} 

Now, the toUpperSync function returns nothing, but also takes a function that, by coincidence, also takes a string. We can execute this function with the result we will usually return.

func toUpperSync(word string, f func(string)) { 
  f(strings.ToUpper(word)) 
} 

We execute the f function with the result of calling the strings.ToUpper method with the provided word (which returns the word parameter in uppercase). Let's write the main function too:

package main 
 
import ( 
  "fmt" 
  "strings" 
) 
 
func main() { 
  toUpperSync("Hello Callbacks!", func(v string) {   
    fmt.Printf("Callback: %s
", v) }) 
} 
 
func toUpperSync(word string, f func(string)) { 
  f(strings.ToUpper(word)) 
} 

In our main code, we have defined our callback. As you can see, we passed the test Hello Callbacks! to convert it to uppercase. Next we pass the callback to be executed with the result of passing our string to uppercase. In this case, we simply print the text in the console with the text Callback in front of it. When we execute this code, we get the following result:

$ go run main.go
Callback: HELLO CALLBACKS!

Strictly speaking, this is a synchronous callback. To make it asynchronous we have to introduce some concurrent handling:

package main 
import ( 
  "fmt" 
  "strings" 
  "sync" 
) 
 
var wait sync.WaitGroup 
 
func main() { 
  wait.Add(1) 
 
  toUpperAsync("Hello Callbacks!", func(v string) { 
    fmt.Printf("Callback: %s
", v) 
    wait.Done() 
  }) 
 
  println("Waiting async response...") 
  wait.Wait() 
} 
 
func toUpperAsync(word string, f func(string)) { 
  go func(){ 
    f(strings.ToUpper(word)) 
  }() 
} 

This is the same code executed asynchronously. We use WaitGroups to handle concurrency (we will see later that channels can also be used for this). Now, our function toUpperAsync is, as its name implies, asynchronous. We launched the callback in a different Goroutine by using the keyword go when calling the callback. We write a small message to show the ordering nature of the concurrent execution more precisely. We wait until the callback signals that it's finished and we can exit the program safely. When we execute this, we get the following result:

$ go run main.go 

Waiting async response...
Callback: HELLO CALLBACKS!

As you can see, the program reaches the end of the main function before executing the callback in the toUpperAsync function. This pattern brings many possibilities, but leaves us open to one big problem called callback hell.

Callback hell

The term callback hell is commonly used to refer to when many callbacks have been stacked within each other. This makes them difficult to reason with and handle when they grow too much. For example, using the same code as before, we could stack another asynchronous call with the contents that we previously printed to the console:

func main() { 
  wait.Add(1) 
 
  toUpperAsync("Hello Callbacks!", func(v string) { 
    toUpperAsync(fmt.Sprintf("Callback: %s
", v), func(v string) { 
      fmt.Printf("Callback within %s", v) 
      wait.Done() 
    }) 
  }) 
  println("Waiting async response...") 
  wait.Wait() 
} 

(We have omitted imports, the package name, and the toUpperAsync function as they have not changed.) Now we have the toUpperAsync function within a toUpperAsync function, and we could embed many more if we want. In this case, we again pass the text that we previously printed on the console to use it in the following callback. The inner callback finally prints it on the console, giving the following output:

$ go run main.go 
Waiting async response...
Callback within CALLBACK: HELLO CALLBACKS!

In this case, we can assume that the outer callback will be executed before the inner one. That's why we don't need to add one more to the WaitGroup.

The point here is that we must be careful when using callbacks. In very complex systems, too many callbacks are hard to reason with and hard to deal with. But with care and rationality, they are powerful tools.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.21.159.82