Golang 入門

Go 構造体メソッド

構造体(Struct)がデータの「骨組み」を定義するものであるならば、メソッド(Methods)はそのデータに真の「魂」と「振る舞い」を吹き込むものです。

Go言語におけるメソッドとは、実のところ特定の型(通常は構造体)に作用する特殊な関数にすぎません。Goには伝統的な意味での class(クラス)キーワードは存在しませんが、メソッドを構造体にバインドすることで、カプセル化や振る舞いの定義といったオブジェクト指向プログラミング(OOP)の核心的な概念を完璧に実現しています。

本章では、メソッドの定義方法を深く掘り下げ、値レシーバ(Value Receiver)とポインタレシーバ(Pointer Receiver)の重要な違いを徹底的に理解し、実戦でそれらを自在に使いこなす術を学びます。

1. メソッドの定義

Go言語において、メソッドは通常の関数とほぼ同じように見えますが、唯一の決定的な違いがあります。それは、func キーワードとメソッド名の間に、レシーバ(Receiver)という特殊なパラメータが存在することです。

このレシーバによって、そのメソッドがどの具体的な型に「属している」のかを宣言します。

1.1 基本構文

func (レシーバ変数 レシーバ型) メソッド名(パラメータリスト) (戻り値リスト) {
    // メソッドのロジック
}

1.2 構造体の面積計算:実装例

Rectangle(長方形)構造体を定義し、それに対して面積を計算する Area メソッドをバインドしてみましょう。

package main

import "fmt"

type Rectangle struct {
    Width  float64
    Height float64
}

// Area メソッドは Rectangle 型にバインドされています
// 'r' はレシーバの変数名です(他言語の this や self に相当しますが、Go では型の頭文字の小文字が一般的です)
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func main() {
    rect := Rectangle{Width: 10, Height: 5}
    
    // ドット (.) を使用して、構造体インスタンス上のメソッドを呼び出します
    area := rect.Area() 
    
    fmt.Println("長方形の面積:", area) // 出力: 長方形の面積: 50
}

2. 核心的な対決:値レシーバ vs. ポインタレシーバ

メソッドを定義する際、非常に重要な選択を迫られます。それは、レシーバを「値(Value)」にするか「ポインタ(Pointer)」にするかという点です。これが、メソッド内部で外部のオリジナルデータを変更できるかどうかを直接左右します。

2.1 値レシーバ (Value Receivers)

値レシーバを使用する場合、メソッドが受け取るのは呼び出し元構造体の「コピー(Copy)」です。メソッド内部でレシーバに対して行ったいかなる変更も、外部のオリジナルデータには一切影響しません。

package main

import "fmt"

type Circle struct {
    Radius float64
}

// ⚠️ これは【値レシーバ】メソッドです
func (c Circle) Scale(factor float64) {
    c.Radius = c.Radius * factor // ここで変更しているのは c のコピーにすぎません!
    fmt.Println("メソッド内部の半径:", c.Radius)
}

func main() {
    circle := Circle{Radius: 5}
    circle.Scale(2)
    
    // オリジナルは 5 のまま、拡大されていません!
    fmt.Println("外部のオリジナル半径:", circle.Radius) // 出力: 外部のオリジナル半径: 5
}

2.2 ポインタレシーバ (Pointer Receivers)

ポインタレシーバを使用する場合、メソッドが受け取るのは呼び出し元構造体のメモリ番地(ポインタ)です。つまり、メソッド内部での変更は、外部のオリジナルデータを直接書き換えることになります。

package main

import "fmt"

type Circle struct {
    Radius float64
}

// これは【ポインタレシーバ】メソッドです(型名の前の * に注目)
func (c *Circle) Scale(factor float64) {
    c.Radius = c.Radius * factor // ポインタを介してオリジナルデータを書き換えます
    fmt.Println("メソッド内部の半径:", c.Radius)
}

func main() {
    circle := Circle{Radius: 5}
    circle.Scale(2) // Go は自動的に背後で circle を &circle に変換して渡します
    
    // オリジナルが正常に変更されました!
    fmt.Println("外部のオリジナル半径:", circle.Radius) // 出力: 外部のオリジナル半径: 10
}

3. どちらを選択すべきか?

コードの明晰さとパフォーマンスを両立させるため、以下の比較表を参考にレシーバを選択してください。

考慮すべき要因推奨されるレシーバ理由の分析
オリジナルデータの変更が必要ポインタレシーバ (*T)スコープを超えて構造体内部のステートを更新するには、ポインタを渡す必要があります。
データの読み取りのみ値レシーバ (T)計算や表示を行うだけでステートを変更しない場合、値レシーバの方が安全(予期せぬ改ざんを防げる)です。
構造体が巨大であるポインタレシーバ (*T)構造体に数十個のフィールドがある場合、値レシーバでは呼び出しのたびに全メモリコピーが発生し、パフォーマンスを損ないます。ポインタ(8バイト)の伝達は極めて効率的です。
一貫性の原則基本的にポインタに寄せるその構造体で一つでもポインタレシーバが必要なメソッドがあるなら、スタイル統一のために全メソッドをポインタレシーバにすることを推奨します。

4. 実戦ケーススタディ

複雑なビジネスロジックにおいて、メソッドがどのように真価を発揮するかを見ていきましょう。

4.1 ケース 1:銀行口座管理 (ステートの変更)

銀行のアカウントにおける入金や出金は、当然ながら残高を更新するため、ポインタレシーバが必須です。一方で、残高照会は読み取り専用であるため、値レシーバで事足ります。

package main

import (
    "fmt"
    "errors"
)

type BankAccount struct {
    AccountNumber string
    Balance       float64
}

// Deposit 入金:残高を更新するため【ポインタレシーバ】を使用
func (account *BankAccount) Deposit(amount float64) {
    account.Balance += amount
}

// Withdraw 出金:残高を更新し、エラー情報を返すため【ポインタレシーバ】を使用
func (account *BankAccount) Withdraw(amount float64) error {
    if amount > account.Balance {
        return errors.New("残高不足です")
    }
    account.Balance -= amount
    return nil
}

// GetBalance 照会:読み取りのみのため【値レシーバ】で十分
func (account BankAccount) GetBalance() float64 {
    return account.Balance
}

func main() {
    acc := BankAccount{AccountNumber: "622202", Balance: 1000}
    
    acc.Deposit(500)
    fmt.Println("入金後の残高:", acc.GetBalance()) // 出力: 1500
    
    err := acc.Withdraw(2000)
    if err != nil {
        fmt.Println("出金失敗:", err) // 出力: 出金失敗: 残高不足です
    }
}

4.2 ケース 2:標準の Stringer インタフェースの実装 (出力フォーマットのカスタマイズ)

Go言語には Stringer という非常に有名な組み込みインタフェースがあります。自身の構造体に String() string というシグネチャのメソッドをバインドするだけで、fmt.Println などでその構造体を出力する際、カスタマイズしたフォーマットを自動的に使用してくれます。

package main

import "fmt"

type Point struct {
    X, Y int
}

// String() メソッドを実装して出力表示をカスタマイズ
func (p Point) String() string {
    return fmt.Sprintf("座標位置: [%d, %d]", p.X, p.Y)
}

func main() {
    point := Point{X: 10, Y: 20}
    
    // fmt.Println は自動的に point.String() を検知して呼び出します
    fmt.Println(point) // 出力: 座標位置: [10, 20]
}