404 Not Found

404 Not Found


nginx

方法

第8课:方法

生活类比

想象你买了一辆车。车有各种功能——启动、加速、刹车、开灯。这些功能不是独立存在的,它们属于这辆车。你不会说"启动"是一个通用动作,你会说"启动了"。

在 Go 中,方法就是绑定了特定类型的函数。就像"启动"属于"车"一样,方法属于它所绑定的类型。你不会把启动功能放在冰箱上——同理,方法定义在它真正服务的类型上。


核心概念

什么是方法?

方法是一种带有接收者(receiver)的函数。接收者将方法绑定到某个类型上,使得该类型的变量可以直接调用这个方法。

接收者(Receiver)

接收者是方法与类型之间的桥梁。它的语法出现在 func 关键字和方法名之间:

GO
func (接收者变量 接收者类型) 方法名(参数列表) 返回类型 {
    // 方法体
}

值接收者 vs 指针接收者

特性 值接收者 func (s Struct) 指针接收者 func (s *Struct)
操作副本 是(修改不影响原值) 否(直接修改原值)
适用场景 只读操作、小型结构体 需要修改字段、大型结构体
接口实现 值和指针均可调用 仅指针类型可调用

方法集规则

这意味着:如果你有一个 *T 类型的变量,它可以调用 T*T 的所有方法;而 T 类型的变量只能调用 T 的方法。

组合优于继承

Go 没有类继承,而是通过组合(embedding)实现代码复用。将一个结构体嵌入另一个结构体,内嵌结构体的方法会自动"提升"到外层结构体。


基本语法与用法

定义方法

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: "旺财"},
        Breed:  "金毛",
    }

    // 可以直接调用嵌入类型的方法
    fmt.Println(dog.Speak()) // 输出: 旺财 发出声音
    fmt.Println(dog.Bark())  // 输出: 旺财 汪汪叫!
}
💡 Tip:方法名要简洁且有意义 方法名应该是一个动词或动词短语,描述它执行的操作。例如 Area()Scale()Save()IsValid()

💡 Tip:保持接收者名称简短 接收者变量通常用类型名的首字母或前两个字母。例如 func (r Rectangle) 而不是 func (rect Rectangle)。这是 Go 的惯例。

💡 Tip:一致性原则 同一个类型的所有方法应该使用相同类型的接收者——要么都是值接收者,要么是指针接收者(需要修改状态时)。不要混用。

💡 Tip:指针接收者的时机 当结构体包含不可复制的字段(如 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

示例:值接收者 vs 指针接收者(难度⭐⭐)

演示两者在行为上的关键差异。

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: "张三", 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
账户[张三] 余额: 1000.00

--- 交易记录 ---
  存入 500.00 元,余额: 1500.00 元
  取出 200.00 元,余额: 1300.00 元
  取款 2000.00 元失败,余额不足

--- 最终状态 ---
账户[张三] 余额: 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: "李四"},
        Department: "技术部",
        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: "王五"},
            Department: "研发部",
            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=李四, Dept=技术部, Salary=15000]
我是 李四 (ID: 2)
年薪: 180000 元

=== Manager ===
Manager[ID=3, Name=王五, Dept=研发部, Team=8人]
我是 王五 (ID: 3)
年薪: 300000 元
王五 管理一个 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:购物车系统

使用方法实现购物车的增删改查和结算。

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: "张三"}

    // 定义商品
    laptop := Product{Name: "笔记本电脑", Price: 6999, Category: "电子产品"}
    mouse := Product{Name: "无线鼠标", Price: 129, Category: "电子产品"}
    book := Product{Name: "Go语言圣经", Price: 79, Category: "图书"}
    coffee := Product{Name: "咖啡", Price: 35, Category: "食品"}

    // 添加商品
    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("咖啡")
    cart.Display()
}

输出:

TEXT
添加商品:
  添加商品: 笔记本电脑 x1
  添加商品: 无线鼠标 x2
  添加商品: Go语言圣经 x3
  添加商品: 咖啡 x5

🛒 张三 的购物车 (4 种商品):
  ─────────────────────────────────────
  笔记本电脑    ¥6999.00 x 1 = ¥6999.00
  无线鼠标      ¥129.00 x 2 = ¥258.00
  Go语言圣经    ¥79.00 x 3 = ¥237.00
  咖啡          ¥35.00 x 5 = ¥175.00
  ─────────────────────────────────────
  合计: ¥7669.00

