2016-03-21 22 views
1

私はこのコードを持っています。私は出力に期待:メソッドをゴルーチン関数として使用する方法

hello : 1 
world : 2 

が、それは出力:

world : 2 
world : 2 

は私のコードに何か問題はありますか?

package main 

import (
    "fmt" 
    "time" 
) 

type Task struct { 
    name string 
    data int32 
} 

func (this *Task) PrintData() { 
    fmt.Println(this.name, ":", this.data) 
} 

func main() { 
    tasks := []Task{{"hello", 1}, {"world", 2}} 
    for _, task := range tasks { 
     go task.PrintData() 
    } 
    time.Sleep(time.Second * 5000) 
} 

答えて

7

PrintDataポインタ受信機であるとtask値であるため、メソッド呼び出しを行うとき、コンパイラが自動的にtaskのアドレスをとります。結果として得られるコールは(&task).PrintData()と同じです。

変数taskは、ループの各繰り返しで異なる値に設定されます。最初のゴルーチンは、taskが2番目の値に設定されるまで実行されません。 this exampleを実行して、各繰り返しで同じアドレスがPrintDataに渡されることを確認します。

これを解決する方法はいくつかあります。第二のループの内側に新しい変数を作成することである

tasks := []*Task{{"hello", 1}, {"world", 2}} 
for _, task := range tasks { 
    go task.PrintData() 
} 

playground example

:最初のスライスで*Taskを使用することである

tasks := []Task{{"hello", 1}, {"world", 2}} 
for _, task := range tasks { 
    task := task 
    go task.PrintData() 
} 

playground example

第です(自動的に挿入されたアドレス操作を使用して)スライス要素のアドレスを取得します。

tasks := []Task{{"hello", 1}, {"world", 2}} 
for i := range tasks { 
    go tasks[i].PrintData() 
} 

playground example

さらに別のオプションは自動的にtaskのアドレスを取ってからのメソッド呼び出しを防ぐために、値の受信機に印刷データを変更することです:

func (this Task) PrintData() { 
    fmt.Println(this.name, ":", this.data) 
} 

playground example

この問題はありますissue discussed in the closures and goroutines FAQと同様です。この問題の違いは、goroutine関数へのポインタを渡すために使用されるメカニズムです。問題のコードは、メソッドのreceiver引数を使用します。 FAQのコードはclosureを使用しています。

+0

ありがとう、構造のメンバーを変更したいので、3番目の方が良いです。 – Devin

+0

@Devin、スライスを変更する場合は、3番目のオプションが唯一の方法です。更新された第3のオプションを参照してください。それはオリジナルよりもあまり冗長ではありません。 –

+1

@Devin Firstオプションは通常、(あなたの意図が 'Task'を変更し、スライス内のポインタを新しいポインタで置き換えるのではない限り)通常動作しますが、3番目のオプションはまだまだ優れています。 – hobbs

1

Go Frequently Asked Questions (FAQ)

What happens with closures running as goroutines?

並行処理でクロージャを使用した場合、一部の混乱が生じる可能性があります。 次のプログラムを考えてみましょう:

func main() { 
    done := make(chan bool) 

    values := []string{"a", "b", "c"} 
    for _, v := range values { 
     go func() { 
      fmt.Println(v) 
      done <- true 
     }() 
    } 

    // wait for all goroutines to complete before exiting 
    for _ = range values { 
     <-done 
    } 
} 

一つが誤って出力としてC、A、Bを見るために期待するかもしれません。あなたはおそらく代わりに を見ますか?c、c、cです。これは、 ループの各反復が変数vの同じインスタンスを使用するため、各クロージャが単一の変数 を共有するためです。クロージャが実行されると、fmt.Printlnが実行された時点でv の値が出力されますが、vはゴルーチンが起動してから に変更されている可能性があります。問題が発生する前にこの問題やその他の問題を検出するには、go vetを実行してください。

vの現在の値を起動時に各クロージャにバインドするには、 は内側のループを変更して、それぞれの反復ごとに新しい変数を作成する必要があります。 一つの方法は、クロージャへの引数として変数を渡すことである。この例では

for _, v := range values { 
    go func(u string) { 
     fmt.Println(u) 
     done <- true 
    }(v) 
} 

は、Vの値が 匿名関数への引数として渡されます。その値は、変数uとして関数 の内部でアクセスできます。

も簡単にはちょうど奇妙に思えるかもしれません宣言 スタイルを使用して、新しい変数を作成することですが、移動中に正常に動作します:

for _, v := range values { 
    v := v // create a new 'v'. 
    go func() { 
     fmt.Println(v) 
     done <- true 
    }() 
} 

だけ使用して閉鎖のための新しい変数を作成します奇妙に思えるかもしれないが、Goでうまくいく宣言スタイル。 task := taskを追加します。例えば、

package main 

import (
    "fmt" 
    "time" 
) 

type Task struct { 
    name string 
    data int32 
} 

func (this *Task) PrintData() { 
    fmt.Println(this.name, ":", this.data) 
} 

func main() { 
    tasks := []Task{{"hello", 1}, {"world", 2}} 
    for _, task := range tasks { 
     task := task 
     go task.PrintData() 
    } 
    time.Sleep(time.Second * 5000) 
} 

出力:

hello : 1 
world : 2 
+0

@MuffinTop:あなたは明らかに偽です。 [The Go Programming Language Specification](https://golang.org/ref/spec)を読んでください。 – peterSO

+0

ありがとう、私の質問を編集していただきありがとうございます、私は今理解しています – Devin

関連する問題