メソッド

レッスン8:メソッド

現実世界のアナロジー

車を買ったと想像してください。車にはさまざまな機能があります — 始動、加速、ブレーキ、ライト点灯。これらの機能は独立して存在するのではなく、車に属しています。「始動」は一般的な動作ではなく、「車が始動した」と言います。

Goでは、メソッドとは特定の型に結びついた関数です。「始動」が「車」に属するように、メソッドは結びついた型に属します。冷蔵庫に始動機能を搭載しないのと同じように、メソッドは本当に奉仕する型に定義されます。


コアコンセプト

メソッドとは?

メソッドとはレシーバを持つ関数です。レシーバはメソッドを型に結びつけ、その型の変数が直接メソッドを呼び出せるようにします。

レシーバ

レシーバはメソッドと型の間の架け橋です。その構文は func キーワードとメソッド名の間に現れます。

GO
func (レシーバ変数 レシーバ型) メソッド名(パラメータ) 戻り値型 {
    // メソッド本体
}

値レシーバとポインタレシーバの比較

特性 値レシーバ func (s Struct) ポインタレシーバ func (s *Struct)
コピーに対して操作 はい(変更は元に影響しない) いいえ(元を直接変更)
使用場面 読み取り専用操作、小さな構造体 フィールドの変更が必要、大きな構造体
インターフェースの実装 値とポインタの両方で呼び出し可能 ポインタ型でのみ呼び出し可能

メソッドセットのルール

つまり、*T 型の変数は T*T の両方のすべてのメソッドを呼び出すことができますが、T 型の変数は T のメソッドのみを呼び出すことができます。

継承より合成

Goにはクラスの継承がありません。代わりに、合成(埋め込み)を通じてコードの再利用を実現します。ある構造体を別の構造体に埋め込むと、埋め込まれた構造体のメソッドが自動的に外側の構造体に「プロモーション」されます。


基本構文と使い方

メソッドの定義

GO
package main

import "fmt"

// 構造体を定義
type Rectangle struct {
    Width  float64
    Height float64
}

// 値レシーバメソッド:面積を計算
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// ポインタレシーバメソッド:拡大縮小(フィールドの変更が必要)
func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}

func main() {
    rect := Rectangle{Width: 10, Height: 5}
    fmt.Println("面積:", rect.Area()) // 出力: 面積: 50

    rect.Scale(2)
    fmt.Println("拡大後の面積:", rect.Area()) // 出力: 拡大後の面積: 200
}

メソッドと関数の違い

GO
// これは関数で、どの型にも結びつかない
func Area(r Rectangle) float64 {
    return r.Width * r.Height
}

// これはメソッドで、Rectangle型に結びついている
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

埋め込み構造体によるメソッドの継承

GO
package main

import "fmt"

// ベース型
type Animal struct {
    Name string
}

// Animal のメソッド
func (a Animal) Speak() string {
    return a.Name + "が鳴きました"
}

// Animal を埋め込む型
type Dog struct {
    Animal  // 埋め込み、フィールド名は不要
    Breed string
}

// Dog 独自のメソッド
func (d Dog) Bark() string {
    return d.Name + "が吠えました!"
}

func main() {
    dog := Dog{
        Animal: Animal{Name: "Rex"},
        Breed:  "ゴールデンレトリバー",
    }

    // 埋め込まれた型のメソッドを直接呼び出し可能
    fmt.Println(dog.Speak()) // 出力: Rexが鳴きました
    fmt.Println(dog.Bark())  // 出力: Rexが吠えました!
}
💡 ヒント:メソッド名は簡潔で意味のあるものに メソッド名は、行われる操作を表す動詞または動詞句にすべきです。例:Area()Scale()Save()IsValid()

💡 ヒント:レシーバ名は短く保つ レシーバ変数は通常、型名の最初の1文字または2文字です。例えば、func (rect Rectangle) よりも func (r Rectangle) が推奨されます。これはGoの慣例です。

💡 ヒント:一貫性の原則 同じ型のすべてのメソッドは同じレシーバ型を使用すべきです — すべて値レシーバか、すべてポインタレシーバ(変更が必要な場合)のどちらかです。混在させないでください。