更新商品数量:
  更新数量: 无线鼠标 x3
  更新数量: Go语言圣经 x2

🛒 张三 的购物车 (4 种商品):
  ─────────────────────────────────────
  笔记本电脑    ¥6999.00 x 1 = ¥6999.00
  无线鼠标      ¥129.00 x 3 = ¥387.00
  Go语言圣经    ¥79.00 x 2 = ¥158.00
  咖啡          ¥35.00 x 5 = ¥175.00
  ─────────────────────────────────────
  合计: ¥7719.00

移除商品:
  移除商品: 咖啡

🛒 张三 的购物车 (3 种商品):
  ─────────────────────────────────────
  笔记本电脑    ¥6999.00 x 1 = ¥6999.00
  无线鼠标      ¥129.00 x 3 = ¥387.00
  Go语言圣经    ¥79.00 x 2 = ¥158.00
  ─────────────────────────────────────
  合计: ¥7544.00

❓ 常见问题

Q1:值接收者和指针接收者到底怎么选?

遵循以下规则:

  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 不可复制

Q2:为什么我调用方法时编译器报错?

最常见的原因是混淆了值接收者和指针接收者的调用方式

GO
type Dog struct { Name string }

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

func main() {
    d := Dog{Name: "旺财"}
    d.Rename("来福")    // ✅ Go 自动取地址,等价于 (&d).Rename("来福")

    // 但下面这种情况会报错:
    // Dog{"旺财"}.Rename("来福")  // ❌ 不能对不可寻址的值取地址
}

Go 编译器会自动处理 d.Rename()(&d).Rename() 的转换,但前提是变量必须是可寻址的(addressable)。字面量和临时值不可寻址。

Q3:方法可以返回多个值吗?

可以,方法和函数的签名规则完全相同,可以有任意数量的参数和返回值。

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
}

Q4:嵌入结构体的方法冲突怎么办?

当两个嵌入类型有同名方法时,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()            // ❌ 编译错误:ambiguous selector c.Hello
    fmt.Println(c.A.Hello()) // ✅ 显式指定: 输出 "A"
    fmt.Println(c.B.Hello()) // ✅ 显式指定: 输出 "B"
}

📖 小节

本课我们学习了 Go 语言中的方法:

  1. 方法定义:通过接收者将函数绑定到类型上
  2. 值接收者 vs 指针接收者:值接收者操作副本,指针接收者可修改原始值
  3. 方法集规则:值类型只有值接收者方法,指针类型拥有所有方法
  4. 组合优于继承:通过嵌入结构体实现方法的复用和覆盖
  5. 最佳实践:接收者名称简短、同一类型保持接收者类型一致、需要修改时用指针

方法是 Go 面向对象编程的基础。下一课我们将学习接口(interface)——Go 实现多态的利器,它与方法紧密配合,构成 Go 类型系统的核心。


📝 作业

练习1:温度转换器 ⭐

创建一个 Temperature 结构体,包含摄氏度字段,并实现以下方法:

GO
// 参考输出:
// 温度: 100.00°C = 212.00°F = 373.15K

练习2:银行账户(带交易记录) ⭐⭐

扩展场景2中的 Account 结构体,添加以下功能:

GO
// 参考输出:
// === 账户报表 ===
// 账户: 张三
// 交易记录:
//   1. +1000.00  (开户)
//   2. -200.00   (消费)
//   3. +500.00   (工资)
// 当前余额: 1300.00

练习3:图形接口(组合继承) ⭐⭐⭐

创建一个图形系统:

  1. 定义 Shape 基础接口,包含 Area() float64Perimeter() float64 方法
  2. 实现 RectangleCircleTriangle 三个结构体
  3. 创建 Canvas 结构体,可以添加多个图形
  4. 实现 Canvas.TotalArea() 计算所有图形的总面积
  5. 使用嵌入结构体的方式,创建 ColoredRectangle,它既有 Rectangle 的方法,又有 Color 字段
GO
// 参考输出:
// 画布上有 3 个图形
// 总面积: xxx.xx
// 红色矩形: 宽=10, 高=5, 颜色=red

下一课

第9课:接口 — 学习 Go 的接口系统:隐式实现、空接口、类型断言、接口组合,以及 Go 最强大的多态机制。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