Golang学习历程【第十篇 方法(method)与接收者】

1. 什么是方法

方法是Go语言中与特定类型关联的函数。如果说函数是独立的功能单元,那么方法就是"属于"某个类型的特殊函数。方法让数据类型具备了行为能力,这是面向对象编程的重要特征。

1.1 方法与函数的区别

让我们通过一个简单的例子来理解:

go 复制代码
// 普通函数
func add(a, b int) int {
    return a + b
}

// 方法(后面会详细讲解语法)
// func (接收者) 方法名(参数) 返回值 { 函数体 }

主要区别

  • 函数:独立存在,通过函数名调用
  • 方法:与特定类型绑定,通过"类型.方法名"调用

1.2 为什么需要方法

方法的主要价值:

  1. 封装性:将数据和操作数据的方法组织在一起
  2. 可读性student.GetName()getName(student) 更直观
  3. 面向对象:为后续的接口实现打基础
  4. 代码组织:更好地组织相关功能

2. 方法的基本语法

2.1 方法定义语法

go 复制代码
func (接收者) 方法名(参数列表) (返回值列表) {
    // 方法体
}

2.2 接收者的两种形式

值接收者

go 复制代码
func (t MyType) MethodName(parameters) returnType {
    // t是MyType类型的副本
    // 修改t不会影响原始数据
}

指针接收者

go 复制代码
func (t *MyType) MethodName(parameters) returnType {
    // t是指向MyType类型的指针
    // 修改t会影响原始数据
}

2.3 简单示例

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) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

// 指针接收者方法:缩放矩形
func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}

// 指针接收者方法:设置尺寸
func (r *Rectangle) SetDimensions(width, height float64) {
    r.Width = width
    r.Height = height
}

func main() {
    // 创建矩形实例
    rect := Rectangle{Width: 10, Height: 5}
    
    fmt.Printf("原始矩形:宽度=%.2f, 高度=%.2f\n", rect.Width, rect.Height)
    fmt.Printf("面积:%.2f\n", rect.Area())
    fmt.Printf("周长:%.2f\n", rect.Perimeter())
    
    // 使用指针方法修改数据
    rect.Scale(2.0)
    fmt.Printf("\n缩放后矩形:宽度=%.2f, 高度=%.2f\n", rect.Width, rect.Height)
    fmt.Printf("新面积:%.2f\n", rect.Area())
    
    rect.SetDimensions(15, 8)
    fmt.Printf("\n重新设置后:宽度=%.2f, 高度=%.2f\n", rect.Width, rect.Height)
    fmt.Printf("最终面积:%.2f\n", rect.Area())
}

运行结果:

bash 复制代码
原始矩形:宽度=10.00, 高度=5.00
面积:50.00
周长:30.00

缩放后矩形:宽度=20.00, 高度=10.00
新面积:200.00

重新设置后:宽度=15.00, 高度=8.00
最终面积:120.00

3. 接收者的深入理解

3.1 值接收者 vs 指针接收者

go 复制代码
package main

import "fmt"

type Counter struct {
    Count int
}

// 值接收者方法
func (c Counter) GetValue() int {
    return c.Count
}

// 值接收者方法(不会修改原始值)
func (c Counter) IncrementByValue() {
    c.Count++  // 这里修改的是副本
    fmt.Printf("值接收者内部Count: %d\n", c.Count)
}

// 指针接收者方法
func (c *Counter) GetPointerValue() int {
    return c.Count
}

// 指针接收者方法(会修改原始值)
func (c *Counter) IncrementByPointer() {
    c.Count++  // 这里修改的是原始值
    fmt.Printf("指针接收者内部Count: %d\n", c.Count)
}

func main() {
    counter := Counter{Count: 10}
    
    fmt.Printf("初始值:%d\n", counter.Count)
    
    // 值接收者调用
    counter.IncrementByValue()
    fmt.Printf("值接收者调用后:%d\n", counter.Count)
    
    // 指针接收者调用
    counter.IncrementByPointer()
    fmt.Printf("指针接收者调用后:%d\n", counter.Count)
    
    // 验证返回值
    fmt.Printf("GetValue(): %d\n", counter.GetValue())
    fmt.Printf("GetPointerValue(): %d\n", counter.GetPointerValue())
}