💡 ヒント:ポインタレシーバを使うタイミング 構造体にコピー不可能なフィールド(sync.Mutex など)が含まれている場合、または構造体が大きい場合は、常にポインタレシーバを使用してください。


サンプルコード

例:基本的なメソッド定義(難易度⭐)

Circle 構造体を定義し、面積と周囲の長さを計算するメソッドを実装します。

GO
package main

import (
    "fmt"
    "math"
)

// Circle 構造体
type Circle struct {
    Radius float64
}

// Area は面積を計算する(値レシーバ、読み取り専用操作)
func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

// Perimeter は周囲の長さを計算する
func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}

// SetRadius は新しい半径を設定する(ポインタレシーバ、フィールドを変更)
func (c *Circle) SetRadius(r float64) {
    if r > 0 {
        c.Radius = r
    }
}

func main() {
    c := Circle{Radius: 5}
    fmt.Printf("半径: %.2f\n", c.Radius)
    fmt.Printf("面積: %.2f\n", c.Area())
    fmt.Printf("周囲の長さ: %.2f\n", c.Perimeter())

    c.SetRadius(10)
    fmt.Printf("\n新しい半径: %.2f\n", c.Radius)
    fmt.Printf("新しい面積: %.2f\n", c.Area())
    fmt.Printf("新しい周囲の長さ: %.2f\n", c.Perimeter())
}
▶ 試してみよう

出力:

TEXT
半径: 5.00
面積: 78.54
周囲の長さ: 31.42

新しい半径: 10.00
新しい面積: 314.16
新しい周囲の長さ: 62.83

例:値レシーバとポインタレシーバの違い(難易度⭐⭐)

両者の主要な動作の違いを実演します。

GO
package main

import "fmt"

// Account は銀行口座
type Account struct {
    Owner   string
    Balance float64
}

// 値レシーバ:残高を取得(読み取り専用、変更なし)
func (a Account) GetBalance() float64 {
    return a.Balance
}

// ポインタレシーバ:入金(残高の変更が必要)
func (a *Account) Deposit(amount float64) {
    if amount > 0 {
        a.Balance += amount
        fmt.Printf("  %.2f入金、残高: %.2f\n", amount, a.Balance)
    }
}

// ポインタレシーバ:出金
func (a *Account) Withdraw(amount float64) bool {
    if amount > 0 && a.Balance >= amount {
        a.Balance -= amount
        fmt.Printf("  %.2f出金、残高: %.2f\n", amount, a.Balance)
        return true
    }
    fmt.Printf("  %.2fの出金に失敗、残高不足\n", amount)
    return false
}

// 値レシーバ:口座情報をフォーマット
func (a Account) String() string {
    return fmt.Sprintf("口座[%s] 残高: %.2f", a.Owner, a.Balance)
}

func main() {
    acc := Account{Owner: "Zhang San", Balance: 1000}
    fmt.Println(acc)

    fmt.Println("\n--- 取引履歴 ---")
    acc.Deposit(500)
    acc.Withdraw(200)
    acc.Withdraw(2000) // 残高不足

    fmt.Println("\n--- 最終状態 ---")
    fmt.Println(acc)
    fmt.Printf("現在の残高: %.2f\n", acc.GetBalance())
}
▶ 試してみよう

出力:

TEXT
口座[Zhang San] 残高: 1000.00

--- 取引履歴 ---
  500.00入金、残高: 1500.00
  200.00出金、残高: 1300.00
  2000.00の出金に失敗、残高不足

--- 最終状態 ---
口座[Zhang San] 残高: 1300.00
現在の残高: 1300.00

例:継承より合成(難易度⭐⭐⭐)

埋め込み構造体によるメソッドの継承とメソッドのオーバーライドを実演します。

GO
package main

import "fmt"

// ==================== ベース層 ====================

// Base 構造体(「親クラス」に類似)
type Base struct {
    ID   int
    Name string
}

// Describe は基本的な説明を返す
func (b Base) Describe() string {
    return fmt.Sprintf("Base[ID=%d, Name=%s]", b.ID, b.Name)
}

// Identify は識別情報を返す
func (b Base) Identify() string {
    return fmt.Sprintf("私は%sです(ID: %d)", b.Name, b.ID)
}

// ==================== ビジネス層 ====================

