引数で指定した型で出力する型を変更するTips

課題感

goは静的型付き言語でfuncの返り値に指定した型は厳格に守る必要があります。
一方で返り値のみ異なるケースで内部の実装の詳細が型以外ほぼ同じような関数を定義したいときは結構あります。
goだと特に同じようなコードを書くことになりがちでこれを共通化したいというユースケースを考えます。

ユースケース

HTTPリクエス

指定したpathのレスポンスをstructに詰め直して返り値を受け取りたいケースを考えるとベタに書いたらこんな風になるんじゃないかと思います。

type User struct {
    ID   int64  `json:"id"`
    Name string `json:"name"`
}

func getUser(url string) (*User, error) {
    u := &User{} 
    resp, err := http.Get(url)
    if err != nil {
        return err
    }

    b, _ := ioutil.ReadAll(resp)
    if err := json.Unmarshal(b, u); err != nil {
        return nil, err
    }
    return u, nil
} 

type Game struct {
    ID    int64  `json:"id"`
    Title string `json:"title"`
}

func getGame(url string) (*Game, error) {
    g := &Game{} 
    resp, err := http.Get(url)
    if err != nil {
        return err
    }

    b, _ := ioutil.ReadAll(resp)
    if err := json.Unmarshal(b, g); err != nil {
        return nil, err
    }
    return g, nil
} 

goらしく同じような書き方になります。最初の説明でも記載した通りここで違うのは関数内で返り値を返すためのstructの初期化のみです。

シグネチャ

以下のようなfuncを考えます。

func HogeSetter(url string, out interface{}) error {
    resp, err := http.Get(url)
    if err != nil {
       return err
    }

    b, _ := ioutil.ReadAll(resp)
    if err := json.Unmarshal(b, out); err != nil {
        return err
    }

    return nil 
}

説明

上記のシグネチャのようなメソッドを使う上での挙動の説明をします。

  • url: リソースのレスポンスを受け取るurlです。ここには https://www.example.com/usershttps://www.example.com/games といった文字列が入ります。
  • out: ここが今回のポイントです。interface型で受け取ることで引数で指定した型の参照型を注入することでメソッドないでjson.Unmarshalを行い、参照型にしたstructにjsonをmappingすることができます。

このシグネチャでは上記のユースケースに記載したような 共通処理 を抽象化できます。urlや詰め込みたい型は外部入力の形で受け取ってメソッド内部で柔軟に型を入れ替えても詰め替えることができるようにします。

実際に使うときは以下のように使うことになると思います。

ユースケース

func get(url string, out interface{}) error {
    resp, err := http.Get(url)
    if err != nil {
       return err
    }

    b, _ := ioutil.ReadAll(resp)
    if err := json.Unmarshal(b, out); err != nil {
        return err
    }

    return nil 
}

func main(){
    u := &User{}
    if err := get("https://www.example.com/users", u); err != nil {
        panic(err)
    }
     
    g := &Game{}
    if err := get("https://www.example.com/games", g); err != nil {
        panic(err)
    }
}

このように共通化可能です。

sample

実際にこうした Setterっぽいけど実装の意図としてはGetterのようなコードがちゃんと動くことを確認します。

package main

import (
    "encoding/json"
    "fmt"
)

type X struct {
    ID int64 `json:"id"`
}

type Y struct {
    Name string `json:"name"`
}

func main() {
    j := []byte(`{"id": 1}`)
    x := &X{}
    if err := typeSetter(j, x); err != nil {
        panic(err)
    }
    fmt.Printf("%#v\n", x)

    jj := []byte(`{"name": "taro"}`)
    y := &Y{}
    if err := typeSetter(jj, y); err != nil {
        panic(err)
    }
    fmt.Printf("%#v", y)
}

func typeSetter(input []byte, out interface{}) error {
    if err := json.Unmarshal(input, out); err != nil {
        return err
    }
    return nil
}

// output
// &main.X{ID:1}
// &main.Y{Name:"taro"}

ref: https://play.golang.org/p/PFb5Do_WJVA ここでRunを押下することで確認できます。

まとめ

goは厳密に型チェックするがゆえに冗長なコードを生み出しがちですが、このエントリで紹介した共通化の書き方はとても便利だなと思いました。