运行结果:

bash 复制代码
初始值:10
值接收者内部Count: 11
值接收者调用后:10
指针接收者内部Count: 11
指针接收者调用后:11
GetValue(): 11
GetPointerValue(): 11

3.2 接收者的选择原则

什么时候使用值接收者:

✅ 数据类型很小(如基本类型、小结构体)

✅ 方法不需要修改接收者数据

✅ 方法需要并发安全(值传递是安全的)

什么时候使用指针接收者:

✅ 方法需要修改接收者数据

✅ 数据类型较大(避免拷贝开销)

✅ 一致性考虑(类型的所有方法都用指针接收者)

go 复制代码
package main

import "fmt"

type SmallData struct {
    Value int
}

type LargeData struct {
    Values [1000]int  // 大数组
    Name   string
    Active bool
}

// SmallData使用值接收者(合理)
func (s SmallData) Double() SmallData {
    s.Value *= 2
    return s
}

// LargeData使用指针接收者(合理)
func (l *LargeData) Activate() {
    l.Active = true
}

func main() {
    // 小数据类型
    small := SmallData{Value: 5}
    doubled := small.Double()
    fmt.Printf("SmallData - 原始: %d, 翻倍后: %d\n", small.Value, doubled.Value)
    
    // 大数据类型
    large := LargeData{Name: "Test"}
    fmt.Printf("LargeData - 修改前Active: %t\n", large.Active)
    large.Activate()
    fmt.Printf("LargeData - 修改后Active: %t\n", large.Active)
}

运行结果:

bash 复制代码
SmallData - 原始: 5, 翻倍后: 10
LargeData - 修改前Active: false
LargeData - 修改后Active: true

4. 不同类型的接收者

4.1 基本类型的方法

go 复制代码
package main

import "fmt"

// 为int类型创建别名
type MyInt int

// 为自定义类型添加方法
func (mi MyInt) Double() MyInt {
    return mi * 2
}

func (mi MyInt) IsEven() bool {
    return mi%2 == 0
}

func (mi *MyInt) Increment() {
    *mi++
}

// 为内置类型创建方法(需要类型别名)
type Celsius float64
type Fahrenheit float64

func (c Celsius) ToFahrenheit() Fahrenheit {
    return Fahrenheit(c*9/5 + 32)
}

func (f Fahrenheit) ToCelsius() Celsius {
    return Celsius((f - 32) * 5 / 9)
}

func main() {
    // 自定义int类型
    num := MyInt(10)
    fmt.Printf("原始值:%d\n", num)
    fmt.Printf("翻倍:%d\n", num.Double())
    fmt.Printf("是否偶数:%t\n", num.IsEven())
    
    num.Increment()
    fmt.Printf("递增后:%d\n", num)
    
    // 温度转换
    tempC := Celsius(25)
    tempF := tempC.ToFahrenheit()
    fmt.Printf("\n%d°C = %.1f°F\n", int(tempC), float64(tempF))
    
    backToC := tempF.ToCelsius()
    fmt.Printf("%.1f°F = %d°C\n", float64(tempF), int(backToC))
}

运行结果:

bash 复制代码
原始值:10
翻倍:20
是否偶数:true
递增后:11

25°C = 77.0°F
77.0°F = 25°C

4.2 结构体方法

go 复制代码
package main

import (
    "fmt"
    "math"
)

type Point struct {
    X, Y float64
}

type Circle struct {
    Center Point
    Radius float64
}

// Point的方法
func (p Point) Distance(other Point) float64 {
    dx := p.X - other.X
    dy := p.Y - other.Y
    return math.Sqrt(dx*dx + dy*dy)
}

func (p Point) Move(dx, dy float64) Point {
    return Point{X: p.X + dx, Y: p.Y + dy}
}

// Circle的方法
func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (c Circle) Circumference() float64 {
    return 2 * math.Pi * c.Radius
}

func (c *Circle) Scale(factor float64) {
    c.Radius *= factor
}