// Employee は Base を埋め込む
type Employee struct {
    Base         // 埋め込み、Base のメソッドを継承
    Department string
    Salary     float64
}

// Base の Describe メソッドをオーバーライド
func (e Employee) Describe() string {
    return fmt.Sprintf("Employee[ID=%d, Name=%s, Dept=%s, Salary=%.0f]",
        e.ID, e.Name, e.Department, e.Salary)
}

// Employee 独自のメソッド
func (e Employee) AnnualSalary() float64 {
    return e.Salary * 12
}

// Manager は Employee を埋め込む(多階層の埋め込み)
type Manager struct {
    Employee        // Employee を埋め込み
    TeamSize int
}

// Describe メソッドをオーバーライド
func (m Manager) Describe() string {
    return fmt.Sprintf("Manager[ID=%d, Name=%s, Dept=%s, Team=%d人]",
        m.ID, m.Name, m.Department, m.TeamSize)
}

// Manager 独自のメソッド
func (m Manager) TeamReport() string {
    return fmt.Sprintf("%sは%d人のチームを管理しています", m.Name, m.TeamSize)
}

func main() {
    // Base インスタンスを作成
    b := Base{ID: 1, Name: "基本オブジェクト"}
    fmt.Println("=== Base ===")
    fmt.Println(b.Describe())
    fmt.Println(b.Identify())

    // Employee インスタンスを作成
    emp := Employee{
        Base:       Base{ID: 2, Name: "Li Si"},
        Department: "Engineering",
        Salary:     15000,
    }
    fmt.Println("\n=== Employee ===")
    fmt.Println(emp.Describe())       // Employee のオーバーライドされたメソッドを呼び出し
    fmt.Println(emp.Identify())       // Base から継承
    fmt.Printf("年間給与: %.0f\n", emp.AnnualSalary())

    // Manager インスタンスを作成
    mgr := Manager{
        Employee: Employee{
            Base:       Base{ID: 3, Name: "Wang Wu"},
            Department: "R&D",
            Salary:     25000,
        },
        TeamSize: 8,
    }
    fmt.Println("\n=== Manager ===")
    fmt.Println(mgr.Describe())       // Manager のオーバーライドされたメソッドを呼び出し
    fmt.Println(mgr.Identify())       // 多階層の埋め込みにより Base から継承
    fmt.Printf("年間給与: %.0f\n", mgr.AnnualSalary()) // Employee から継承
    fmt.Println(mgr.TeamReport())     // Manager 独自のメソッド
}
▶ 試してみよう

出力:

TEXT
=== Base ===
Base[ID=1, Name=基本オブジェクト]
私は基本オブジェクトです(ID: 1)

=== Employee ===
Employee[ID=2, Name=Li Si, Dept=Engineering, Salary=15000]
私はLi Siです(ID: 2)
年間給与: 180000

=== Manager ===
Manager[ID=3, Name=Wang Wu, Dept=R&D, Team=8人]
私はWang Wuです(ID: 3)
年間給与: 300000
Wang Wuは8人のチームを管理しています

現実世界の使用場面

場面1:ユーザー認証システム

メソッドを使ってユーザー認証ロジックをカプセル化します。

GO
package main

import (
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "time"
)

// User 構造体
type User struct {
    Username    string
    PasswordHash string
    Email       string
    CreatedAt   time.Time
    IsActive    bool
    LoginCount  int
}

// NewUser は新しいユーザーを作成する(コンストラクタパターン)
func NewUser(username, password, email string) *User {
    return &User{
        Username:     username,
        PasswordHash: hashPassword(password),
        Email:        email,
        CreatedAt:    time.Now(),
        IsActive:     true,
        LoginCount:   0,
    }
}

// hashPassword はパスワードをハッシュ化する(パッケージレベルの関数、メソッドではない)
func hashPassword(password string) string {
    h := sha256.Sum256([]byte(password))
    return hex.EncodeToString(h[:])
}

// VerifyPassword はパスワードを検証する
func (u *User) VerifyPassword(password string) bool {
    return u.PasswordHash == hashPassword(password)
}

// Login はユーザーログイン
func (u *User) Login(password string) error {
    if !u.IsActive {
        return fmt.Errorf("アカウント%sは無効になっています", u.Username)
    }
    if !u.VerifyPassword(password) {
        return fmt.Errorf("パスワードが正しくありません")
    }
    u.LoginCount++
    return nil
}

