メインコンテンツまでスキップ

メソッドとインターフェース

.NET (C#) の経験がある開発者向けに、メソッドレシーバ、暗黙的インターフェース実装、小さなインターフェース設計を解説します。

1. メソッドとレシーバ​

Go にクラスはありません。任意の名前付き型に対して、レシーバ付き関数としてメソッドを定義します。

type Counter struct {
n int
}

// ポインタレシーバ: 元の値を変更できる
func (c *Counter) Inc() { c.n++ }

// 値レシーバ: コピーに対して動作する
func (c Counter) Value() int { return c.n }

メソッドは型の定義の外 (同じパッケージ内であればどのファイルでも) に書けます。C# の拡張メソッドに近い感覚ですが、他パッケージの型にメソッドは追加できません。

値レシーバとポインタレシーバの使い分け​

観点ポインタレシーバ (c *T)値レシーバ (c T)
状態の変更できるできない (コピーが変わるだけ)
コピーコスト小さい構造体が大きいと高い
sync.Mutex 等を含む必須 (コピー禁止)不可
向いている型状態を持つ型、サービス小さな不変の値 (time.Time など)

指針:

  • 迷ったらポインタレシーバにする
  • 1つの型のレシーバは統一する (値とポインタを混在させない)
  • 変更しない小さな値型 (座標、金額など) は値レシーバでよい
var c Counter
c.Inc() // 変数なら自動的に (&c).Inc() に解釈される
fmt.Println(c.Value())

2. 暗黙的インターフェース実装​

Go のインターフェースは構造的型付けです。implements のような宣言は不要で、必要なメソッドを持っていれば自動的にそのインターフェースを満たします。

type Notifier interface {
Notify(msg string) error
}

type EmailNotifier struct{ Addr string }

func (e *EmailNotifier) Notify(msg string) error {
fmt.Printf("send to %s: %s\n", e.Addr, msg)
return nil
}

// EmailNotifier は Notifier を「宣言なしに」満たす
var n Notifier = &EmailNotifier{Addr: "[email protected]"}

利点​

  • 定義元と利用側が疎結合: 実装側はインターフェースを知らなくてよい。標準ライブラリや外部ライブラリの型にも、利用側で独自のインターフェースを当てはめられる
  • インターフェースを使う側 (消費者) が定義するのが Go の慣習。必要なメソッドだけを宣言できる
  • テスト用のモックやスタブを作りやすい

注意点​

意図せず満たしてしまう、または満たしているつもりで満たしていないことがあります。コンパイル時に保証したい場合は次のイディオムを使います。

// *EmailNotifier が Notifier を満たさなければコンパイルエラーになる
var _ Notifier = (*EmailNotifier)(nil)

メソッドセットの落とし穴​

値 T のメソッドセットには値レシーバのメソッドだけが含まれ、*T には両方が含まれます。ポインタレシーバで実装した場合、値をインターフェースに代入するとエラーになります。

var n Notifier = EmailNotifier{} // コンパイルエラー: Notify は *EmailNotifier のメソッド
var n Notifier = &EmailNotifier{} // OK

3. .NET との対応表​

.NET (C#)Go
class Foo : IBar (明示的に実装)構造的型付け (暗黙的に満たす)
拡張メソッドレシーバ付きメソッド (同一パッケージ内)
継承 (class A : B)継承なし。構造体の埋め込みで委譲
抽象クラス / virtual / overrideなし。インターフェースと関数値で表現
object / dynamicany / 型アサーション / type switch
is / as型アサーション v, ok := x.(T)
switch パターンマッチング (型)type switch
インターフェースのデフォルト実装なし
IDisposableio.Closer (Close() error)
IEnumerable<T>iter.Seq[T] (Go 1.23+) / スライス

4. 小さなインターフェース設計​

Go の標準ライブラリには、メソッドが1つだけの有名なインターフェースが多数あります。

type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
type Stringer interface { String() string }

設計の指針:

  • インターフェースは小さく (1〜3メソッド)。大きいほど実装も差し替えも難しくなる
  • IUserRepository のような巨大インターフェースを実装側に置かない。利用側に必要な分だけ定義する
  • Accept interfaces, return structs: 引数にはインターフェース、戻り値には具体型を使う
  • 実装が1つしかない段階で、先回りしてインターフェースを作らない
// 利用側 (service パッケージ) が必要なメソッドだけを宣言する
type userStore interface {
FindByID(ctx context.Context, id int) (*User, error)
}

type UserService struct {
store userStore
}

// 戻り値は具体型。呼び出し側が必要に応じてインターフェースに当てはめる
func NewUserService(store userStore) *UserService {
return &UserService{store: store}
}

インターフェースの合成​

小さなインターフェースを組み合わせて大きなものを作れます。

type ReadWriter interface {
Reader
Writer
}

構造体の埋め込み​

継承の代わりに、構造体を埋め込んでメソッドを昇格させ、委譲を簡潔に書けます。

type Logger struct{ prefix string }

func (l *Logger) Log(msg string) { fmt.Println(l.prefix + msg) }

type Service struct {
*Logger // 埋め込み: s.Log(...) と呼べる
name string
}

s := Service{Logger: &Logger{prefix: "[svc] "}, name: "api"}
s.Log("started")

これは is-a 関係ではなく委譲です。Service を *Logger として扱うことはできません。

5. 型アサーションと type switch​

インターフェース値から具体型を取り出すには型アサーションを使います。

var x any = "hello"

s, ok := x.(string) // ok 形式: 失敗しても panic しない
if ok {
fmt.Println(len(s))
}

n := x.(int) // ok なし: 型が違うと panic

複数の型で分岐する場合は type switch を使います。

func describe(x any) string {
switch v := x.(type) {
case nil:
return "nil"
case int:
return fmt.Sprintf("int: %d", v)
case string:
return "string: " + v
case fmt.Stringer:
return "stringer: " + v.String()
case error:
return "error: " + v.Error()
default:
return fmt.Sprintf("other: %T", v)
}
}

any の多用は避けます。型安全性が失われるため、ジェネリクス (Goのジェネリクス) や小さなインターフェースで表現できないか先に検討します。

6. C# 開発者がつまずきやすいポイント​

nil インターフェースの罠​

インターフェース値は**(型, 値) の組**です。nil ポインタを代入すると、インターフェース自体は nil ではなくなります。

type MyErr struct{}

func (*MyErr) Error() string { return "my error" }

func run() error {
var e *MyErr = nil
return e // 型情報を持つので、返される error は nil ではない
}

if err := run(); err != nil {
fmt.Println("エラー扱いになってしまう")
}

対策: エラーがないときは return nil と明示的に書く。具象型の変数を error として返さない。

その他のポイント​

  • implements がない: 実装の把握は IDE の「実装へ移動」やコンパイル時アサーションで行う
  • インターフェースの置き場所: C# のように実装側のプロジェクトに置かず、利用側のパッケージに置く
  • ポインタレシーバの型を値で渡す: メソッドセットの違いでコンパイルエラーになる
  • 継承がない: 埋め込みは便利だが、オーバーライドやポリモーフィズムは実現しない
  • メソッドのオーバーロードがない: 同名メソッドは1つだけ。別名を付ける (ParseInt / ParseFloat)
  • ゲッター/セッター命名: GetName() ではなく Name()、SetName() はそのまま使う

7. コード例​

C#​

public interface INotifier
{
Task NotifyAsync(string message);
}

public class EmailNotifier : INotifier
{
public Task NotifyAsync(string message) => Task.CompletedTask;
}

public class OrderService
{
private readonly INotifier _notifier;
public OrderService(INotifier notifier) => _notifier = notifier;

public Task PlaceAsync() => _notifier.NotifyAsync("placed");
}

Go​

package main

import (
"context"
"fmt"
)

// 利用側 (OrderService) が必要なメソッドだけを宣言
type notifier interface {
Notify(ctx context.Context, msg string) error
}

type OrderService struct {
notifier notifier
}

func NewOrderService(n notifier) *OrderService {
return &OrderService{notifier: n}
}

func (s *OrderService) Place(ctx context.Context) error {
return s.notifier.Notify(ctx, "placed")
}

// 実装側は notifier の存在を知らない
type EmailNotifier struct{}

func (EmailNotifier) Notify(ctx context.Context, msg string) error {
fmt.Println("email:", msg)
return nil
}

// テスト用スタブも数行で書ける
type fakeNotifier struct{ got []string }

func (f *fakeNotifier) Notify(_ context.Context, msg string) error {
f.got = append(f.got, msg)
return nil
}

func main() {
svc := NewOrderService(EmailNotifier{})
_ = svc.Place(context.Background())
}

8. まとめ​

  • メソッドは任意の名前付き型にレシーバ付きで定義する。迷ったらポインタレシーバに統一する
  • インターフェースは暗黙的に満たされる。implements 宣言は不要で、利用側が必要な分だけ定義する
  • 小さなインターフェースを好み、Accept interfaces, return structs を基本にする
  • 継承はなく、埋め込みによる委譲とインターフェースの合成で再利用する
  • 型の取り出しは v, ok := x.(T) と type switch。any の多用は避ける
  • nil ポインタを入れたインターフェースは nil ではない点に注意する

参考資料​