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

メソッドとは、レシーバーを受け取る関数のことであり、インターフェースとはメソッドのシグネチャの集合です。Goは、クラス継承の複雑さを伴わずに、簡潔な構文を用いてオブジェクト指向の「動作の抽象化」を実現しています。

Goにはクラスはありませんが、メソッドは存在します。また、implementsというキーワードはありませんが、ダックタイピングに基づくインターフェースがあります。このレッスンでは、Goにおけるオブジェクト指向プログラミングのすべての基本概念を習得し、インターフェースを使用して切り替え可能なマルチ決済ゲートウェイシステムを構築する方法を学びます。

1. 学習内容


2. あるEコマース決済エンジニアの実話

(1) 課題:switch文がハードコーディングされており、新しい決済ゲートウェイを追加するにはコアコードの修正が必要となる。

チャーリーは、あるECプラットフォームのバックエンドエンジニアです。彼は決済モジュールの保守を担当しています:

「現在、Stripeに対応していますが、PayPalも追加したいと考えています。しかし、決済コードにはif gateway == "stripe"が至る所に含まれているため、PayPalを追加するにはファイル全体を書き直す必要があります。」

彼は前任者が書いたコードを開いた:

GO
// 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の解決策:暗黙的なインターフェースの実装

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

出力:

TEXT
Stripe: charged $99.99
PayPal: charged $49.99

(3) メリット:オープン・アンド・クローズの原則

方法 新しいゲートウェイを追加 コアコードを修正 リスク
スイッチのハードコーディング charge 関数の修正 ✅ 必須 🔴 高
インターフェースの抽象化 インターフェースを実装するための新しい構造体を作成する ❌ 不要 🟢 低
💡 ヒント: Goのインターフェースは暗黙的に実装されます。つまり、構造体(struct)がインターフェースで定義されたすべてのメソッドを持っている限り、その構造体は「自動的に」そのインターフェースを実装することになります。これはJavaのimplementsよりも柔軟性が高く、サードパーティのパッケージに含まれる型であっても、(外部パッケージで定義された)インターフェースを実装させることができます。


3. メソッドの定義

(1) メソッド = レシーバーを持つ関数

GO
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) 値の受け取りとポインタの受け取り

GO
パッケージ 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)
}

出力:

TEXT
受信者の後に:11
ポインタの受信後:12

▶ サンプル:値型とポインタ型の受け取り先の選択

GO
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())
}
▶ 試してみよう

出力:

TEXT
Alice Smith (28)
Sum: 499500

(3) 値型とポインタ型の受取側の選択ガイド

シナリオ 受信者の種類 理由
このメソッドはレシーバーを変更しません 値とポインタのどちらも使用可能です 値レシーバーの方が安全です(副作用がありません)
このメソッドにはレシーバーが必要です ポインタ 値レシーバーはコピーを変更します
大きな構造体(100バイト超) ポインタ 大きなオブジェクトのコピーは避ける
レシーバーは map/slice/func である 値(参照型) それ自体が参照である
型はプリミティブ型である 値(ポインタは不要) サイズが小さく、コピーにかかるオーバーヘッドが少ない

4. インターフェース:暗黙の実装(ダックタイピング)

(1) インターフェースの定義

GO
// Defining an interface: a set of メソッド signatures
type Stringer interface {
    String() 文字列
}

▶ サンプル:暗黙の実装

GO
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)
}
▶ 試してみよう

出力:

TEXT
Buddy says: Woof!
Whiskers says: Meow!

(2) インターフェースの値:動的型 + 動的値

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

出力:

TEXT
nil: <nil>, <nil>
type=main.Dog, 値={Buddy}

5. 空インターフェース interface{} と型アサーション

(1) 空のインターフェース:任意の型

GO
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"})
}

出力:

TEXT
type=int, value=42
type=string, value=hello
type=float64, value=3.14
type=main.Dog, value={Buddy}

▶ サンプル:型アサーション(コンマ区切り構文)

GO
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)
}
▶ 試してみよう

出力:

TEXT
String: hello (len=5)
Int: 42 (double=84)
Unknown type: float64 = 3.14

(2) タイプ切り替え

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

出力:

TEXT
string: "hello" (len=5)
int: 42
float64: 3.14
bool: true
unknown: []int

(3) 型アサーションと型スイッチの比較

シナリオ 推奨事項
特定の型であるか確認する 型のアサーション v.(T)
複数のタイプを選択 タイプ切り替え v.(type)
確認する(値は不要) _, ok := v.(T)

6. インターフェースの合成

(1) 既存のインターフェースを埋め込んで新しいインターフェースを作成する

GO
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")
}

▶ サンプル:インターフェース合成の実践的な応用

GO
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")
}
▶ 試してみよう

出力:

TEXT
[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つのインターフェース

GO
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

▶ サンプル:Readerのさまざまな実装方法

GO
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.
}
▶ 試してみよう

出力:

