Shall we call all the IO relate function in goroutine?

Hi all, I’m new with Golang. When I first learn goroutine, I found it’s amazing.

I was wondering when shall we use goroutine?

For example, for the monadic async style in other language, like OCaml, basically we do IO function like follows:

let handle_file (filename: string) =
  let%map text = Reader.file_contents filename in
  int_of_string text;;

The return type would be int Deferred.t , when we call this function, the scheduler would arrange it in the best performance.

In Golang, I just created function like:

func check(e error) {
	if e != nil {
		panic(e)
	}
}

func handleFile(filename string) string {
	dat, err := ioutil.ReadFile("./file.txt")
	check(err)
	res, err := strconv.Atoi(dat)
	check(err)
	return res
}

Just not sure if maybe we should call this function via goroutine or directly?

I’m not familiar with functional programming languages, but the specific use case you have here makes me think of Python’s asyncio package (and packages based on it) and C#'s System.Threading.Tasks.Task type where there’s a difference between “normal” values and future/task values.

Go doesn’t have this distinction; the runtime automatically shuffles goroutines around OS threads and/or delegates IO to background OS threads so that you don’t need to bother with async vs. synchronous functions.

The direct call in your code to ioutil.ReadFile is fine, however your check function is not idiomatic Go unless you handle the panics further up the call chain.

1 Like

Thanks for your replying, from my understanding, Golang will call all the IO relate function by goroutine implicitly, and we don’t need to make it explicitly like this:

func main() {
  out := make(chan int)
  go handleFile("./file.txt", out)
  fmt.Println(<-out)
}

func handleFile(filename string, out chan<- int){
  dat, _ := ioutil.ReadFile("./file.txt")
  res, _ := strconv.Atoi(dat)
  out <- res
}

I’m not good with diagrams, so I went with “ascii art” to show what happens when you use the channel:

+--------------------------------------+
|            Goroutine #1              |
+--------------------------------------+
|                                      |
|  +--------------------------------+  |
|  |  (Some init. before main call) |  |
|  +--------------------------------+  |
|                                      |
|  func main() {                       |
|                                      |
|    out := make(chan int)             |
|                                      |
|    go handleFile("./file.txt", out)  +------------------------------------------------------+
|                                      |                     Goroutine #2                     |
|    fmt.Println(<-out)                +------------------------------------------------------+
|                                      |                                                      |
|  +--------------------------------+  |  func handleFile(filename string, out chan<- int) {  |
|  |  Sleep while waiting for <-out |  |                                                      |
|  +--------------------------------+  |    dat, _ := ioutil.ReadFile("./file.txt")           |
|                                      |                                                      |
|                                      |  +------------------------------------------------+  |
|                                      |  | Somewhere down ioutil.ReadFile's call chain,   |  |
|                                      |  | IO is handed off to the OS and the goroutine   |  |
|                                      |  | is effectively blocked, allowing the runtime   |  |
|                                      |  | to schedule another goroutine.  In the case of |  |
|                                      |  | this example, there are no other goroutines.   |  |
|                                      |  +------------------------------------------------+  |
|                                      |                                                      |
|                                      |    res, _ := strconv.Atoi(dat)                       |
|                                      |                                                      |
|                                      |    out <- res                                        |
|                                      |                                                      |
|                                      |  +------------------------------------------------+  |
|                                      |  | Send res into the out channel, unblocking      |  |
|                                      |  | Goroutine #1.                                  |  |
|                                      |  +------------------------------------------------+  |
|                                      |                                                      |
|                                      +------------------------------------------------------+
|                                      |
|  +--------------------------------+  |
|  | Resume printing the result from|  |
|  | the out channel.               |  |
|  +--------------------------------+  |
|                                      |
+--------------------------------------+

You can see that the result of using another goroutine and a channel in this case is that instead of just the main goroutine being blocked while it awaits the result of ioutil.ReadFile, you have two blocked goroutines: Goroutine #1 while it awaits the receive from the channel and Goroutine #2 while it awaits the result from ioutil.ReadFile. When ioutil.ReadAll completes, Goroutine #2 wakes up and sends the result into the out channel and then Goroutine #2 terminates. Then Goroutine #1 wakes up and continues the call to fmt.Println. If you just called iotuil.ReadFile directly without starting another goroutine, the code would be clearer and have less overhead.

Your example has some unnecessary complexity for reading a single file. If you need to process multiple files, then using separate goroutines makes sense. You could do something like this:

func main() {
  out := make(chan int)
  go func() {
    // wg keeps track of the number of running goroutines so we don't close
    // the out channel until all the files have been handled.
    var wg sync.WaitGroup
    for _, filename in range getNumberFilenames() {
      // closures are the only scenario where values are passed "by reference"
      // in Go.  To not pass the filename by reference (because it changes
      // every loop iteration), copy it:
      filename := filename
      wg.Add(1)
      go func() {
        defer wg.Done()
        handleFile(f, out)
      }()
    }
    wg.Wait()
    close(out)
  }()
  for _, x := range out {
    fmt.Println(x)
  }
}
1 Like

Amazing example, awesome thanks! @skillian

As you mentioned

Somewhere down ioutil.ReadFile’s call chain, IO is handed off to the OS and the goroutine is effectively blocked, allowing the runtime to schedule another goroutine.

I was wondering if maybe we could just simplify the case to:

func main() {
  for _, filename in range getNumberFilenames() {
      filename := filename
      res := handleFile(f, out)
      fmt.Println(res)
  }
}


func handleFile(filename string){
  dat, _ := ioutil.ReadFile(filename)
  res, _ := strconv.Atoi(dat)
  return res
}