8.1. 保持自己忙碌或做自己的工作

这个程序有什么问题?

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "net/http"
  6. )
  7. func main() {
  8. http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  9. fmt.Fprintln(w, "Hello, GopherCon SG")
  10. })
  11. go func() {
  12. if err := http.ListenAndServe(":8080", nil); err != nil {
  13. log.Fatal(err)
  14. }
  15. }()
  16. for {
  17. }
  18. }

该程序实现了我们的预期,它提供简单的 Web 服务。 然而,它同时也做了其他事情,它在无限循环中浪费 CPU 资源。 这是因为 main 的最后一行上的 for {} 将阻塞 main goroutine,因为它不执行任何 IO、等待锁定、发送或接收通道数据或以其他方式与调度器通信。

由于 Go 语言运行时主要是协同调度,该程序将在单个 CPU 上做无效地旋转,并可能最终实时锁定。

我们如何解决这个问题? 这是一个建议。

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "net/http"
  6. "runtime"
  7. )
  8. func main() {
  9. http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  10. fmt.Fprintln(w, "Hello, GopherCon SG")
  11. })
  12. go func() {
  13. if err := http.ListenAndServe(":8080", nil); err != nil {
  14. log.Fatal(err)
  15. }
  16. }()
  17. for {
  18. runtime.Gosched()
  19. }
  20. }

这看起来很愚蠢,但这是我看过的一种常见解决方案。 这是不了解潜在问题的症状。

现在,如果你有更多的经验,你可能会写这样的东西。

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "net/http"
  6. )
  7. func main() {
  8. http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  9. fmt.Fprintln(w, "Hello, GopherCon SG")
  10. })
  11. go func() {
  12. if err := http.ListenAndServe(":8080", nil); err != nil {
  13. log.Fatal(err)
  14. }
  15. }()
  16. select {}
  17. }

空的 select 语句将永远阻塞。 这是一个有用的属性,因为现在我们不再调用 runtime.GoSched() 而耗费整个 CPU。 但是这也只是治疗了症状,而不是病根。

我想向你提出另一种你可能在用的解决方案。 与其在 goroutine 中运行 http.ListenAndServe,会给我们留下处理 main goroutine 的问题,不如在 main goroutine 本身上运行 http.ListenAndServe

贴士:如果 Go 语言程序的 main.main 函数返回,无论程序在一段时间内启动的其他 goroutine 在做什么, Go 语言程序会无条件地退出。

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "net/http"
  6. )
  7. func main() {
  8. http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  9. fmt.Fprintln(w, "Hello, GopherCon SG")
  10. })
  11. if err := http.ListenAndServe(":8080", nil); err != nil {
  12. log.Fatal(err)
  13. }
  14. }

所以这是我的第一条建议:如果你的 goroutine 在得到另一个结果之前无法取得进展,那么让自己完成此工作而不是委托给其他 goroutine 会更简单。

这通常会消除将结果从 goroutine 返回到其启动程序所需的大量状态跟踪和通道操作。

贴士:许多 Go 程序员过度使用 goroutine,特别是刚开始时。与生活中的所有事情一样,适度是成功的关键。