// Deactivate はアカウントを無効にする
func (u *User) Deactivate() {
    u.IsActive = false
}

// Info はユーザー情報を返す
func (u User) Info() string {
    status := "有効"
    if !u.IsActive {
        status = "無効"
    }
    return fmt.Sprintf("[%s] %s | メール: %s | 状態: %s | ログイン回数: %d",
        u.Username, u.Username, u.Email, status, u.LoginCount)
}

func main() {
    user := NewUser("zhangsan", "mySecret123", "zhangsan@example.com")
    fmt.Println("ユーザーを作成しました:")
    fmt.Println(user.Info())

    // 正しいパスワードでログイン
    fmt.Println("\n正しいパスワードでログイン:")
    err := user.Login("mySecret123")
    if err != nil {
        fmt.Println("ログイン失敗:", err)
    } else {
        fmt.Println("ログイン成功!")
    }
    fmt.Println(user.Info())

    // 間違ったパスワードでログイン
    fmt.Println("\n間違ったパスワードでログイン:")
    err = user.Login("wrongPassword")
    if err != nil {
        fmt.Println("ログイン失敗:", err)
    }

    // アカウント無効化後のログイン試行
    fmt.Println("\nアカウント無効化後のログイン試行:")
    user.Deactivate()
    err = user.Login("mySecret123")
    if err != nil {
        fmt.Println("ログイン失敗:", err)
    }
    fmt.Println(user.Info())
}

出力例:

TEXT
ユーザーを作成しました:
[zhangsan] zhangsan | メール: zhangsan@example.com | 状態: 有効 | ログイン回数: 0

正しいパスワードでログイン:
ログイン成功!
[zhangsan] zhangsan | メール: zhangsan@example.com | 状態: 有効 | ログイン回数: 1

間違ったパスワードでログイン:
ログイン失敗: パスワードが正しくありません

アカウント無効化後のログイン試行:
ログイン失敗: アカウントzhangsanは無効になっています
[zhangsan] zhangsan | メール: zhangsan@example.com | 状態: 無効 | ログイン回数: 1

場面2:ショッピングカートシステム

メソッドを使ってショッピングカートのCRUDとチェックアウトを実装します。

GO
package main

import "fmt"

// Product は商品
type Product struct {
    Name     string
    Price    float64
    Category string
}

// CartItem はカートアイテム
type CartItem struct {
    Product  Product
    Quantity int
}

// Subtotal は小計を計算する
func (ci CartItem) Subtotal() float64 {
    return ci.Product.Price * float64(ci.Quantity)
}

// ShoppingCart はショッピングカート
type ShoppingCart struct {
    Items  []CartItem
    Owner  string
}

// Add は商品をカートに追加する
func (sc *ShoppingCart) Add(p Product, qty int) {
    // 既に存在するか確認
    for i := range sc.Items {
        if sc.Items[i].Product.Name == p.Name {
            sc.Items[i].Quantity += qty
            fmt.Printf("  数量を更新: %s x%d\n", p.Name, sc.Items[i].Quantity)
            return
        }
    }
    sc.Items = append(sc.Items, CartItem{Product: p, Quantity: qty})
    fmt.Printf("  商品を追加: %s x%d\n", p.Name, qty)
}

// Remove は商品をカートから削除する
func (sc *ShoppingCart) Remove(name string) bool {
    for i, item := range sc.Items {
        if item.Product.Name == name {
            sc.Items = append(sc.Items[:i], sc.Items[i+1:]...)
            fmt.Printf("  商品を削除: %s\n", name)
            return true
        }
    }
    fmt.Printf("  商品が見つかりません: %s\n", name)
    return false
}

// Total は合計金額を計算する
func (sc ShoppingCart) Total() float64 {
    var total float64
    for _, item := range sc.Items {
        total += item.Subtotal()
    }
    return total
}

// Count は商品の種類数を返す
func (sc ShoppingCart) Count() int {
    return len(sc.Items)
}

