LoginSignup
51
35

More than 5 years have passed since last update.

Goの構造体の研究

Posted at

これは Origami Advent Calender の六日目の記事として書かれた、Goの構造体に関する地味な記事です。
クリスマスにふさわしいピカピカのかっこいいフレームワークやキラキラしたライブラリの話は一切書いておりません。

Go structの研究

はじめに

Goには他のオブジェクト指向言語におけるclassというものは存在しません。
関連する変数をひとまとめにする機能としてstruct(構造体)が提供されています。
普段何気なく使っているものですが、type aliasしたらJSONタグがどうなるかとか、書けばわかるけど、すぐ忘れてしまうような部分があるものです。
この記事ではそのようなstructにまつわる素朴な疑問について確認していくことにします。

基本

structの典型的な定義は以下のようなものです。

type Account struct {
    Email    string
    Password string
    Rank     int
}

このstructは何らかのサービスのアカウント情報を表していると考えてください。

これを実際に使う場合、いくつかの方法があります。

newで初期化してポインタ型変数として使う

func main() {
    account := new(Account)
    account.Email = "sample@example.com"
    account.Password = "password"
    account.Rank = 1
    fmt.Printf("%v", account)
}

compositeリテラルで初期化してかつそのアドレスを返すことでポインタ型変数として使う

func main() {
    account := &Account{
        Email:    "sample@example.com",
        Password: "password",
        Rank:     1,
    }
    fmt.Printf("%v", account)
}

compositeリテラルで初期化して使う

func main() {
    account := Account{
        Email:    "sample@example.com",
        Password: "password",
        Rank:     1,
    }
    fmt.Printf("%v", account)
}

以上のうち new(Account) と、 `&Account{} は、
初期化時に各フィールドを設定可能かという点を除いて、実用上は同一です。

大きく異なるのはポインタ型の変数を作るこれらとは違って値を作り出す Account{} です。
この差は構造体にメソッドを作ったり、関数の引数にした時に明らかになります。

package main

import "fmt"

type Account struct {
    Email    string
    Password string
    Rank     int
}

func updatePassword(account Account, password string) {
    account.Password = password
    fmt.Printf("account.Password(func)  : %s\n", account.Password)
    fmt.Printf("account.Password(pointer): %p\n", &account)
}

func updatePasswordP(account *Account, password string) {
    account.Password = password
    fmt.Printf("accountP.Password(func)  : %s\n", account.Password)
    fmt.Printf("accountP.Password(pointer): %p\n", account)
}

func main() {
    account := Account{
        Email:    "sample@example.com",
        Password: "password",
        Rank:     1,
    }
    fmt.Printf("account.Password(pointer): %p\n", &account)
    fmt.Printf("account.Password(before): %s\n", account.Password)
    updatePassword(account, "new password")
    fmt.Printf("account.Password(after) : %s\n", account.Password)

    fmt.Println("-----------------")
    accountP := &Account{
        Email:    "sample@example.com",
        Password: "password",
        Rank:     1,
    }
    fmt.Printf("accountP.Password(pointer): %p\n", accountP)
    fmt.Printf("accountP.Password(before): %s\n", accountP.Password)
    updatePasswordP(accountP, "new password")
    fmt.Printf("accountP.Password(after) : %s\n", accountP.Password)
}

実行結果はこのようになるはずです(pointerの値は実行のたびに変わるでしょう)。

$ go run ac.go 
account.Password(pointer): 0xc420014150
account.Password(before): password
account.Password(func)  : new password
account.Password(pointer): 0xc420014180
account.Password(after) : password
-----------------
accountP.Password(pointer): 0xc4200141b0
accountP.Password(before): password
accountP.Password(func)  : new password
accountP.Password(pointer): 0xc4200141b0
accountP.Password(after) : new password

上のコードを実行してみると分かる通り、構造体のポインタを引数にした場合は、
渡した構造体のフィールドの内容が更新されていますが、構造体の値を引数にした場合、
値がコピーされてしまうため、関数内での変更は元の変数に影響しません。
メソッドでも同様ですが、これは宿題とします。

JSONタグとtype alias, 埋め込み

次に構造体に対してtype aliasや埋め込みを利用した時にJSONタグがどのように機能するかを調べます

Goでは構造体のフィールドにタグをつけることでメタ情報を付与できます。
タグは利用できる場所は狭いものの、メタ情報の付与という意味では、Javaにおけるアノテーションに近い機能です。

おそらく最もよく使われるタグはJSONタグです。このタグは標準パッケージの encoding/json を使って、
構造体をJSONに変換する際に、各フィールドをどのように扱うかについての情報を付与するために用います。
encoding/json はJSONタグがない場合、構造体の各フィールドの名前をそのままキーとして用います。
Goではある値や関数がpublicかどうかを、それの名前がアルファベットの大文字で始まっているかどうかで決めるため、
JSONタグを使わないとキーが使われてしまい、非常に気持ち悪いJSONが作られてしまいます。
例えば Account をそのまま encoding/json でJSONにすると、こんなことになります

{"Email":"sample@example.com","Password":"password","Rank":1}

ということでまずは早速、Account にJSONタグを付与してJSONにエンコード、
そしてJSON文字列からGoの構造体へデコードしてみましょう。

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
)

type Account struct {
    Email    string `json:"email"`
    Password string `json:"password"`
    Rank     int    `json:"rank"`
}

func main() {
    account := &Account{
        Email:    "sample@example.com",
        Password: "password",
        Rank:     1,
    }

    // encode
    buf := new(bytes.Buffer)
    json.NewEncoder(buf).Encode(account)
    fmt.Println(buf.String())

    // decode
    decoded := new(Account)
    json.NewDecoder(buf).Decode(decoded)
    fmt.Println(decoded)
}
// 実行結果
// {"email":"sample@example.com","password":"password","rank":1}
//
// &{sample@example.com password 1} 

注意点としては Decode にはポインタを渡す必要があるところです。
理由は前の節に書いた通りです。
さて、これとtype aliasを組み合わせるとどうなるでしょう。

// package, import, Account構造体定義省略

type UserAccount Account

func main() {
    account := &UserAccount{
        Email:    "sample@example.com",
        Password: "password",
        Rank:     1,
    }

    // encode
    buf := new(bytes.Buffer)
    json.NewEncoder(buf).Encode(account)
    fmt.Println(buf.String())

    // decode
    decoded := new(UserAccount)
    json.NewDecoder(buf).Decode(decoded)
    fmt.Println(decoded)
}
// 実行結果
// {"email":"sample@example.com","password":"password","rank":1}
//
// &{sample@example.com password 1}

このようにalias typeにもJSONタグは引き継がれます
では埋め込みの場合はどうでしょう。

// package, import, Account構造体定義省略

type AccountParam struct {
    Account
}

func main() {
    account := new(AccountParam)
    account.Email = "sample@example.com"
    account.Password = "password"
    account.Rank = 

    // encode
    buf := new(bytes.Buffer)
    json.NewEncoder(buf).Encode(account)
    fmt.Println(buf.String())

    // decode
    decoded := new(AccountParam)
    json.NewDecoder(buf).Decode(decoded)
    fmt.Println(decoded)
}
// 実行結果
// {"email":"sample@example.com","password":"password","rank":1}
//
// &{sample@example.com password 1}

単純な埋め込みの場合もJSONタグは引き継がれました。
では、埋め込んだ構造体にフィールドを追加した場合、特に埋め込まれた構造体が持つフィールドと
同名のフィールドを追加して、別のタグを設定したらどうなるでしょう。

先ほどのコードのうち、 type AccountParam struct の定義を以下のように書き換えて再実行してみてください。

type AccountParam struct {
    Account
    PasswordCurrent string `json:"password_current"`
    Rank            int    `json:"user_rank"`
}

再実行結果
```
{"email":"sample@example.com","password":"password","rank":0,"password_current":"password_now","user_rank":1}