func main() {
    // 点的操作
    p1 := Point{X: 0, Y: 0}
    p2 := Point{X: 3, Y: 4}
    
    distance := p1.Distance(p2)
    fmt.Printf("两点距离:%.2f\n", distance)
    
    p3 := p1.Move(1, 1)
    fmt.Printf("移动后点:(%f, %f)\n", p3.X, p3.Y)
    
    // 圆的操作
    circle := Circle{
        Center: Point{X: 0, Y: 0},
        Radius: 5,
    }
    
    fmt.Printf("\n圆的半径:%.2f\n", circle.Radius)
    fmt.Printf("圆的面积:%.2f\n", circle.Area())
    fmt.Printf("圆的周长:%.2f\n", circle.Circumference())
    
    circle.Scale(2)
    fmt.Printf("\n缩放后半径:%.2f\n", circle.Radius)
    fmt.Printf("缩放后面积:%.2f\n", circle.Area())
}

运行结果:

bash 复制代码
两点距离:5.00
移动后点:(1.000000, 1.000000)

圆的半径:5.00
圆的面积:78.54
圆的周长:31.42

缩放后半径:10.00
缩放后面积:314.16

4.3 切片和映射的方法

go 复制代码
package main

import "fmt"

// 为切片类型创建别名
type IntSlice []int

// 为映射类型创建别名
type StringMap map[string]string

// IntSlice的方法
func (is IntSlice) Sum() int {
    sum := 0
    for _, v := range is {
        sum += v
    }
    return sum
}

func (is IntSlice) Average() float64 {
    if len(is) == 0 {
        return 0
    }
    return float64(is.Sum()) / float64(len(is))
}

func (is *IntSlice) Append(value int) {
    *is = append(*is, value)
}

// StringMap的方法
func (sm StringMap) Get(key string) (string, bool) {
    value, exists := sm[key]
    return value, exists
}

func (sm StringMap) Set(key, value string) {
    sm[key] = value
}

func (sm StringMap) Keys() []string {
    keys := make([]string, 0, len(sm))
    for k := range sm {
        keys = append(keys, k)
    }
    return keys
}

func main() {
    // 切片方法
    numbers := IntSlice{1, 2, 3, 4, 5}
    fmt.Printf("数字切片:%v\n", numbers)
    fmt.Printf("总和:%d\n", numbers.Sum())
    fmt.Printf("平均值:%.2f\n", numbers.Average())
    
    numbers.Append(6)
    numbers.Append(7)
    fmt.Printf("添加元素后:%v\n", numbers)
    fmt.Printf("新总和:%d\n", numbers.Sum())
    
    // 映射方法
    studentGrades := StringMap{
        "张三": "85",
        "李四": "92",
        "王五": "78",
    }
    
    fmt.Printf("\n学生 grades:%v\n", studentGrades)
    
    // 获取值
    if grade, exists := studentGrades.Get("李四"); exists {
        fmt.Printf("李四的成绩:%s\n", grade)
    }
    
    // 设置新值
    studentGrades.Set("赵六", "88")
    fmt.Printf("添加赵六后:%v\n", studentGrades)
    
    // 获取所有键
    keys := studentGrades.Keys()
    fmt.Printf("所有学生:%v\n", keys)
}

运行结果:

bash 复制代码
数字切片:[1 2 3 4 5]
总和:15
平均值:3.00
添加元素后:[1 2 3 4 5 6 7]
新总和:28

学生 grades:map[张三:85 李四:92 王五:78]
李四的成绩:92
添加赵六后:map[张三:85 李四:92 王五:78 赵六:88]
所有学生:[张三 李四 王五 赵六]

5. 方法与接口的关系

5.1 方法集的概念

go 复制代码
package main

import "fmt"

type Shape interface {
    Area() float64
    Perimeter() float64
}

type Rectangle struct {
    Width  float64
    Height float64
}

type Circle struct {
    Radius float64
}

// Rectangle实现Shape接口
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

// Circle实现Shape接口
func (c Circle) Area() float64 {
    return 3.14159 * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * 3.14159 * c.Radius
}

// 使用接口的函数
func PrintShapeInfo(s Shape) {
    fmt.Printf("面积:%.2f\n", s.Area())
    fmt.Printf("周长:%.2f\n", s.Perimeter())
}

