Golang中是否应该在goroutine里调用所有IO相关函数?

Golang中是否应该在goroutine里调用所有IO相关函数? 大家好,我是Go语言的新手。当我第一次学习goroutine时,我发现它非常神奇。

我想知道我们何时应该使用goroutine?

例如,在其他语言中,比如OCaml,对于单子异步风格,我们基本上像下面这样执行IO函数:

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

返回类型将是int Deferred.t,当我们调用这个函数时,调度器会以最佳性能来安排它。

在Go语言中,我创建的函数如下:

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
}

只是不确定我们是否应该通过goroutine来调用这个函数,还是直接调用?


更多关于Golang中是否应该在goroutine里调用所有IO相关函数?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

4 回复

感谢您的回复。根据我的理解,Go语言会隐式地通过goroutine调用所有与IO相关的函数,我们不需要像下面这样显式地处理:

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
}

更多关于Golang中是否应该在goroutine里调用所有IO相关函数?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


太棒了,非常感谢!@skillian

正如你提到的:

ioutil.ReadFile 调用链的某个环节,IO 操作被移交给操作系统,此时 goroutine 实际上被阻塞,从而允许运行时调度另一个 goroutine。

我在想,也许我们可以将情况简化为:

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
}

我不熟悉函数式编程语言,但你这里的具体用例让我想到了 Python 的 asyncio 包(以及基于它的包)和 C# 的 System.Threading.Tasks.Task 类型,在这些语言中,“普通”值和未来/任务值之间存在区别。

Go 语言没有这种区别;运行时环境会自动在操作系统线程之间调度 goroutine,和/或将 IO 委托给后台操作系统线程,因此你无需区分异步函数和同步函数。

在你的代码中直接调用 ioutil.ReadFile 是可以的,但是你的 check 函数不符合 Go 语言的惯用法,除非你在调用链的更上层处理 panic。

func main() {
    fmt.Println("hello world")
}

在Go中,是否使用goroutine处理IO取决于具体场景。Go的IO操作默认是阻塞的,但通过goroutine可以轻松实现并发。以下是关键考虑因素和示例:

直接调用的情况

  • 简单脚本或顺序逻辑
  • IO操作是主要业务逻辑且无需并发
  • 需要立即使用结果
// 直接调用示例
func main() {
    result := handleFile("data.txt")
    fmt.Println("Result:", result)
}

使用goroutine的情况

  • 需要并发处理多个IO操作
  • 避免阻塞主线程(如HTTP服务器)
  • 实现超时控制
// goroutine调用示例
func main() {
    ch := make(chan int)
    
    go func() {
        result := handleFile("data.txt")
        ch <- result
    }()
    
    // 同时执行其他任务
    fmt.Println("Processing other tasks...")
    
    // 获取结果
    result := <-ch
    fmt.Println("File result:", result)
}

实际应用示例(HTTP服务器)

func handleRequest(w http.ResponseWriter, r *http.Request) {
    // 每个请求使用独立goroutine处理IO
    go processFileAsync("upload.txt")
    
    // 立即响应客户端
    w.Write([]byte("Request accepted"))
}

func processFileAsync(filename string) {
    result := handleFile(filename)
    // 异步处理结果
    fmt.Printf("Processed %s: %d\n", filename, result)
}

带错误处理的goroutine模式

func handleFileAsync(filename string) <-chan error {
    errCh := make(chan error, 1)
    
    go func() {
        defer close(errCh)
        _, err := ioutil.ReadFile(filename)
        if err != nil {
            errCh <- err
            return
        }
        // 处理文件...
    }()
    
    return errCh
}

重要原则

  1. 不要为所有IO都创建goroutine - 过度并发会增加复杂度
  2. 考虑使用sync.WaitGroup管理多个goroutine
func processMultipleFiles(filenames []string) {
    var wg sync.WaitGroup
    
    for _, fname := range filenames {
        wg.Add(1)
        go func(filename string) {
            defer wg.Done()
            handleFile(filename)
        }(fname)
    }
    
    wg.Wait()
}
  1. 对于网络服务,通常每个连接使用一个goroutine
func serve(conn net.Conn) {
    defer conn.Close()
    // IO操作在连接goroutine中执行
    buf := make([]byte, 1024)
    n, err := conn.Read(buf)
    // ... 处理数据
}

选择是否使用goroutine的关键因素是:是否需要并发、是否需要避免阻塞、以及是否要利用多核性能。对于简单的顺序逻辑,直接调用更清晰;对于需要并发或响应的系统,goroutine是更好的选择。

回到顶部