struct とメソッド
読了目安 約3分
添字で対応づけていたデータに struct で名前を付け、メソッドとレシーバの使い分けを学びます。
この章の目次
競プロでは、ユーザーの名前を names[i]、点数を scores[i] に入れ、添字で対応づけることがよくあります。
添字の対応から、名前のあるまとまりへ
この対応は、書いた本人の頭の中にしかありません。 属性が増えるほど配列も増え、1 本だけ並び替え忘れる事故も起きます。
struct(構造体)は、複数のデータをひとまとまりにして名前を付ける仕組みです。
package main
import "fmt"
type User struct {
Name string
Score int
}
func main() {
u := User{Name: "neziki", Score: 1200}
fmt.Println(u.Name, u.Score)
u.Score += 100
fmt.Println(u)
}type User struct {...} が定義、User{...} がリテラル(値をその場で作る記法)で、フィールドには u.Name のようにドットでアクセスします。
順序だけの書き方はフィールドの追加や並び替えに追従できないので、フィールド名を明示してください。
競プロの 2 本の配列は、struct の slice 1 本になります。
package main
import "fmt"
type User struct {
Name string
Score int
}
func main() {
users := []User{
{Name: "alice", Score: 1500},
{Name: "bob", Score: 900},
{Name: "carol", Score: 2100},
}
for _, u := range users {
fmt.Printf("%s: %d 点\n", u.Name, u.Score)
}
}メソッド
メソッドは、特定の型に結びつけた関数です。 関数名の前にレシーバ(その型の値を受け取る特別な引数)を書くと、値に対してドットで呼び出せます。
package main
import "fmt"
type User struct {
Name string
Score int
}
func (u User) Rank() string {
if u.Score >= 2000 {
return "上級"
}
if u.Score >= 1000 {
return "中級"
}
return "初級"
}
func main() {
u := User{Name: "neziki", Score: 1200}
fmt.Println(u.Name, "は", u.Rank())
}func (u User) Rank() の (u User) がレシーバです。
メソッドにすると、「この処理は User のものだ」という所属がコードに現れます。
ポインタレシーバと値レシーバ
さきほどの (u User) は値レシーバで、struct のコピーが渡るため、変更しても呼び出し元は変わりません。
呼び出し元の値を変更するには、値の場所(ポインタ)が渡るポインタレシーバ (u *User) を使います。
package main
import "fmt"
type User struct {
Name string
Score int
}
// 値レシーバ: コピーを変更するので、呼び出し元は変わらない
func (u User) AddScoreBroken(delta int) {
u.Score += delta
}
// ポインタレシーバ: 呼び出し元の値そのものを変更する
func (u *User) AddScore(delta int) {
u.Score += delta
}
func main() {
u := User{Name: "neziki", Score: 1200}
u.AddScoreBroken(100)
fmt.Println("値レシーバの後:", u.Score)
u.AddScore(100)
fmt.Println("ポインタレシーバの後:", u.Score)
}変更するメソッドが 1 つでもあるなら、その型のメソッドはすべてポインタレシーバに統一するのが Go の慣習です。 混ざっていると、「呼び出し元を変えるのか」を毎回考えることになるからです。
ユーザーも投稿も struct になる
Web サービスが扱う対象は、ユーザー、投稿、コメント、セッションのように「複数の属性を持つひとまとまり」ばかりです。 Part 5 で作る掲示板でも、中心はユーザーと投稿を表す 2 つの struct です(何をどう作るか)。
試してみよう
次のコードの穴を埋めて、いいねを数えられる投稿を作ってください。
package main
import "fmt"
type Post struct {
Title string
Likes int
}
// ここに、いいねを 1 増やすメソッド Like を書く
func main() {
p := Post{Title: "はじめての投稿"}
// p.Like()
// p.Like()
fmt.Println(p.Title, "のいいね:", p.Likes) // 2 と表示されれば成功
}どちらのレシーバにすべきか考えてから書き、コメントアウトを外して確かめてください(値レシーバでは Likes は増えません)。
余裕があれば、Rank の例にならって「いいねが 10 以上なら『人気』を返す」メソッドも足してみましょう。