func main() {
    rect := Rectangle{Width: 10, Height: 5}
    circle := Circle{Radius: 3}
    
    fmt.Println("=== 矩形 ===")
    PrintShapeInfo(rect)
    
    fmt.Println("\n=== 圆形 ===")
    PrintShapeInfo(circle)
    
    // 接口切片
    shapes := []Shape{rect, circle}
    fmt.Println("\n=== 批量处理 ===")
    for i, shape := range shapes {
        fmt.Printf("形状%d:\n", i+1)
        PrintShapeInfo(shape)
        fmt.Println()
    }
}

运行结果:

bash 复制代码
=== 矩形 ===
面积:50.00
周长:30.00

=== 圆形 ===
面积:28.27
周长:18.85

=== 批量处理 ===
形状1:
面积:50.00
周长:30.00

形状2:
面积:28.27
周长:18.85

6. 方法的高级用法

6.1 方法链式调用

go 复制代码
package main

import "fmt"

type Calculator struct {
    Result float64
}

func (c Calculator) Add(x float64) Calculator {
    c.Result += x
    return c
}

func (c Calculator) Subtract(x float64) Calculator {
    c.Result -= x
    return c
}

func (c Calculator) Multiply(x float64) Calculator {
    c.Result *= x
    return c
}

func (c Calculator) Divide(x float64) Calculator {
    if x != 0 {
        c.Result /= x
    }
    return c
}

func (c Calculator) GetValue() float64 {
    return c.Result
}

func main() {
    calc := Calculator{Result: 10}
    
    result := calc.
        Add(5).
        Multiply(2).
        Subtract(3).
        Divide(2).
        GetValue()
    
    fmt.Printf("计算结果:%.2f\n", result)
    
    // 分步演示
    calc2 := Calculator{Result: 10}
    fmt.Printf("初始值:%.2f\n", calc2.GetValue())
    
    calc2 = calc2.Add(5)
    fmt.Printf("加5后:%.2f\n", calc2.GetValue())
    
    calc2 = calc2.Multiply(2)
    fmt.Printf("乘2后:%.2f\n", calc2.GetValue())
    
    calc2 = calc2.Subtract(3)
    fmt.Printf("减3后:%.2f\n", calc2.GetValue())
    
    calc2 = calc2.Divide(2)
    fmt.Printf("除2后:%.2f\n", calc2.GetValue())
}

运行结果:

bash 复制代码
计算结果:13.50
初始值:10.00
加5后:15.00
乘2后:30.00
减3后:27.00
除2后:13.50

6.2 嵌入类型的方法提升

go 复制代码
package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func (p Person) GetName() string {
    return p.Name
}

func (p Person) GetAge() int {
    return p.Age
}

type Student struct {
    Person      // 嵌入Person
    School string
    Grade  int
}

func (s Student) GetSchool() string {
    return s.School
}

type Teacher struct {
    Person      // 嵌入Person
    Subject string
    Salary  float64
}

func (t Teacher) GetSubject() string {
    return t.Subject
}

func main() {
    student := Student{
        Person: Person{Name: "小明", Age: 18},
        School: "第一中学",
        Grade:  12,
    }
    
    teacher := Teacher{
        Person:  Person{Name: "王老师", Age: 35},
        Subject: "数学",
        Salary:  8000,
    }
    
    // 可以直接调用嵌入类型的方法
    fmt.Printf("学生姓名:%s\n", student.GetName())    // 提升方法
    fmt.Printf("学生年龄:%d\n", student.GetAge())     // 提升方法
    fmt.Printf("学校:%s\n", student.GetSchool())
    fmt.Printf("年级:%d\n", student.Grade)
    
    fmt.Printf("\n老师姓名:%s\n", teacher.GetName())  // 提升方法
    fmt.Printf("老师年龄:%d\n", teacher.GetAge())     // 提升方法
    fmt.Printf("科目:%s\n", teacher.GetSubject())
    fmt.Printf("薪资:%.2f\n", teacher.Salary)
    
    // 验证类型关系
    fmt.Printf("\n学生也是Person类型:%t\n", isPerson(student))
    fmt.Printf("老师也是Person类型:%t\n", isPerson(teacher))
}

