Golang代码中的这个问题是什么原因导致的

Golang代码中的这个问题是什么原因导致的 我正在学习使用通道和go http.get的goroutine,在下面的代码运行时,它会卡住,没有任何错误或警告。这里有什么问题?

package main

import (
	"bufio"
	"fmt"
	"net/http"
	"sync"
)

var wg sync.WaitGroup

func worker(url <-chan string){
	defer wg.Done()
	resp, err := http.Get(<-url)
	if err != nil {
		panic(err)
	}
	fmt.Println("response status:", resp.Status)
	scanner := bufio.NewScanner(resp.Body)
	for i := 0; scanner.Scan() && i < 5; i++ {
		fmt.Println(scanner.Text())
	}
	if err := scanner.Err(); err != nil {
		panic(err)
	}
}

func main() {
	wg.Add(3)
	url := make(chan string)
	urls := []string{
		"http://gobyexample.com",
		"http://att.com",
		"http://domaintools.com",
		"http://microsoft.com",
		"http://google.com",
	}
	for i := 0; i < 3; i++ {
		url <- urls[i]
		go worker(url)
	}

	wg.Wait()
}

更多关于Golang代码中的这个问题是什么原因导致的的实战教程也可以访问 https://www.itying.com/category-94-b0.html

19 回复

jameswang2015:

我这里已经很晚了,明天再执行第三步。

收到。

更多关于Golang代码中的这个问题是什么原因导致的的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我已经成功执行了第一步,运行正常。我的 Go 版本是 go1.12.9

[下一页 →](/t/whats-the-issue-with-this-code/15642?page=2)

对于第二步,我认为defer received.Body.Close()这样写没问题,因为它不在for循环中。

现在时间很晚了,我明天再完成第三步。

url 是无缓冲的,因此由于通道上没有接收器,它无法越过 url <- urls[i]

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

func worker(url <-chan string) 用于定义一个仅能接收值的通道,这类似于定义一种更严格的通道类型。

我不太清楚内部超时的使用(为什么在这种情况下需要它?)以及并发规划。你能为此举个例子吗?

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

jameswang2015:

我已经成功执行了第一步。

这很可能与我的 golang-linters 有关。请注意这个警告:go - 如果不关闭 response.Body 会发生什么? - Stack Overflow

jameswang2015:

这是更新后的第一步:

很好。

现在你对第二步有信心了吗?你可以继续第三步了。先制定一个计划。我们可以在编写代码之前先审查一下。

注意
如果你想要更多挑战,可以在第三步之前尝试将所有 fmt 打印抽象成一个函数。这会增加现有第二步的复杂度,但能换取一些实际的调试经验,并且需要更长的学习时间。 😄

引用 hollowaykeanho:

  1. 在不使用并发的情况下执行 http.Client.Get。首先在 main 函数中完成所有操作。
  2. 仍然仅在 main 函数中,将 http.Client.Get 抽象到其自身的函数中。
  3. 规划你的并发(为什么我会遇到间歇性的死锁错误?)。

如果你感到困惑,尝试一次只做一步并发布你的代码。这次,将由你来编写代码。

for 循环中使用 defer 是否正确?我收到“可能存在资源泄漏”的警告。

在 for 循环中使用 defer 没有意义。你需要在不使用 defer 关键字的情况下,手动使用 received.Body.Close() 来关闭它。例如:

  1. 在所有 panic(err) 之前
  2. 在第一个循环(for i := 0; i < len(urls); i++)的末尾,再执行一次

如何找出?

查找所有循环的退出点(例如 panic 是一个退出点),循环结束是另一个退出点。

jameswang2015:

这里是第一步

你运行过了吗?我遇到了:

main.go:22:30: bodyclose: response body must be closed (bodyclose)             
                received, err := client.Get(urls[i])  

你需要关闭 Body。否则会导致资源泄漏,客户端可能无法为后续的"keep-alive"请求复用与服务器的持久 TCP 连接。要关闭它,请在 get 请求后使用以下代码:

defer received.Body.Close()

注意: 你已经进行到第二步了。


在进入第三步之前,请确保尽可能清晰地进行所有重构。

