質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.50%
Go

Go(golang)は、Googleで開発されたオープンソースのプログラミング言語です。

Q&A

解決済

2回答

393閲覧

go の Interface の動きがわからない

golang-student

総合スコア8

Go

Go(golang)は、Googleで開発されたオープンソースのプログラミング言語です。

0グッド

0クリップ

投稿2017/12/27 04:30

###前提・実現したいこと

  • GoでJSONに値を追加する関数を作成したく、以下のようなset関数を作成しました。

(処理内容は質問用にシンプルにしてあります)

  • ["value1"]という元のJSONをset関数で["value1", "value2"]に変更できれば成功です。

###発生している問題・エラーメッセージ

  • しかし、実際には["value1"]のままになってしまいます。
  • コード実行時の出力は下記のコメントの通りです。

###該当のソースコード

package main import ( "fmt" "encoding/json" "reflect" ) func main() { jsonStr := `["value1"]` var obj interface{} json.Unmarshal(([]byte)(jsonStr), &obj) set(obj, "value2") fmt.Println("result=", obj) // ["value1"]のままになり、追加できない } func set(obj interface{}, value interface{}) { v := reflect.ValueOf(obj) // いきなり v.Index(1) を指定するとindex out of range のエラーが出るので、ダミーを append s := "dummy" sr := reflect.ValueOf(s) v = reflect.Append(v, reflect.Indirect(sr)) fmt.Println("v=", v) // [value1, dummy] // 上で作成した dummy を指定 v = v.Index(1) fmt.Println("v=", v) // dummy // dummy を value2 で上書き v.Set(reflect.Indirect(reflect.ValueOf(value))) fmt.Println("v=", v) // value2 }

###試したこと
reflect.Append関数の入力と出力で使われているvが同じ実体を指していることを期待していましたが、どうもそうではないようなので、アサーション等を試してみましたがNGでした。

###質問
様々な形式を取りうるJSONなので、Interfaceの利用は不可避かと思いますが、正しく理解できていないようです。お手数ですが、対処法をご教示いただけないでしょうか。

よろしくお願いいたします。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答2

0

ベストアンサー

様々な形式を取りうるJSONなので、Interfaceの利用は不可避かと思いますが、正しく理解できていないようです。お手数ですが、対処法をご教示いただけないでしょうか。

質問者さんは、コードの間違いを正して正しく理解されたい様に見えましたので、代替案ではなく添削とさせて頂きます。

正解はこのコードになります。

go

1package main 2 3import ( 4 "fmt" 5 "encoding/json" 6 "log" 7 "reflect" 8) 9 10func main() { 11 jsonStr := `["value1"]` 12 13 var obj interface{} 14 err := json.Unmarshal(([]byte)(jsonStr), &obj) 15 if err != nil { 16 log.Fatal(err) 17 } 18 set(&obj, "value2") // ※1 19 fmt.Println("result=", obj) 20} 21 22func set(obj interface{}, value interface{}) { 23 v := reflect.ValueOf(obj).Elem() // ※2 24 25 s := "dummy" 26 sr := reflect.ValueOf(s) 27 v.Set(reflect.Append(v.Elem(), sr)) // ※3 28 fmt.Println("v=", v) // [value1, dummy] 29 30 v = v.Elem() // ※4 31 32 // 上で作成した dummy を指定 33 v = v.Index(1) 34 fmt.Println("v=", v) // dummy 35 36 // dummy を value2 で上書き 37 v.Set(reflect.Indirect(reflect.ValueOf(value))) 38 fmt.Println("v=", v) // value2 39}

https://play.golang.org/p/EPZe8uMoEbQ

まず Go の slice は伸長し長さがcapを超えるとアドレスが変わります。どういう事かというとこれを実行して貰えると分かります。

go

1package main 2 3import ( 4 "fmt" 5) 6 7func main() { 8 // cap=2 の array 9 a := make([]int, 0, 2) 10 11 a = append(a, 0) 12 fmt.Printf("len=%d,cap=%d,addr=%p\n", len(a), cap(a), a) 13 14 a = append(a, 0) 15 fmt.Printf("len=%d,cap=%d,addr=%p\n", len(a), cap(a), a) 16 17 a = append(a, 0) 18 fmt.Printf("len=%d,cap=%d,addr=%p\n", len(a), cap(a), a) 19}

https://play.golang.org/p/Ob6JoVN4nwh

cap を超えたタイミングで容量が収まらないので別のメモリを確保しています。これと同じ事が質問者さんのコードでも起きます。ですので set 関数には obj のアドレスを渡し、set の中ではポインタのまま扱わなければならない事になります。

set の最初の ※1 では obj のアドレスを渡しています。※2 ではポインタをデリファレンスしています。v はデリファレンス結果であり、set の中での v は呼び出し元の obj のアドレスを持っている事になります。次に reflect.Append は interface{} 型のまま呼び出せないので実体を参照する為にもう1回 Elem() を呼び出しています。※3 で reflect.Append の戻り値を使って v の値を更新しています。

go

1v.Set(reflect.Append(v.Elem(), sr))

注意としては v という変数を上書きしても、それは新しいメモリで上書きされているだけで obj のアドレスに対しては更新されていないこいう事です。つまり上書きした変数に対して操作を行っても呼び出し元には戻らない事になります。

あと余談ですが sr はポインタでもインタフェース型でもないので Indirect は要りません。

※4 で再度 Elem() を実行しています。ここまで v は Set を呼び出す為にポインタとして扱ってきましたが、以降は slice の中身の操作なので len が cap を超える事はありませんのでデリファレンスした実態のまま操作できます。

投稿2017/12/27 09:01

編集2017/12/27 09:21
mattn

総合スコア5030

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

golang-student

2017/12/28 02:03

mattn様 丁寧なご回答ありがとうございます。頂いた内容をよく読み、いくつか簡単なコードを書きながら試すことで、動きがだいぶ分かってきました。プログラム内部の動きが1つ1つ見えてくるようで、reflectに対するモヤモヤがだいぶ晴れました。 ところで既にお察しかもしれませんが、質問に挙げたset関数はmattnさんが公開しているgo-jsonpointerのset関数を参考にしたものです。同パッケージを利用させていただいたところ、JSONへの追加は提供機能の範囲外になっているようだったので、なぜそういう動きをするのかを知るためにコードを参照し、勉強がてら追加機能を実現するにはどうすればよいのだろうと考えていた次第です。まさか、ご本人様からご回答をいただけるとは思いませんでした。驚きと感謝を感じております。どうもありがとうございました。
guest

0

様々な形式を取りうるJSONなので、Interfaceの利用は不可避かと思いますが

使ってもいいですが、今回の内容であれば使う必要はありません。
例えば、操作したいJSONがコード内部で生成されるものでしたら以下のような発想をします。

sample 1

package main import ( "encoding/json" "fmt" "log" ) func main() { s := []string{"value1"} var j string if b, err := json.Marshal(s); err != nil { log.Fatal(err) } else { j = string(b) } fmt.Println(j) s = append(s, "value2") if b, err := json.Marshal(s); err != nil { log.Fatal(err) } else { j = string(b) } fmt.Println(j) }

内部でJSONを生成しているのであれば、何らかの構造を持っているはずです。
それがsliceなのかmapなのかstructなのか、それらを複合的に合わせたものなのかは設計者の都合なので知りませんが、JSONは構造を有した構造化データですので、元構造の定義に従った操作を行うのが一般的です。

質問内容に書かれていませんが、仮に外部から与えられるJSONだったとしても、構造を知らないものを弄れるわけがありませんので、当然知っているはずです。
単純な配列型で、["value1", ...]となるのだということが既に分っているのであれば、以下のようにします。
sample 2

package main import ( "encoding/json" "fmt" "log" ) var ( sampleJson = `["value1"]` appendValue = "value2" ) func main() { s := new([]string) if err := json.Unmarshal([]byte(sampleJson), s); err != nil { log.Fatal(err) } // append *s = append(*s, appendValue) var j string if b, err := json.Marshal(s); err != nil { log.Fatal(err) } else { j = string(b) } fmt.Printf(`%s + "%s" -> %s`, sampleJson, appendValue, j) }

事前に知っている構造を定義して、そこにjson.Unmarshalします。
その後、その構造に対して操作を行い、json.Marshalで再びJSONに戻せば、操作終了です。

もし構造も知らない未知のJSONを操作したいのであれば、上記の方法とは異なります。
尤も、未知のJSONの場合、何を根拠に値を追加しに行くことになるのかが非常に重要なのですが、その旨は書かれていないのでそういうことをしたいわけではないと解釈しました。

投稿2017/12/27 05:25

WhiteRaven

総合スコア40

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

golang-student

2017/12/27 06:13

説明が足りなかったのですが、構造が未知なJSONを対象としています。例で挙げたものは質問を分かりやすくするためのものであり、実際は複雑なものになります。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.50%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問