メソッドとインターフェース
.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 を「宣言なしに」満たす
利点
- 定義元と利用側が疎結合: 実装側はインターフェースを知らなくてよい。標準ライブラリや外部ライブラリの型にも、利用側で独自のインターフェースを当てはめられる
- インターフェースを使う側 (消費者) が定義するのが 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 / dynamic | any / 型アサーション / type switch |
is / as | 型アサーション v, ok := x.(T) |
switch パターンマッチング (型) | type switch |
| インターフェースのデフォルト実装 | なし |
IDisposable | io.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 ではない点に注意する