Goのメソッドとインターフェース
メソッドとは、レシーバーを受け取る関数のことであり、インターフェースとはメソッドのシグネチャの集合です。Goは、クラス継承の複雑さを伴わずに、簡潔な構文を用いてオブジェクト指向の「動作の抽象化」を実現しています。
Goにはクラスはありませんが、メソッドは存在します。また、implementsというキーワードはありませんが、ダックタイピングに基づくインターフェースがあります。このレッスンでは、Goにおけるオブジェクト指向プログラミングのすべての基本概念を習得し、インターフェースを使用して切り替え可能なマルチ決済ゲートウェイシステムを構築する方法を学びます。
1. 学習内容
- メソッドの定義(値パラメータとポインタパラメータ)
- インターフェースの定義と暗黙の実装(ダックタイピング)
- 空のインターフェースの目的
interface{} - 型アサーションと型スイッチ
- インターフェースの構成(組み込みインターフェース)
io.Reader/io.Writer標準インターフェース- API を活用した切り替え可能なマルチ決済ゲートウェイの構築
2. あるEコマース決済エンジニアの実話
(1) 課題:switch文がハードコーディングされており、新しい決済ゲートウェイを追加するにはコアコードの修正が必要となる。
チャーリーは、あるECプラットフォームのバックエンドエンジニアです。彼は決済モジュールの保守を担当しています:
「現在、Stripeに対応していますが、PayPalも追加したいと考えています。しかし、決済コードには
if gateway == "stripe"が至る所に含まれているため、PayPalを追加するにはファイル全体を書き直す必要があります。」
彼は前任者が書いたコードを開いた:
// Bad Code: Hard-coded payment logic
func charge(amount float64, gateway string) error {
switch gateway {
case "stripe":
// Stripe HTTP API calls...
return stripeCharge(amount)
case "paypal":
// Adding PayPal means I have to add another case here.
return nil
default:
return fmt.Errorf("unknown gateway: %s", gateway)
}
}
決済ゲートウェイが追加されるたびに、charge関数を修正しなければならない。これはオープン・クローズドの原則(拡張にはオープン、修正にはクローズド)に反している。
(2) Goの解決策:暗黙的なインターフェースの実装
// payment.go
パッケージ main
import "fmt"
// Define the Payment Interface
type PaymentGateway interface {
Charge(amount float64) エラー
Refund(transactionID 文字列) エラー
}
// Stripe Implementation (No need to write "implements"!)
type Stripe struct {
apiKey 文字列
}
func (s Stripe) Charge(amount float64) エラー {
fmt.Printf("Stripe: charged $%.2f\n", amount)
return nil
}
func (s Stripe) Refund(txID 文字列) エラー {
fmt.Printf("Stripe: refunded %s\n", txID)
return nil
}
// PayPal Implementation
type PayPal struct {
email 文字列
}
func (p PayPal) Charge(amount float64) エラー {
fmt.Printf("PayPal: charged $%.2f\n", amount)
return nil
}
func (p PayPal) Refund(txID 文字列) エラー {
fmt.Printf("PayPal: refunded %s\n", txID)
return nil
}
// Consumer Code: Depends only on the interface, not on the specific implementation
func processPayment(gw PaymentGateway, amount float64) エラー {
return gw.Charge(amount)
}
func main() {
stripe := Stripe{apiKey: "sk_test_xxx"}
paypal := PayPal{email: "merchant@example.com"}
// 同じ processPayment 関数で異なる実装を受け入れられる
processPayment(stripe, 99.99)
processPayment(paypal, 49.99)
}
出力:
Stripe: charged $99.99
PayPal: charged $49.99
(3) メリット:オープン・アンド・クローズの原則
| 方法 | 新しいゲートウェイを追加 | コアコードを修正 | リスク |
|---|---|---|---|
| スイッチのハードコーディング | charge 関数の修正 |
✅ 必須 | 🔴 高 |
| インターフェースの抽象化 | インターフェースを実装するための新しい構造体を作成する | ❌ 不要 | 🟢 低 |
implementsよりも柔軟性が高く、サードパーティのパッケージに含まれる型であっても、(外部パッケージで定義された)インターフェースを実装させることができます。
3. メソッドの定義
(1) メソッド = レシーバーを持つ関数
package main
import "fmt"
type Rectangle struct {
Width float64
Height float64
}
// Method: The receiver (r Rectangle) is placed between the `func` keyword and the function name
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func main() {
rect := Rectangle{Width: 10, Height: 5}
fmt.Printf("Area: %.2f\n", rect.Area()) // Area: 50.00
}
(2) 値の受け取りとポインタの受け取り
パッケージ main
import "fmt"
type Counter struct {
Value int
}
// Value receiver: Operates on a copy; does not affect the original オブジェクト
func (c Counter) IncrementValue() Counter {
c.Value++
return c
}
// Pointer receiver: Directly modifies the original オブジェクト
func (c *Counter) IncrementPointer() {
c.Value++
}
func main() {
c := Counter{Value: 10}
// 値レシーバーは戻り値を使う必要がある
c = c.IncrementValue()
fmt.Printf("After the 値 is received: %d\n", c.Value)
// Direct Modification by the Pointer Recipient
c.IncrementPointer()
fmt.Printf("After the pointer is received: %d\n", c.Value)
}
出力:
受信者の後に:11
ポインタの受信後:12
▶ サンプル:値型とポインタ型の受け取り先の選択
package main
import "fmt"
type User struct {
Name string
Age int
}
// Value recipients: Suitable for small objects and read-only operations
func (u User) Info() string {
return fmt.Sprintf("%s (%d)", u.Name, u.Age)
}
// Pointer receiver: Suitable for large objects and modification operations
func (u *User) SetName(name string) {
u.Name = name
}
type LargeData struct {
data [1000]int
}
// Large structures must be passed as pointers (to avoid copying 1,000 `int`s).
func (l *LargeData) Process() int {
sum := 0
for _, v := range l.data {
sum += v
}
return sum
}
func main() {
u := User{Name: "Alice", Age: 28}
u.SetName("Alice Smith")
fmt.Println(u.Info())
ld := LargeData{}
for i := 0; i < 1000; i++ {
ld.data[i] = i
}
fmt.Printf("Sum: %d\n", ld.Process())
}
出力:
Alice Smith (28)
Sum: 499500
(3) 値型とポインタ型の受取側の選択ガイド
| シナリオ | 受信者の種類 | 理由 |
|---|---|---|
| このメソッドはレシーバーを変更しません | 値とポインタのどちらも使用可能です | 値レシーバーの方が安全です(副作用がありません) |
| このメソッドにはレシーバーが必要です | ポインタ | 値レシーバーはコピーを変更します |
| 大きな構造体(100バイト超) | ポインタ | 大きなオブジェクトのコピーは避ける |
| レシーバーは map/slice/func である | 値(参照型) | それ自体が参照である |
| 型はプリミティブ型である | 値(ポインタは不要) | サイズが小さく、コピーにかかるオーバーヘッドが少ない |
4. インターフェース:暗黙の実装(ダックタイピング)
(1) インターフェースの定義
// Defining an interface: a set of メソッド signatures
type Stringer interface {
String() 文字列
}
▶ サンプル:暗黙の実装
package main
import "fmt"
// 1. Define an interface
type Speaker interface {
Speak() string
}
// 2. Define two structs, both of which implement the Speak method
type Dog struct{ Name string }
func (d Dog) Speak() string {
return fmt.Sprintf("%s says: Woof!", d.Name)
}
type Cat struct{ Name string }
func (c Cat) Speak() string {
return fmt.Sprintf("%s says: Meow!", c.Name)
}
// 3. Consumption Function: Accepts an interface
func greet(s Speaker) {
fmt.Println(s.Speak())
}
func main() {
dog := Dog{Name: "Buddy"}
cat := Cat{Name: "Whiskers"}
// Both `Dog` and `Cat` implicitly implement `Speaker`; the `implements` keyword is not required.
greet(dog)
greet(cat)
}
出力:
Buddy says: Woof!
Whiskers says: Meow!
(2) インターフェースの値:動的型 + 動的値
package main
import "fmt"
type Speaker interface {
Speak() string
}
type Dog struct{ Name string }
func (d Dog) Speak() string {
return fmt.Sprintf("%s says: Woof!", d.Name)
}
func main() {
var s Speaker // インターフェース変数,デフォルト nil
fmt.Printf("nil: %T, %v\n", s, s)
s = Dog{Name: "Buddy"} // インターフェースには動的型と動的値が格納されている
fmt.Printf("type=%T, value=%v\n", s, s)
}
出力:
nil: <nil>, <nil>
type=main.Dog, 値={Buddy}
5. 空インターフェース interface{} と型アサーション
(1) 空のインターフェース:任意の型
package main
import "fmt"
// An empty interface can store any type.
func describe(v interface{}) {
fmt.Printf("type=%T, value=%v\n", v, v)
}
func main() {
describe(42)
describe("hello")
describe(3.14)
describe(Dog{Name: "Buddy"})
}
出力:
type=int, value=42
type=string, value=hello
type=float64, value=3.14
type=main.Dog, value={Buddy}
▶ サンプル:型アサーション(コンマ区切り構文)
package main
import "fmt"
func printValue(v interface{}) {
// Type Assertion: Extract Underlying Value
if s, ok := v.(string); ok {
fmt.Printf("String: %s (len=%d)\n", s, len(s))
return
}
if n, ok := v.(int); ok {
fmt.Printf("Int: %d (double=%d)\n", n, n*2)
return
}
fmt.Printf("Unknown type: %T = %v\n", v, v)
}
func main() {
printValue("hello")
printValue(42)
printValue(3.14)
}
出力:
String: hello (len=5)
Int: 42 (double=84)
Unknown type: float64 = 3.14
(2) タイプ切り替え
package main
import "fmt"
func inspect(v interface{}) {
switch val := v.(type) {
case string:
fmt.Printf("string: %q (len=%d)\n", val, len(val))
case int:
fmt.Printf("int: %d\n", val)
case float64:
fmt.Printf("float64: %.2f\n", val)
case bool:
fmt.Printf("bool: %v\n", val)
default:
fmt.Printf("unknown: %T\n", val)
}
}
func main() {
inspect("hello")
inspect(42)
inspect(3.14)
inspect(true)
inspect([]int{1, 2, 3})
}
出力:
string: "hello" (len=5)
int: 42
float64: 3.14
bool: true
unknown: []int
(3) 型アサーションと型スイッチの比較
| シナリオ | 推奨事項 |
|---|---|
| 特定の型であるか確認する | 型のアサーション v.(T) |
| 複数のタイプを選択 | タイプ切り替え v.(type) |
| 確認する(値は不要) | _, ok := v.(T) |
6. インターフェースの合成
(1) 既存のインターフェースを埋め込んで新しいインターフェースを作成する
package main
import "fmt"
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// Combine Reader and Writer to form a new interface
type ReadWriter interface {
Reader
Writer
}
// Implementation
type File struct{}
func (f File) Read(p []byte) (n int, err error) {
return len(p), nil
}
func (f File) Write(p []byte) (n int, err error) {
return len(p), nil
}
func main() {
var rw ReadWriter = File{}
buf := make([]byte, 10)
rw.Read(buf)
rw.Write(buf)
fmt.Println("The ReadWriter composite interface works as expected")
}
▶ サンプル:インターフェース合成の実践的な応用
package main
import "fmt"
type Logger interface {
Log(message string)
}
type Notifier interface {
Notify(message string)
}
// Combination
type LoggerNotifier interface {
Logger
Notifier
}
type ConsoleService struct{}
func (c ConsoleService) Log(message string) {
fmt.Printf("[LOG] %s\n", message)
}
func (c ConsoleService) Notify(message string) {
fmt.Printf("[NOTIFY] %s\n", message)
}
func main() {
var svc LoggerNotifier = ConsoleService{}
svc.Log("System Startup")
svc.Notify("User Alice is logged in")
}
出力:
[LOG] システムの起動
[NOTIFY] ユーザー Alice ログイン
(2) インターフェースの組み合わせ手法に関するクイックリファレンス
| 組み合わせ | 構文 | 説明 |
|---|---|---|
| 単一インターフェースの埋め込み | type A interface { B } |
AにはBのすべてのメソッドが含まれている |
| 複数のインターフェースの埋め込み | type A interface { B; C } |
AにはBとCのすべてのメソッドが含まれている |
| 埋め込み + 新しいメソッド | type A interface { B; C; Do() } |
A にはメソッド B、C、および Do が含まれている |
7. io.Reader / io.Writer 標準インターフェース
(1) 標準ライブラリにおける最も重要な2つのインターフェース
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
▶ サンプル:Readerのさまざまな実装方法
package main
import (
"fmt"
"io"
"strings"
)
func printReader(r io.Reader) {
buf := make([]byte, 8)
for {
n, err := r.Read(buf)
if err == io.EOF {
break
}
fmt.Printf("read: %q\n", buf[:n])
}
}
func main() {
// `strings.Reader` implements `io.Reader`
fmt.Println("=== strings.Reader ===")
printReader(strings.NewReader("hello world"))
// You can also use `bytes.Reader`, `os.File`, and so on.
}
出力:
=== strings.Reader ===
read: "hello wo"
read: "rld"
(2) io.Reader + io.Writer の組み合わせ(標準ライブラリの chain)
パッケージ main
import (
"fmt"
"io"
"strings"
)
func main() {
// Implementing a copy using `io.Reader` and `io.Writer`
reader := strings.NewReader("hello Go interfaces")
writer := &strings.Builder{}
// io.Copy accepts any Reader and Writer
n, _ := io.Copy(writer, reader)
fmt.Printf("copied %d bytes: %q\n", n, writer.String())
}
出力:
copied 19 bytes: "hello Go interfaces"
(3) io.Reader を実装する標準インターフェースの一覧
| タイプ | パッケージ | 実装 |
|---|---|---|
strings.Reader |
文字列 | リーダー |
bytes.Reader |
バイト | リーダー |
os.File |
OS | 読み取り・書き込み |
bytes.Buffer |
バイト | 読み取り・書き込み |
net.Conn |
ネットワーク | 読み取り・書き込み |
gzip.Reader |
圧縮/gzip | リーダー |
8. 完全な例:複数の決済ゲートウェイの抽象化
このレッスンで学んだ重要な概念をすべて結びつけて、完全な決済システムをビルドしてみましょう:
// payment_system.go
package main
import (
"fmt"
"time"
)
// ---------- Interface Definition ----------
type PaymentGateway interface {
Charge(amount float64) (string, error) // 取引画面に戻る ID
Refund(transactionID string) error
Name() string
}
// Logging Interface (Combination Example)
type TransactionLogger interface {
Log(transactionID, gateway string, amount float64, success bool)
}
// ---------- Stripe Implementation ----------
type Stripe struct {
apiKey string
}
func (s Stripe) Charge(amount float64) (string, error) {
txID := fmt.Sprintf("STRIPE-%s", s.txID())
fmt.Printf("[Stripe] charging $%.2f -> %s\n", amount, txID)
return txID, nil
}
func (s Stripe) Refund(txID string) error {
fmt.Printf("[Stripe] refunding %s\n", txID)
return nil
}
func (s Stripe) Name() string {
return "Stripe"
}
func (Stripe) txID() string {
return fmt.Sprintf("%d", time.Now().UnixNano())
}
// ---------- PayPal Implementation ----------
type PayPal struct {
email string
}
func (p PayPal) Charge(amount float64) (string, error) {
txID := fmt.Sprintf("PP-%s", p.txID())
fmt.Printf("[PayPal] charging $%.2f -> %s\n", amount, txID)
return txID, nil
}
func (p PayPal) Refund(txID string) error {
fmt.Printf("[PayPal] refunding %s\n", txID)
return nil
}
func (p PayPal) Name() string {
return "PayPal"
}
func (PayPal) txID() string {
return fmt.Sprintf("%d", time.Now().UnixNano())
}
// ---------- Alipay Implementation ----------
type Alipay struct {
appID string
}
func (a Alipay) Charge(amount float64) (string, error) {
txID := fmt.Sprintf("ALI-%s", a.txID())
fmt.Printf("[Alipay] charging $%.2f -> %s\n", amount, txID)
return txID, nil
}
func (a Alipay) Refund(txID string) error {
fmt.Printf("[Alipay] refunding %s\n", txID)
return nil
}
func (a Alipay) Name() string {
return "Alipay"
}
func (Alipay) txID() string {
return fmt.Sprintf("%d", time.Now().UnixNano())
}
// ---------- Log Implementation (Example of an Empty Interface + Type Assertion)----------
type ConsoleLogger struct{}
func (c ConsoleLogger) Log(transactionID, gateway string, amount float64, success bool) {
status := "SUCCESS"
if !success {
status = "FAILED"
}
fmt.Printf("[%s] %s | %s | $%.2f | %s\n",
status, transactionID, gateway, amount, time.Now().Format(time.RFC3339))
}
// ---------- Payment Services ----------
type PaymentService struct {
gateway PaymentGateway
logger TransactionLogger
}
func NewPaymentService(gw PaymentGateway, logger TransactionLogger) *PaymentService {
return &PaymentService{gateway: gw, logger: logger}
}
func (s *PaymentService) Charge(amount float64) error {
txID, err := s.gateway.Charge(amount)
if err != nil {
s.logger.Log("", s.gateway.Name(), amount, false)
return err
}
s.logger.Log(txID, s.gateway.Name(), amount, true)
return nil
}
func (s *PaymentService) SwitchGateway(gw PaymentGateway) {
fmt.Printf("\nSwitch payment gateways: %s -> %s\n", s.gateway.Name(), gw.Name())
s.gateway = gw
}
// ---------- main ----------
func main() {
logger := ConsoleLogger{}
stripe := Stripe{apiKey: "sk_test_xxx"}
paypal := PayPal{email: "merchant@example.com"}
alipay := Alipay{appID: "2025xxxx"}
// Getting Started with Stripe
service := NewPaymentService(stripe, logger)
service.Charge(99.99)
service.Charge(49.99)
// Switch to PayPal at runtime (flexibility provided by the API)
service.SwitchGateway(paypal)
service.Charge(199.99)
// Switch back to Alipay
service.SwitchGateway(alipay)
service.Charge(299.99)
}
期待される出力:
[Stripe] charging $99.99 -> STRIPE-1741500000000
[SUCCESS] STRIPE-1741500000000 | Stripe | $99.99 | 2026-07-08T10:00:00Z
[Stripe] charging $49.99 -> STRIPE-1741500000001
[SUCCESS] STRIPE-1741500000001 | Stripe | $49.99 | 2026-07-08T10:00:00Z
決済ゲートウェイの切り替え:Stripe -> PayPal
[PayPal] charging $199.99 -> PP-1741500000002
[SUCCESS] PP-1741500000002 | PayPal | $199.99 | 2026-07-08T10:00:00Z
決済ゲートウェイの切り替え:PayPal -> Alipay
[Alipay] charging $299.99 -> ALI-1741500000003
[SUCCESS] ALI-1741500000003 | Alipay | $299.99 | 2026-07-08T10:00:00Z
classDiagram
class PaymentGateway {
<<interface>>
+Charge(amount float64) (string, error)
+Refund(transactionID string) error
+Name() string
}
class Stripe {
-apiKey string
+Charge(amount float64) (string, error)
+Refund(transactionID string) error
+Name() string
}
class PayPal {
-email string
+Charge(amount float64) (string, error)
+Refund(transactionID string) error
+Name() string
}
class Alipay {
-appID string
+Charge(amount float64) (string, error)
+Refund(transactionID string) error
+Name() string
}
class PaymentService {
-gateway PaymentGateway
-logger TransactionLogger
+Charge(amount float64) error
+SwitchGateway(gw PaymentGateway)
}
PaymentGateway <|.. Stripe : 暗黙的な実装
PaymentGateway <|.. PayPal : 暗黙的な実装
PaymentGateway <|.. Alipay : 暗黙的な実装
PaymentService o--> PaymentGateway : インターフェースへの依存(ストラテジーパターン)
PaymentService 内の gateway フィールドは、具体的な型ではなくインターフェース型です。インターフェース変数には、そのインターフェースを実装する任意の値を格納できます。これが、Go における ストラテジーパターン の実装の基礎となっています。
❓ よくある質問
implements キーワードを指定しなくても、そのインターフェースを自動的に実装します。つまり、(1) サードパーティのパッケージの型も、あなたが定義したインターフェースを実装できること、(2) 1つの型が、まったく無関係な複数のインターフェースを実装できるということです。interface{} の目的は何ですか?fmt.Println(a ...interface{});(2) マップに異なる型の値を格納する場合:map[string]interface{};(3) データのデシリアライズ(JSONをinterface{}にパースする場合)。v, ok := x.(T) を使用してください。この場合、ok が false であってもパニックは発生しません。カンマ付き構文を使用しない場合、アサーションの失敗によりパニックが発生します。つまり、x が文字列でない場合、v := x.(string) でパニックが発生します。io.ReaderのReadメソッドは、いつEOFを返すのですか?Readは(0, io.EOF)を返します。なお、EOFはエラーではなく正常な終了を示すため、読み込みが完了したかどうかを確認するためにerr != nilを使用することはできません。err == io.EOFを使用する必要があります。📖 まとめ
- メソッド = レシーバーを持つ関数。レシーバーは値でもポインタでもよい
- 値受け取りの場合はコピーに対して操作が行われるのに対し、ポインタ受け取りの場合は元のオブジェクトが変更される
- インターフェースとは、メソッドのシグネチャの集合のことです。Go言語では暗黙の実装(ダックタイピング)を採用しています。
- 空のインターフェース
interface{}は、あらゆる型を表します - 型アサーション
v.(T)はインターフェースの動的値を抽出します。一方、comma-okはパニックを防ぐ役割を果たします。 - インターフェースの合成は、インターフェースを埋め込むことで新しいインターフェースを作成します (
type A interface { B; C }) io.Readerとio.Writerは、Go 標準ライブラリにおいて最も基本的な 2 つのインターフェースです- インターフェースは、コードが「開閉の原則」(拡張には開かれ、変更には閉じられている)に従うことを保証する
📝 練習問題
-
基本問題(難易度 ⭐):
Shapeインターフェース (Area() float64) を定義し、2つの構造体—Circle(半径)とRectangle(幅と高さ)—を実装して、それぞれの面積を計算し、出力してください。 -
上級問題(難易度 ⭐⭐):
Cacheインターフェース (Get(key string) (interface{}, bool)/Set(key string, value interface{})) を実装し、map[string]interface{}およびメモリ制限版(最大 10 キー)を用いて、2 つの異なる戦略を実装してください。ポインタ型のリシーバーを使用する必要があります。 -
課題(難易度 ⭐⭐⭐):プラグイン可能なストレージバックエンドを実装する:
Storeインターフェースを定義し(Save(key string, data []byte) error/Load(key string) ([]byte, error)/Delete(key string) error))を定義し、MemoryStore(マップストレージ)およびFileStore(os.WriteFile/os.ReadFileを使用したファイルストレージ)を実装し、最後にBackupServiceを使用して2つのストレージタイプ間でデータを同期させます。