// Display はカートの内容を表示する
func (sc ShoppingCart) Display() {
    fmt.Printf("\n🛒 %sさんのカート(%d商品):\n", sc.Owner, sc.Count())
    fmt.Println("  ─────────────────────────────────────")
    for _, item := range sc.Items {
        fmt.Printf("  %-12s $%.2f x %d = $%.2f\n",
            item.Product.Name, item.Product.Price, item.Quantity, item.Subtotal())
    }
    fmt.Println("  ─────────────────────────────────────")
    fmt.Printf("  合計: $%.2f\n", sc.Total())
}

func main() {
    cart := ShoppingCart{Owner: "Zhang San"}

    // 商品を定義
    laptop := Product{Name: "Laptop", Price: 999, Category: "Electronics"}
    mouse := Product{Name: "Wireless Mouse", Price: 29, Category: "Electronics"}
    book := Product{Name: "Go Programming", Price: 45, Category: "Books"}
    coffee := Product{Name: "Coffee", Price: 5, Category: "Food"}

    // 商品を追加
    fmt.Println("商品を追加:")
    cart.Add(laptop, 1)
    cart.Add(mouse, 2)
    cart.Add(book, 3)
    cart.Add(coffee, 5)
    cart.Display()

    // 数量を更新
    fmt.Println("\n数量を更新:")
    cart.Add(mouse, 1)  // マウスを1つ追加
    cart.Add(book, -1)  // 本を1冊削除(負の数で指定)
    cart.Display()

    // 商品を削除
    fmt.Println("\n商品を削除:")
    cart.Remove("Coffee")
    cart.Display()
}

出力:

TEXT
商品を追加:
  商品を追加: Laptop x1
  商品を追加: Wireless Mouse x2
  商品を追加: Go Programming x3
  商品を追加: Coffee x5

🛒 Zhang Sanさんのカート(4商品):
  ─────────────────────────────────────
  Laptop       $999.00 x 1 = $999.00
  Wireless Mouse $29.00 x 2 = $58.00
  Go Programming $45.00 x 3 = $135.00
  Coffee       $5.00 x 5 = $25.00
  ─────────────────────────────────────
  合計: $1217.00

数量を更新:
  数量を更新: Wireless Mouse x3
  数量を更新: Go Programming x2

🛒 Zhang Sanさんのカート(4商品):
  ─────────────────────────────────────
  Laptop       $999.00 x 1 = $999.00
  Wireless Mouse $29.00 x 3 = $87.00
  Go Programming $45.00 x 2 = $90.00
  Coffee       $5.00 x 5 = $25.00
  ─────────────────────────────────────
  合計: $1201.00

商品を削除:
  商品を削除: Coffee

🛒 Zhang Sanさんのカート(3商品):
  ─────────────────────────────────────
  Laptop       $999.00 x 1 = $999.00
  Wireless Mouse $29.00 x 3 = $87.00
  Go Programming $45.00 x 2 = $90.00
  ─────────────────────────────────────
  合計: $1176.00

❓ よくある質問

質問1:値レシーバとポインタレシーバ、どうやって選べばいい?

以下のルールに従ってください:

  1. レシーバのフィールドを変更する必要がある → ポインタレシーバ
  2. レシーバが大きな構造体(フィールドや配列が多い) → ポインタレシーバ(コピーのオーバーヘッドを回避)
  3. 構造体に sync.Mutex などのコピー不可能なフィールドが含まれている → ポインタレシーバを必ず使用
  4. 読み取り専用操作で構造体が小さい → 値レシーバで十分
  5. 迷ったとき → ポインタレシーバを優先
GO
type Small struct { X int }
func (s Small) Get() int { return s.X }       // ✅ 値レシーバ、読み取り専用

type Large struct { Data [1024]byte }
func (l *Large) Process() { /* ... */ }        // ✅ ポインタレシーバ、コピーを回避

type Safe struct { mu sync.Mutex }
func (s *Safe) Lock() { s.mu.Lock() }         // ✅ ポインタを必ず使用、Mutexはコピー不可能

質問2:メソッドを呼び出すとコンパイラがエラーを出すのはなぜ?

最も一般的な理由は、値レシーバとポインタレシーバの呼び出し規則を混同していることです。

GO
type Dog struct { Name string }

func (d *Dog) Rename(name string) {
    d.Name = name
}

