2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

JSON unmarshalやORMがreturnで結果を返すのではなく変数のポインタを使う理由

Last updated at Posted at 2020-02-11

はじめに

GoでJSONをデコードするとき、こんな感じに変数のポインタを渡しますよね。

var result SomeStruct
err := json.Unmarshal(b, &result) // ポインタを渡して結果を詰め込んでもらう

rubyやpythonなら、JSONデコードってこんな感じですよね

# ruby
result = JSON.parse(some_json) # 入口からJSONが入って出口からパース結果が出てくる
# python 
result = json.load(some_json) # 入口からJSONが入って出口からパース結果が出てくる

さて、なぜでしょうか。

TL;DR

動的言語なら関数がどんな型を返しても良い訳です。
resultの型が何であっても、問題なく取り扱うことができます。
が、静的言語であるGoはそのようにはいきません。
同じようにresultを受け取る方式では、関数を利用する側は
取り出したい型がわかってるのに、取り出し後に毎回型アサーションをしなければなりません。

コレでピンときた人はブラウザバックです。

以上、では味気ないので、ツンと来なかった人は以下進んで下さい。

値に変更を加える、2種類の処理方法

Go言語では(他の言語でもですが…)関数やメソッドの引数に変数を渡すことも、
変数のポインタを渡すこともできます。
そして、変数を書き換える関数の実装には以下のように2通りの方法があります。

package main

import "fmt"

// A: 変数を受け取り、returnで新しい変数を返す
func plusOne(n int) int {
    return n + 1
}

// B: 変数のポインタを受け取り、直接書き換える
func plusOnePt(n *int) {
    *n++
}

func main() {
    // Aの使用側
    n1 := 10
    n1 = plusOne(n1)
    fmt.Println(n1) // 11

    // Bの使用側
    n2 := 10
    plusOnePt(&n2)
    fmt.Println(n2) // 11
}

変数のポインタを受け取って、新しい変数のポインタを作って返却する方式もありますが、
とりあえず「とある変数を元のスコープとは別の場所で直接書き換えるかそうでないか」
という比較がしたいので、2パターンのみ記載してます。

また、簡単のため以降の章でもreturnで新しい値を返す方式をA方式、
ポインタの指す値を直接書き換える方式をB方式と表現します。

どちらを使うべきか

直感で何となく分かるかと思いますが、基本的にはAを使うべきです。
関数に突っ込んだ値が使用側のコードとは違う場所でいつの間にか書き換わる
というのは直感的ではないですし、出現頻度的にもAの方式のほうが「なんとなく自然」
であることはそれなりの量のコードを書いてこられた方は実感できるのではないでしょうか。

パターンBが必要になる場面

さて、タイトルの回収です。

「素直に、入口に値を入れたら出口から出てきてくれればいいじゃん」
と思う人も多いかと思いますが、そうは問屋が卸さないパターンがあります。

  • interface{}を返す実装を避けたいとき★本題
  • 愚直な実装ではメモリを大量消費してしまうとき
  • ガベージコレクションを減らしたいとき
  • etc...

まさかり飛んできそうなので一応リストにはしましたが、
メモリ管理については今回の本題ではないのでそちらの話は省きます。

interface{} を返す実装を避けるとき

返却値が interface{} になってしまう場合。
言い換えると、何を返却すればいいのか関数の実装段階で不定ななもの、ですね。

例として encoding/json のMarshalとUnmarshal見比べてみましょう。

// (前準備: 以下のような構造体があったとします。)
type Message struct {
    Name string
    Body string
    Time int64
}

まずはMarshalです。
関数シグネチャは func Marshal(v interface{}) ([]byte, error)
使い方は以下、returnされた値を受け取る方式(A方式)ですね。

m := Message{"Alice", "Hello", 1294706395881547000}
b, err := json.Marshal(m)

次はUnmarshalです。
関数シグネチャは func Unmarshal(data []byte, v interface{}) error
使い方は以下、ポインタを渡して値を詰め込んでもらう方式(B方式)です。

m = Message{
    Name: "Alice",
    Body: "Hello",
    Time: 1294706395881547000,
}

b := []byte(`{"Name":"Bob","Food":"Pickle"}`)
var m Message
err := json.Unmarshal(b, &m)

この2つ、同じJSONを扱う関数なのになぜ処理の仕方が違うのか?
結論から言うと、「返却したい値」が不定の場合にパターンBを使用します。

  • Marshalは常に []byte を返す(関数実装時に決定している)
  • UnmarshalはJSONの値を詰め込んだ任意のstruct (関数実装時には不定)

Marshal, Unmarshal共に、組み込み型に加えユーザーが好き勝手作った構造体を扱わなければなりません。
つまり、これらの関数が作られた段階では引数・返却値として扱うべき構造体がそもそも存在しないのです。
この未知の構造体を、Marshalは「入力」として、Unmarshalは「出力」として受け付ける
という違いがあります。

仮にUnmarshalをMarshal同様にA方式で実装すると、関数シグネチャはこの様になります。
func Unmarshal(data []byte) (v interface{}, error)

さて、もしこのような実装の場合、関数利用者はどのようにJSONのUnmarshalを行うでしょうか。

m = Message{
    Name: "Alice",
    Body: "Hello",
    Time: 1294706395881547000,
}

b := []byte(`{"Name":"Bob","Food":"Pickle"}`)
maybeMessage, err := json.Unmarshal(b)
// 型アサーションする
if m, ok := maybeMessage.(Message); !ok {
    ... 
}
...

こうなりますね。関数利用者は自分がUnmarshalしたい型はMessage型だと知っているのに、
わざわざ型アサーションでjsonのunmarshal結果がMessage型であることを示さなければなりません。
Go言語ではinterface Xを満たす型であればどのような型でもinterface Xとして扱える反面、
一度interface Xとして振る舞わせてしまったら再び元の型として扱いたくても
それを暗黙的に行うことはできません。

このように、使用者側が受け取りたい型を知っているにも関わらず型アサーションの負担を強いるような場面
では、B方式を取る選択をする事が多いです。その他の例では、コードを端折ってしまいましたが僕の好きなORMである
Gormでも、やはりDBから実際に値をロードするべき入れ物が不定なため、パターンBの実装になっています。

終わりに

コードを書いていて関数やメソッドが interface{} を返すような実装になりそうなら、
パターンBの実装を検討してみてはいかがでしょうか。ただ、ポインタによる書き換えは
いわゆる「驚き最小の原則」には従っていないと個人的には思っており、利用側が
使い方を理解できるカタチで関数コメントなりドキュメントなりに記しておく必要もあります。
最終的には「どちらの実装になっていればより利用者が楽できるだろうか」が
判断基準になってくるのかな、と思います。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?