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

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

ただいまの
回答率

90.75%

  • Go

    457questions

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

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

解決済

回答 2

投稿

  • 評価
  • クリップ 0
  • VIEW 319

前提・実現したいこと

  • 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の利用は不可避かと思いますが、正しく理解できていないようです。お手数ですが、対処法をご教示いただけないでしょうか。

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

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

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

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

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 2

checkベストアンサー

+1

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

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

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

package main

import (
    "fmt"
    "encoding/json"
    "log"
    "reflect"
)

func main() {
    jsonStr := `["value1"]`

    var obj interface{}
    err := json.Unmarshal(([]byte)(jsonStr), &obj)    
    if err != nil {
        log.Fatal(err)
    }
    set(&obj, "value2") // ※1
    fmt.Println("result=", obj)
}

func set(obj interface{}, value interface{}) {
    v := reflect.ValueOf(obj).Elem() // ※2

    s := "dummy"
    sr := reflect.ValueOf(s)
    v.Set(reflect.Append(v.Elem(), sr)) // ※3
    fmt.Println("v=", v)    // [value1, dummy]

    v = v.Elem() // ※4

    // 上で作成した dummy を指定
    v = v.Index(1)
    fmt.Println("v=", v)    // dummy

    // dummy を value2 で上書き
    v.Set(reflect.Indirect(reflect.ValueOf(value)))
    fmt.Println("v=", v)    // value2
}


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

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

package main

import (
    "fmt"
)

func main() {
    // cap=2 の array
    a := make([]int, 0, 2)

    a = append(a, 0)
    fmt.Printf("len=%d,cap=%d,addr=%p\n", len(a), cap(a), a)

    a = append(a, 0)
    fmt.Printf("len=%d,cap=%d,addr=%p\n", len(a), cap(a), a)

    a = append(a, 0)
    fmt.Printf("len=%d,cap=%d,addr=%p\n", len(a), cap(a), a)
}


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 の値を更新しています。

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

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

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

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

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/12/28 11:03

    mattn様

    丁寧なご回答ありがとうございます。頂いた内容をよく読み、いくつか簡単なコードを書きながら試すことで、動きがだいぶ分かってきました。プログラム内部の動きが1つ1つ見えてくるようで、reflectに対するモヤモヤがだいぶ晴れました。

    ところで既にお察しかもしれませんが、質問に挙げたset関数はmattnさんが公開しているgo-jsonpointerのset関数を参考にしたものです。同パッケージを利用させていただいたところ、JSONへの追加は提供機能の範囲外になっているようだったので、なぜそういう動きをするのかを知るためにコードを参照し、勉強がてら追加機能を実現するにはどうすればよいのだろうと考えていた次第です。まさか、ご本人様からご回答をいただけるとは思いませんでした。驚きと感謝を感じております。どうもありがとうございました。

    キャンセル

+1

様々な形式を取りうる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 15:13

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

    キャンセル

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

  • ただいまの回答率 90.75%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る

  • Go

    457questions

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