func main() {
    d := Dog{Name: "Rex"}
    d.Rename("Buddy")    // ✅ Goが自動的にアドレスを取り、(&d).Rename("Buddy") と同等

    // しかし、これはエラーになります:
    // Dog{"Rex"}.Rename("Buddy")  // ❌ アドレス不可能な値のアドレスを取得できない
}

Goコンパイラは d.Rename()(&d).Rename() の変換を自動的に処理しますが、変数がアドレス可能な場合のみです。リテラルや一時的な値はアドレス不可能です。

質問3:メソッドは複数の値を返すことはできますか?

はい、メソッドと関数は同じシグネチャルールを持ち、任意の数のパラメータと戻り値を持つことができます。

GO
type Calculator struct {
    Value float64
}

// 商と剰余を返す
func (c Calculator) DivideBy(divisor float64) (float64, float64, error) {
    if divisor == 0 {
        return 0, 0, fmt.Errorf("除数はゼロにできません")
    }
    return c.Value / divisor, math.Mod(c.Value, divisor), nil
}

質問4:埋め込み構造体のメソッド衝突はどうなる?

2つの埋め込まれた型が同じ名前のメソッドを持つ場合、Goコンパイラはエラーを報告するため、どちらを呼び出すかを明示的に指定する必要があります。

GO
type A struct{}
func (A) Hello() string { return "A" }

type B struct{}
func (B) Hello() string { return "B" }

type C struct {
    A
    B
}

func main() {
    c := C{}
    // c.Hello()            // ❌ コンパイルエラー:曖昧なセレクタ c.Hello
    fmt.Println(c.A.Hello()) // ✅ 明示的:出力 "A"
    fmt.Println(c.B.Hello()) // ✅ 明示的:出力 "B"
}

📖 まとめ

このレッスンではGoのメソッドについて学びました:

  1. メソッドの定義:レシーバを通じて関数を型に結びつける
  2. 値レシーバとポインタレシーバ:値レシーバはコピーに対して操作し、ポインタレシーバは元の値を変更できる
  3. メソッドセットのルール:値型は値レシーバのメソッドのみを持ち、ポインタ型はすべてのメソッドを持つ
  4. 継承より合成:埋め込み構造体を通じてメソッドの再利用とオーバーライドを実現
  5. ベストプラクティス:レシーバ名を短く保ち、同じ型には一貫したレシーバ型を使用し、変更が必要な場合はポインタを使用

メソッドはGoにおけるオブジェクト指向プログラミングの基盤です。次のレッスンではインターフェースについて学びます — Goの強力な多態性の仕組みで、メソッドと密接に連携してGoの型システムの中核を形成します。


📝 演習

演習1:温度変換 ⭐

摂氏フィールドを持つ Temperature 構造体を作成し、以下のメソッドを実装してください:

GO
// 期待される出力:
// Temperature: 100.00°C = 212.00°F = 373.15K

演習2:銀行口座(取引履歴付き) ⭐⭐

場面2のAccount構造体を以下の機能で拡張してください:

GO
// 期待される出力:
// === 口座明細書 ===
// 口座名:Zhang San
// 取引履歴:
//   1. +1000.00  (初回入金)
//   2. -200.00   (購入)
//   3. +500.00   (給与)
// 現在の残高:1300.00

演習3:図形システム(合成継承) ⭐⭐⭐

図形システムを作成してください:

  1. Area() float64Perimeter() float64 メソッドを持つ Shape ベースインターフェースを定義
  2. RectangleCircleTriangle 構造体を実装
  3. 複数の図形を保持できる Canvas 構造体を作成
  4. Canvas.TotalArea() ですべての図形の合計面積を計算
  5. 埋め込み構造体を使って ColoredRectangle を作成(Rectangleのメソッドと Color フィールドの両方を持つ)
GO
// 期待される出力:
// Canvas には3つの図形があります
// 合計面積:xxx.xx
// 赤い長方形:幅=10、高さ=5、色=赤

次のレッスン

レッスン9:インターフェース — Goのインターフェースシステムについて学びます:暗黙の実装、空のインターフェース、型アサーション、インターフェースの合成、そしてGoの最も強力な多態性の仕組み。

Web-Tutorial.com

Web-Tutorial 技術チーム

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

100%