jameswang2015: 我不太清楚如何使用内部超时(为什么在这种情况下需要它?)以及规划并发。你能举个例子吗?

假设服务器处理一个请求需要120秒(1分钟),而你的应用程序每个请求最多只允许60秒,那么你将会把所有时间预算都耗费在等待上。

jameswang2015: func worker(url <-chan string) 是定义一个仅接收值的通道,这就像定义一个更严格的通道类型。

这就是为什么我现在不推荐使用它(除非你已经精通并发并且对逻辑有很强的控制能力)。最好传入一个通道,这样你就可以完全控制等待/读取操作。

jameswang2015: 规划并发。你能举个例子吗?

你查看过那个链接吗?里面有一个关于如何规划并发的清晰示例。如果你能清晰地规划并发上下文,就完全不会使用全局变量,特别是互斥锁。

以下是第一步:

package main

import (
	"bufio"
	"fmt"
	"net/http"
	"time"
)

func main() {
	urls := []string{
		"http://gobyexample.com",
		"http://att.com",
		"http://domaintools.com",
		"http://microsoft.com",
		"http://google.com",
	}
	client := http.Client{
		Timeout: 5 * time.Second,
	}
	for i := 0; i < len(urls); i++ {
		received, err := client.Get(urls[i])
		if err != nil {
			panic(err)
		}
		fmt.Println("status: ", received.Status)
		scanner := bufio.NewScanner(received.Body)
		for i := 0; scanner.Scan() && i < 5; i++ {
			fmt.Println(scanner.Text())
		}
		if err := scanner.Err(); err != nil {
			panic(err)
		}
	}
}

以下是第二步:

package main

import (
	"bufio"
	"fmt"
	"net/http"
	"time"
)

func httpCall(url string) {
	client := http.Client{
		Timeout: 5 * time.Second,
	}
	received, err := client.Get(url)
	if err != nil {
		panic(err)
	}
	defer received.Body.Close()

	fmt.Println("status: ", received.StatusCode)
	scanner := bufio.NewScanner(received.Body)
	for i := 0; scanner.Scan() && i < 5; i++ {
		fmt.Println(scanner.Text())
	}
	if err := scanner.Err(); err != nil {
		panic(err)
	}
}
func main() {
	urls := []string{
		"http://gobyexample.com",
		"http://att.com",
		"http://domaintools.com",
		"http://microsoft.com",
		"http://google.com",
	}
	for i := 0; i < len(urls); i++ {
		httpCall(urls[i])
	}
}

以下是更新后的第一步内容:

package main

import (
	"bufio"
	"fmt"
	"net/http"
	"time"
)

func main() {
	urls := []string{
		"http://gobyexample.com",
		"http://att.com",
		"http://domaintools.com",
		"http://microsoft.com",
		"http://google.com",
	}
	client := http.Client{
		Timeout: 5 * time.Second,
	}
	for i := 0; i < len(urls); i++ {
		received, err := client.Get(urls[i])
		if err != nil {
			received.Body.Close()
			panic(err)
		}
		fmt.Println("status: ", received.Status)
		scanner := bufio.NewScanner(received.Body)
		for i := 0; scanner.Scan() && i < 5; i++ {
			fmt.Println(scanner.Text())
		}
		if err := scanner.Err(); err != nil {
			received.Body.Close()
			panic(err)
		}
		received.Body.Close()
	}
}

以下是更新后的步骤1:

package main

import (
	"bufio"
	"fmt"
	"net/http"
	"time"
)

func main() {
	urls := []string{
		"http://gobyexample.com",
		"http://att.com",
		"http://domaintools.com",
		"http://microsoft.com",
		"http://google.com",
	}
	client := http.Client{
		Timeout: 5 * time.Second,
	}
	for i := 0; i < len(urls); i++ {
		received, err := client.Get(urls[i])
		if err != nil {
			panic(err)
		}
		defer received.Body.Close()
		fmt.Println("status: ", received.Status)
		scanner := bufio.NewScanner(received.Body)
		for i := 0; scanner.Scan() && i < 5; i++ {
			fmt.Println(scanner.Text())
		}
		if err := scanner.Err(); err != nil {
			panic(err)
		}
	}
}

for循环中使用defer是否正确?我收到了“可能资源泄漏”的警告。