&{{sample@example.com password 0} password_now 1}
```

追加したPasswordCurrent(password_current)は問題ないようです。
しかしRankについては、埋め込まれた側のRank(user_rank)も、埋め込んだ側のRank(rank)もJSONに出てきてしまいました。
Rank(rank)の値が0なのは account.Rank = 1 でアクセスしているのは Rank(user_rank) だからです。

ただしこれはあくまでデフォルトの変換ルールによるもので、 encoding/json は任意の変換をするための機能も提供しています。
デフォルトの変換は便利ではありますが、ある程度複雑な変換を必要とする場合、自分で書いてしまった方がよいでしょう。

構造体の埋め込みは is-a 関係を作るか

構造体の埋め込みについても少し調べて見ます。
しばしば構造体の埋め込みを他の言語の継承と同様であると語る人がいますが、それは正しいでしょうか。
継承なのであれば埋め込まれた型と、埋め込んだ型の間に、 is-a 関係があるので、
親となる型の変数に、子の値を代入することができるはずです。やってみましょう。

package main

type Account struct {
    Email    string `json:"email"`
    Password string `json:"password"`
    Rank     int    `json:"rank"`
}

type AccountParam struct {
    Account
    PasswordCurrent string `json:"password_current"`
}

func main() {
    var a *Account
    a = new(AccountParam)
}

おそらく、

cannot use new(AccountParam) (type *AccountParam) as type *Account in assignment

というエラーが出たはずです。つまり AccountAccountParam には is-a 関係はありません。
埋め込んだ方が持つフィールドと同名のフィールドを定義した場合の挙動を考えれば分かりますが、
Goの構造体の埋め込みは、 has-a 関係の特殊な形式でしかありません。
has-a であるため、このようなことが可能です。

func main() {
    a := AccountParam{}
    b := Account{}

    a.Account = b

    fmt.Println(a)
}

構造体はあくまで関連する変数をまとめるための機能であり、他の言語におけるclassの代替物ではないわけです。

このことは設計上大きな意味を持ちます。
Goにおけるオブジェクト指向的な機能、設計の真骨頂はinterfaceとメソッドにあり、
教科書的ではありますが、設計を考える場合はまずinterfaceから始めるべきでしょう。

その具体的な手法については別の機会に譲りたいと思います。

まとめ

  • 値とポインタの違いは大事
  • 標準のJSON変換は埋め込みでJSONタグのオーバーライドを許可しない
  • 構造体はclassではなく、継承は存在しない
  • Goは楽しい

We are hiring

採用云々とは別に、外部の人とタダ飯を食べられる権利(意訳)という福利厚生があるので、
Goのあまりピカピカキラキラしてない話や、設計について飲み食いしながら語りたい方がいましたら、
連絡お待ちしております。

51
35
1

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
51
35