【Go言語入門】8.構造体についてさくっと学ぶ

構造体の学習はC言語以来だったけど、クラスのないGoでは要となってくるし、ここでしっかりおさえておきたいと思います。

今回は

前回の記事

定義

構造体はフィールドの集合体です。あらかじめ任意の単位に区切ってデータ群を定義できます。

たいやきの型だとか設計書だとかいろんなものに例えられますが、過去に入門書を読み漁った結果、アプリケーションの一部っぽいソースコードを見るほうがわかりやすかったというのが個人的見解です。

アプリケーションを想定してみる

package main

import "fmt"

type User struct {
  Id          int
  Name        string
  Description string
  Follow      int
  Follower    int
}

func main() {
  u1 := User{}
  u2 := new(User)
  fmt.Println(u1, u2)
}
// >>> {0 Ken  0 0} &{0 Mio  0 0}

何らかのSNSを開発していると仮定してその中のユーザを想像してみてください。

実務でここまで単調になることはないですが、上で説明した任意の単位というというのがなんとなく掴めるかと思います。

mainの中でに種類の初期化方法を書きましたが、両者の違いは関数newを使った初期化は出力に&がついているというところです。

これは関数newで構造体を初期化した場合、返り値がポインタになるからです。

試しに型をチェックしてみると、

fmt.Printf("u1: %T, u2: %T", u1, u2)
// >>> u1: main.User, u2: *main.User

このようになります。

なので初期化時、u1 := &User{}とすると変数u1の型はポインタ型となります。

これは関数newでの初期化と同一です。

今回は「構造体と普段見るアプリケーションとを結びつけて雰囲気をつかむ」というところに焦点を絞っているので、ポインタの説明は前回の記事を参考にしてください。

では「ユーザ1がユーザ2をフォローする」という部分を切りだしてみます。

package main

import "fmt"

type User struct {
  Id          int
  Name        string
  Description string
  Follow      int
  Follower    int
}

func follow(u1, u2 User) {
  u1.Follow++
  u2.Follower++
}

func main() {
  ken := User{
    Id:          0,
    Name:        "Ken",
    Description: "大学生です!よろしく!",
    Follow:      0,
    Follower:    0,
  }
  mio := User{
    Id:          1,
    Name:        "Mio",
    Description: "美容学生してます",
    Follow:      0,
    Follower:    0,
  }
  follow(ken, mio)
  fmt.Println(ken, mio)
}

// >>> {0 Ken 大学生です!よろしく! 0 0} {1 Mio 美容学生してます 0 0}

KenさんがMioさんにフォローする、といった処理を書いてみましたが、残念ながら値は変わっていません。

これはどうなっているかというと、関数followを実行した際、引数にUser型の2つの変数が渡されておりますが、厳密には変数ken変数mio自体を扱っているわけではなく、値が同一のコピーです

つまり、一時的に作られたコピーの値は変更されているが、本体の値は変わっていないということになります。(したがって値の取得や値を使った演算結果を返り値にする、ということは問題なくできます。)

これをうまくやるにはまたしてもポインタを使います。

func follow(u1, u2 User) {
  u1.Follow++
  u2.Follower++
}
↓
func follow(u1, u2 *User) {
  u1.Follow++
  u2.Follower++
}
...
follow(ken, mio)
↓
follow(&ken, &mio)

// >>> {0 Ken 大学生です!よろしく! 1 0} {1 Mio 美容学生してます 0 1}

これで値が変更されているのを確認できました。

構造体はこのような形で使用していきます。

オブジェクト指向言語であれば、クラスがここの役割を担います。

ちなみにさり気なく書いているので気づきにくいですが、User型を引数で受け取った後、u1.Followという書き方をしておりますが、〇〇.△△という書き方でフィールドへのアクセスが可能です。

func main() {
  u1 := User{}
  u2 := new(User)
  u1.Follow += 1
  u2.Follower += 1
}

直感的なので呑み込めるかと思います。

またこのようにした場合、先程の例と違ってコピーではないので、しっかり値が変更されます。

ここで疑問に感じてほしい点がもう一点ありまして、u1とu2では根本的に型が違います。(User型とUser型のポインタ)

何故どちらとも同様の書き方でフィールドにアクセスできているのかという点です。ポインタから値を引き出すには*変数でしたよね。

これは構造体のポインタがフィールドにアクセスする場合、p.fieldと書いても(*p).fieldとプログラム側で暗黙的に解釈してくれます。

こういった暗黙的変換は見落としがちなので、しっかり頭に入れておきましょう。

構造体については他の章と関連付いている部分が非常に多いので、今回は本当にさらっとしか触れてません。

後の記事で徐々に補足していきます。