func isPerson(p Person) bool {
    return true
}

运行结果:

bash 复制代码
学生姓名:小明
学生年龄:18
学校:第一中学
年级:12

老师姓名:王老师
老师年龄:35
科目:数学
薪资:8000.00

学生也是Person类型:true
老师也是Person类型:true

7. 方法的注意事项

7.1 方法名冲突

go 复制代码
package main

import "fmt"

type Base struct {
    Name string
}

func (b Base) GetName() string {
    return b.Name
}

type Derived struct {
    Base
    Name string  // 与Base中的Name同名
}

func (d Derived) GetName() string {
    // 明确指定调用哪个GetName
    return "Derived: " + d.Name + ", Base: " + d.Base.GetName()
}

func main() {
    derived := Derived{
        Base: Base{Name: "BaseName"},
        Name: "DerivedName",
    }
    
    fmt.Printf("调用派生类GetName: %s\n", derived.GetName())
    fmt.Printf("直接访问Name: %s\n", derived.Name)
    fmt.Printf("访问Base.Name: %s\n", derived.Base.Name)
}

运行结果:

bash 复制代码
调用派生类GetName: Derived: DerivedName, Base: BaseName
直接访问Name: DerivedName
访问Base.Name: BaseName

7.2 空指针调用方法

go 复制代码
package main

import "fmt"

type Data struct {
    Value int
}

func (d *Data) GetValue() int {
    if d == nil {
        return 0  // 安全检查
    }
    return d.Value
}

func (d *Data) SetValue(v int) {
    if d != nil {  // 安全检查
        d.Value = v
    }
}

func main() {
    var ptr *Data = nil
    
    // 空指针调用方法(安全)
    fmt.Printf("空指针GetValue: %d\n", ptr.GetValue())
    
    // 空指针调用修改方法(安全)
    ptr.SetValue(100)  // 不会有问题,因为有安全检查
    
    // 创建实际对象
    actual := &Data{Value: 42}
    fmt.Printf("实际对象GetValue: %d\n", actual.GetValue())
    
    actual.SetValue(99)
    fmt.Printf("修改后GetValue: %d\n", actual.GetValue())
}

运行结果:

bash 复制代码
空指针GetValue: 0
实际对象GetValue: 42
修改后GetValue: 99

8. 总结

方法是Go语言实现面向对象编程的核心机制:

核心概念

  • 方法是与特定类型绑定的函数
  • 通过接收者将方法与类型关联
  • 值接收者和指针接收者有不同的行为特点

使用原则

  • 值接收者:适用于不修改数据、小型数据类型
  • 指针接收者:适用于需要修改数据、大型数据类型
  • 保持类型所有方法接收者类型的一致性

重要特性

  • 方法提升:嵌入类型的方法可以被直接调用
  • 接口实现:方法是实现接口的基础
  • 链式调用:返回接收者类型支持方法链

掌握方法的使用是理解Go语言面向对象特性的关键,也为后续学习接口打下了坚实基础。


上一篇:Golang学习历程【Golang学习历程【第九篇 结构体(struct)】

下一篇:Golang学习历程【第十一篇 接口(interface)】

相关推荐
宇钶宇夕2 小时前
CoDeSys入门实战一起学习(二十八):(ST)三台电机顺起逆停程序详解
运维·学习·自动化·软件工程
u0109272712 小时前
C++与人工智能框架
开发语言·c++·算法
挖矿大亨2 小时前
C++中空指针访问成员函数
开发语言·c++
im_AMBER2 小时前
Leetcode 111 两数相加
javascript·笔记·学习·算法·leetcode
团子的二进制世界2 小时前
Sentinel-服务保护(限流、熔断降级)
java·开发语言·sentinel·异常处理
虫小宝2 小时前
淘客系统的容灾演练与恢复:Java Chaos Monkey模拟节点故障下的服务降级与快速切换实践
java·开发语言
zz34572981132 小时前
c语言基础概念9
c语言·开发语言
yxm26336690812 小时前
【洛谷压缩技术续集题解】
java·开发语言·算法
键盘帽子2 小时前
多线程情况下长连接中的session并发问题
java·开发语言·spring boot·spring·spring cloud