🔩 ねじき教室 Go と Web の教室

struct とメソッド

読了目安 約3分

添字で対応づけていたデータに struct で名前を付け、メソッドとレシーバの使い分けを学びます。

この章の目次

競プロでは、ユーザーの名前を names[i]、点数を scores[i] に入れ、添字で対応づけることがよくあります。

添字の対応から、名前のあるまとまりへ

この対応は、書いた本人の頭の中にしかありません。 属性が増えるほど配列も増え、1 本だけ並び替え忘れる事故も起きます。

struct(構造体)は、複数のデータをひとまとまりにして名前を付ける仕組みです。

Go
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 本になります。

Go
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)
	}
}

メソッド

メソッドは、特定の型に結びつけた関数です。 関数名の前にレシーバ(その型の値を受け取る特別な引数)を書くと、値に対してドットで呼び出せます。

Go
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) を使います。

Go
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 です(何をどう作るか)。

試してみよう

次のコードの穴を埋めて、いいねを数えられる投稿を作ってください。

Go
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 以上なら『人気』を返す」メソッドも足してみましょう。