1. 什么是方法
方法是Go语言中与特定类型关联的函数。如果说函数是独立的功能单元,那么方法就是"属于"某个类型的特殊函数。方法让数据类型具备了行为能力,这是面向对象编程的重要特征。
1.1 方法与函数的区别
让我们通过一个简单的例子来理解:
go
// 普通函数
func add(a, b int) int {
return a + b
}
// 方法(后面会详细讲解语法)
// func (接收者) 方法名(参数) 返回值 { 函数体 }
主要区别:
- 函数:独立存在,通过函数名调用
- 方法:与特定类型绑定,通过"类型.方法名"调用
1.2 为什么需要方法
方法的主要价值:
- 封装性:将数据和操作数据的方法组织在一起
- 可读性 :
student.GetName()比getName(student)更直观 - 面向对象:为后续的接口实现打基础
- 代码组织:更好地组织相关功能
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语言面向对象特性的关键,也为后续学习接口打下了坚实基础。