TEXT
=== strings.Reader ===
read: "hello wo"
read: "rld"

(2) io.Readerio.Writer の組み合わせ(標準ライブラリの chain

GO
パッケージ 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())
}

出力:

TEXT
copied 19 bytes: "hello Go interfaces"

(3) io.Reader を実装する標準インターフェースの一覧

タイプ パッケージ 実装
strings.Reader 文字列 リーダー
bytes.Reader バイト リーダー
os.File OS 読み取り・書き込み
bytes.Buffer バイト 読み取り・書き込み
net.Conn ネットワーク 読み取り・書き込み
gzip.Reader 圧縮/gzip リーダー

8. 完全な例:複数の決済ゲートウェイの抽象化

このレッスンで学んだ重要な概念をすべて結びつけて、完全な決済システムをビルドしてみましょう:

GO
// 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)
}

期待される出力:

TEXT
[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
100%
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 における ストラテジーパターン の実装の基礎となっています。


❓ よくある質問

Q 戻り値の型として「値」と「ポインタ」のどちらを選ぶべきですか?
A 3つのルールがあります:(1) 戻り値を変更する必要がある場合 → ポインタ;(2) 大きな構造体(100バイト以上)の場合 → ポインタ;(3) それ以外の場合は、値パラメータを優先します。ある型でポインタパラメータを使用する場合は、すべてのメソッドで同じ規約に従うことを推奨します。
Q 暗黙的なインターフェースの実装をどのように理解すればよいですか?
A 構造体(struct)がインターフェースで定義されたすべてのメソッドのシグネチャを持っている限り、implements キーワードを指定しなくても、そのインターフェースを自動的に実装します。つまり、(1) サードパーティのパッケージの型も、あなたが定義したインターフェースを実装できること、(2) 1つの型が、まったく無関係な複数のインターフェースを実装できるということです。
Q 空のインターフェース interface{} の目的は何ですか?
A あらゆる型の値を受け入れます。一般的な使用例:(1) fmt.Println(a ...interface{});(2) マップに異なる型の値を格納する場合:map[string]interface{};(3) データのデシリアライズ(JSONをinterface{}にパースする場合)。
Q 型アサーションが失敗した場合はどうすればよいですか?
A カンマ付き構文 v, ok := x.(T) を使用してください。この場合、ok が false であってもパニックは発生しません。カンマ付き構文を使用しない場合、アサーションの失敗によりパニックが発生します。つまり、x が文字列でない場合、v := x.(string) でパニックが発生します。
Q インターフェースの構成とネストされた構造体の違いは何ですか?
A (1) インターフェースの構成 → メソッドのシグネチャを組み合わせる(挙動の再利用);(2) ネストされた構造体 → フィールドとメソッドを組み合わせる(データの再利用)。インターフェースの構成は「挙動の抽象化」に該当し、ネストされた構造体は「データの集約」に該当します。
Q インターフェースの値は値型ですか、それとも参照型ですか?
A インターフェースの値は参照型です。インターフェース変数は内部的に(型、値)のペアを格納しており、値が代入される際には、基となるデータそのものではなく、このペアがコピーされます。そのため、インターフェースの値をパラメータとして渡す処理は非常に軽量です(ポインタが2つだけ)。
Q io.ReaderReadメソッドは、いつEOFを返すのですか?
A すべてのデータの読み取りが完了すると、Readは(0, io.EOF)を返します。なお、EOFはエラーではなく正常な終了を示すため、読み込みが完了したかどうかを確認するためにerr != nilを使用することはできません。err == io.EOFを使用する必要があります。
Q 型は複数のインターフェースを同時に実装できますか?
A はい。型が複数のインターフェースのすべてのメソッドを持っている限り、それらすべてを暗黙的に実装します。これは、Goのインターフェース設計の大きな利点の一つでもあります。つまり、継承階層なしに柔軟な構成が可能になるということです。

📖 まとめ


📝 練習問題

  1. 基本問題(難易度 ⭐)Shape インターフェース (Area() float64) を定義し、2つの構造体—Circle(半径)と Rectangle(幅と高さ)—を実装して、それぞれの面積を計算し、出力してください。

  2. 上級問題(難易度 ⭐⭐)Cache インターフェース (Get(key string) (interface{}, bool) / Set(key string, value interface{})) を実装し、map[string]interface{} およびメモリ制限版(最大 10 キー)を用いて、2 つの異なる戦略を実装してください。ポインタ型のリシーバーを使用する必要があります。

  3. 課題(難易度 ⭐⭐⭐)プラグイン可能なストレージバックエンドを実装する:Store インターフェースを定義し(Save(key string, data []byte) error / Load(key string) ([]byte, error) / Delete(key string) error))を定義し、MemoryStore(マップストレージ)およびFileStoreos.WriteFile / os.ReadFileを使用したファイルストレージ)を実装し、最後にBackupServiceを使用して2つのストレージタイプ間でデータを同期させます。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%