为了账号安全,请及时绑定邮箱和手机立即绑定

Golang,goroutines:恐慌:运行时错误:无效的内存地址

Golang,goroutines:恐慌:运行时错误:无效的内存地址

Go
开满天机 2021-12-07 18:46:45
我对 golang 比较陌生,并试图理解主要原理并使用 chanels 编写基于 gouroutines 的代码。在我使用的其他语言中没有这样的工具,我想知道会出现像恐慌这样的错误......我的代码:package mainimport "fmt"import (    "time")type Work struct {    x,y,z int}func worker(in <-chan *Work, out chan<- *Work){    for w := range in {        w.z = w.x + w.y        time.Sleep(time.Duration(w.z))        out <-w    }}func sendWork(in chan <- *Work){    var wo *Work    wo.x, wo.y, wo.z = 1,2,3    in <- wo    in <- wo    in <- wo    in <- wo    in <- wo}func receiveWork(out <-chan *Work ) []*Work{    var  slice []*Work    for el := range out {        slice = append(slice, el)    }    return slice}func main() {    in, out := make(chan *Work), make(chan *Work)    for i := 0; i<3; i++{        go worker(in, out)    }    go sendWork(in)    data := receiveWork(out)    fmt.Printf("%v", data)}但在终端我得到了这个:panic: runtime error: invalid memory address or nil pointer dereference[signal 0xc0000005 code=0x1 addr=0x0 pc=0x401130]goroutine 8 [running]:main.sendWork(0xc0820101e0)        C:/temp/gocode/src/helloA/helloA.go:21 +0x20created by main.main        C:/temp/gocode/src/helloA/helloA.go:43 +0xe4goroutine 1 [chan receive]:main.receiveWork(0xc082010240, 0x0, 0x0, 0x0)        C:/temp/gocode/src/helloA/helloA.go:31 +0x80main.main()        C:/temp/gocode/src/helloA/helloA.go:45 +0xf2goroutine 5 [chan receive]:main.worker(0xc0820101e0, 0xc082010240)        C:/temp/gocode/src/helloA/helloA.go:12 +0x55created by main.main        C:/temp/gocode/src/helloA/helloA.go:40 +0xafgoroutine 6 [runnable]:main.worker(0xc0820101e0, 0xc082010240)        C:/temp/gocode/src/helloA/helloA.go:11created by main.main        C:/temp/gocode/src/helloA/helloA.go:40 +0xafgoroutine 7 [runnable]:main.worker(0xc0820101e0, 0xc082010240)        C:/temp/gocode/src/helloA/helloA.go:11created by main.main        C:/temp/gocode/src/helloA/helloA.go:40 +0xaf我如何确定问题出在哪里,以及如何很好地关闭 gouroutines,而不是将它们作为进程...
查看完整描述

1 回答

?
陪伴而非守候

TA贡献1757条经验 获得超8个赞

nil 取消引用:

您正在尝试访问由指针引用的结构,但该指针尚未设置为该结构的实例。您必须声明一个可以指向指针的结构。


错误首先出现在这里:


wo.x, wo.y, wo.z = 1,2,3

您尝试写入指向的对象的位置wo。但是这里的指针为零;它实际上并不是指向Work. 我们必须创建该实例,以便我们可以指向它。


指向结构体的指针的 nil 值为nil。如果你没有为它声明一个结构体实例,它指向 nil。


var wo *Work

声明wo为类型的指针Work到nil。


 var wo = &Work{}

声明wo为指向 的Work新实例的类型指针Work。


或者您可以使用较短的语法:


wo := &Work{}

至于死锁:


当我们关闭一个通道时,该通道上的范围循环将退出。在func worker我们范围内的一个频道。当这个通道关闭时,worker(s) 将退出。


为了等待所有工人完成处理,我们使用sync.WaitGroup. 这是在继续之前等待一组 goroutine 完成运行的简单方法。


首先你告诉等待组它应该等待多少个 goroutine。


wg.Add(3)

然后你等待:


wg.Wait()

直到所有的 goroutine 都调用了


wg.Done()

当他们完成执行时他们会这样做。


在这种情况下,我们需要在所有 worker 执行完毕后关闭输出通道,以便func receiveWork可以退出其 for range 循环。我们可以通过为此任务启动一个新的 goroutine 来做到这一点:


go func() {

    wg.Wait()

    close(out)

}()

这是整个文件,经过这些编辑:


package main


import (

    "fmt"

    "sync"

    "time"

)


type Work struct {

    x, y, z int

}


func worker(in <-chan *Work, out chan<- *Work, wg *sync.WaitGroup) {

    for w := range in {

        w.z = w.x + w.y

        time.Sleep(time.Duration(w.z))

        out <- w

    }

    wg.Done() // this worker is now done; let the WaitGroup know.

}


func sendWork(in chan<- *Work) {

    wo := &Work{x: 1, y: 2, z: 3} // more compact way of initializing the struct

    in <- wo

    in <- wo

    in <- wo

    in <- wo

    in <- wo

    close(in) // we are done sending to this channel; close it

}


func receiveWork(out <-chan *Work) []*Work {

    var slice []*Work

    for el := range out {

        slice = append(slice, el)

    }

    return slice

}


func main() {

    var wg sync.WaitGroup

    in, out := make(chan *Work), make(chan *Work)

    wg.Add(3) // number of workers

    for i := 0; i < 3; i++ {

        go worker(in, out, &wg)

    }


    go sendWork(in)


    go func() {

        wg.Wait()

        close(out)

    }()


    data := receiveWork(out)


    fmt.Printf("%v", data)

}

哪些输出:


[0x104382f0 0x104382f0 0x104382f0 0x104382f0 0x104382f0]

这可能不是您所期望的。然而,它确实突出了此代码的一个问题。稍后再谈。


如果你想打印结构体的内容,你可以停止使用指向 的指针Work,或者遍历切片的元素并逐个打印它们,如下所示:


for _, w := range data {

    fmt.Printf("%v", w)

}

输出:


&{1 2 3}&{1 2 3}&{1 2 3}&{1 2 3}&{1 2 3}

Go 在打印时不会跟随指针向下一步,以避免无限递归,因此您必须手动执行此操作。


比赛条件:


由于您*Work在通道中多次发送指向同一个实例的指针,因此多个 goroutine 在没有同步的情况下同时访问同一个实例。您可能想要的是停止使用指针,而使用值。Work而不是*Work.


如果你想使用指针,也许因为Work它实际上非常大,你可能想要创建多个实例,*Work所以你只将它发送到一个 goroutine。


以下是围棋比赛检测器对代码的评价:


C:/Go\bin\go.exe run -race C:/gopath/src/github.com/drathier/scratchpad/go/main/main.go

[0xc0820403c0 0xc0820403c0 0xc0820403c0 0xc0820403c0 0xc0820403c0]==================

WARNING: DATA RACE

Write by goroutine 6:

  main.worker()

      C:/gopath/src/github.com/drathier/scratchpad/go/main/main.go:15 +0x8a


Previous write by goroutine 8:

  main.worker()

      C:/gopath/src/github.com/drathier/scratchpad/go/main/main.go:15 +0x8a


Goroutine 6 (running) created at:

  main.main()

      C:/gopath/src/github.com/drathier/scratchpad/go/main/main.go:45 +0x10c


Goroutine 8 (running) created at:

  main.main()

      C:/gopath/src/github.com/drathier/scratchpad/go/main/main.go:45 +0x10c

==================

Found 1 data race(s)

exit status 66

在这一行:


w.z = w.x + w.y

所有 goroutine 都在w.z同时修改,所以如果它们尝试将不同的值写入w.z,则无法知道实际最终会出现什么值。再一次,这很容易通过创建 的多个实例*Work或使用值而不是指针来解决:Work。


查看完整回答
反对 回复 2021-12-07
  • 1 回答
  • 0 关注
  • 135 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信