jameswang2015:

func worker(url <-chan string){

这种做法比较奇怪。传递通道时应该使用 func worker(url chan string)

jameswang2015:

resp, err := http.Get(<-url)

执行网络请求时应该设置内部超时。这很重要,因为你永远不知道会遇到什么错误,你的应用程序应该足够独立地取消调用。这里有个很好的例子:

client := http.Client{
    Timeout: 5 * time.Second,
}
resp, err := client.Get(url)
...

jameswang2015:

var wg sync.WaitGroup

你的并发设计不清晰且缺乏规划。否则你不会使用全局变量。你可以尝试以下几种方式:

  1. 在不使用并发的情况下执行 http.Client.Get。先在 main 函数中完成所有操作。
  2. 仍然只在 main 函数中处理,将 http.Client.Get 抽象到独立的函数中。
  3. 规划好你的并发设计(为什么我会遇到间歇性死锁错误?)。

记住,如果你无法在单个进程中处理,就不要考虑使用并发来倍增问题。

感谢!!我正在学习带通道的goroutine,注意到对于无缓冲通道,必须在主函数的goroutine之前使用(无论是向通道发送还是从通道接收)。(同意吗?)但我在编码时忘记了这个规则——感谢指出!我只是好奇为什么这段代码没有像往常一样报告"死锁"错误,而是直接卡住了——是因为我使用了http.get吗?

我也更新了代码如下,现在可以正常工作了——如果有其他实现方式请告诉我。谢谢!

package main

import (
	"bufio"
	"fmt"
	"net/http"
	"sync"
)

var wg sync.WaitGroup

func worker(url <-chan string){
	defer wg.Done()
	resp, err := http.Get(<-url)
	if err != nil {
		panic(err)
	}
	fmt.Println("response status:", resp.Status)
	scanner := bufio.NewScanner(resp.Body)
	for i := 0; scanner.Scan() && i < 5; i++ {
		fmt.Println(scanner.Text())
	}
	if err := scanner.Err(); err != nil {
		panic(err)
	}
}

func main() {
	wg.Add(3)
	url := make(chan string)
	urls := []string{
		"http://gobyexample.com",
		"http://att.com",
		"http://domaintools.com",
		"http://microsoft.com",
		"http://google.com",
	}
	for i := 0; i < 3; i++ {
		go worker(url)
	}
	for i := 0; i < 3; i++ {
		url <- urls[i]
	}
	wg.Wait()
}

问题在于通道操作和goroutine启动的顺序错误,导致死锁。代码中先向无缓冲通道发送数据,然后启动goroutine,但此时没有接收者,造成阻塞。

具体问题:

  1. 主goroutine在向通道发送数据时,没有其他goroutine在接收
  2. 通道操作阻塞了goroutine的启动

修正后的代码:

package main

import (
	"bufio"
	"fmt"
	"net/http"
	"sync"
)

var wg sync.WaitGroup

func worker(url <-chan string) {
	defer wg.Done()
	for u := range url {
		resp, err := http.Get(u)
		if err != nil {
			panic(err)
		}
		fmt.Println("response status:", resp.Status)
		scanner := bufio.NewScanner(resp.Body)
		for i := 0; scanner.Scan() && i < 5; i++ {
			fmt.Println(scanner.Text())
		}
		if err := scanner.Err(); err != nil {
			panic(err)
		}
		resp.Body.Close()
	}
}

func main() {
	wg.Add(3)
	url := make(chan string)
	
	// 先启动worker goroutine
	for i := 0; i < 3; i++ {
		go worker(url)
	}
	
	urls := []string{
		"http://gobyexample.com",
		"http://att.com", 
		"http://domaintools.com",
		"http://microsoft.com",
		"http://google.com",
	}
	
	// 然后发送数据
	for i := 0; i < 3; i++ {
		url <- urls[i]
	}
	
	close(url)
	wg.Wait()
}

主要修改:

  1. 先启动goroutine再发送数据
  2. 使用for range循环从通道读取
  3. 添加了resp.Body.Close()释放资源
  4. 在处理完所有URL后关闭通道

这样确保有接收者准备好后再发送数据,避免死锁。

回到顶部