6.1 方法(Method)—— 给 struct 绑定函数

Go 没有 class,但有「方法」——它就是绑定到某个类型上的函数

type User struct {
    Name string
    Age  int
}

// 方法定义:在 func 后加 (接收者) 来绑定
// 这个u是接收者变量(Receiver Variable),作用域仅限于这个方法内部,可以随便写。
// 如果方法体内用不到这个实例,你甚至可以省略变量名,只写类型:
func (u User) Greet() string {
    return "Hi, I'm " + u.Name
}

u := User{Name: "张三"}
fmt.Println(u.Greet())   // Hi, I'm 张三

(u User)接收者(receiver),这个u是接收者变量(Receiver Variable),作用域仅限于这个方法内部,可以随便写。相当于 JS 里的 this 或 PHP 里的 $this。但 Go 官方和社区强烈不推荐thisself。官方惯例是使用类型首字母的小写缩写。如果方法体内用不到这个实例,你甚至可以省略变量名,只写类型:

func (User) Greet() string {
    return "Hi, I'm " + u.Name
}

接收者变量(u 代表调用这个方法的实例,但具体是原件还是复印件

  • (u User)复印件(内部修改不影响外部,只读推荐)。
  • (u *User)原件地址(内部修改影响外部,增删改推荐)。

对比 JS/PHP:JS/PHP 把数据和方法都塞在 class 里。Go 把它们拆开定义(数据在 struct,方法在外面绑定),但调用形式一样:u.Greet()

⚠️ 先记住一个限制:方法只能定义在「包级别」

和函数一样(第 03 章 3.5.1 讲过),方法定义(带接收者的函数)只能写在包级别,不能嵌套在函数内部

// ❌ 错误:方法定义不能嵌套在函数里
func main() {
    func (u User) Greet() string {    // 编译错误
        return u.Name
    }
}

为什么? 因为「User 有 Greet 方法」是类型层面的静态信息——编译期就确定了。而函数内部是运行时的执行逻辑,两者不该混。如果允许在函数内定义方法,会出现「进 if 分支才有这个方法」「每次调用函数都重新定义方法」这种诡异问题。

Go 直接禁止:方法和 struct 一样,平铺在包级别。实际项目里,一个文件的标准布局是:

package user

// 1. struct 定义(类型)
type User struct {
    Name string
    Age  int
}

// 2. 方法定义(和 struct 平铺,通常在它下面)
func (u User) Greet() string { return "Hi, " + u.Name }
func (u *User) HaveBirthday() { u.Age++ }
func (u User) IsAdult() bool { return u.Age >= 18 }

struct 在上、方法在下,平铺在一个文件里——这是 Go 代码的标准长相。你在第 13 章博客项目里会看到大量这种布局。


6.2 值接收者 vs 指针接收者

这是 Go 方法最重要的决策点:

// 值接收者:u 是拷贝,修改不影响原对象
func (u User) SetAgeValue(age int) {
    u.Age = age    // 改的是拷贝
}

// 指针接收者:u 是指针,修改影响原对象
func (u *User) SetAge(age int) {
    u.Age = age    // 改原对象
}

调用对比

u := User{Name: "张三", Age: 28}

u.SetAgeValue(99)
fmt.Println(u.Age)   // 28(没变)

u.SetAge(99)         // Go 自动取地址调用 (&u).SetAge(99)
fmt.Println(u.Age)   // 99(变了)

这其实就是第 05 章的「值 vs 指针」,应用到方法上。经验法则:默认用指针接收者(除非 struct 很小且只读)。同一个类型的所有方法接收者要统一。


6.3 接口(Interface)—— Go 的核心抽象

开篇:接口到底解决了什么问题

先讲个你可能遇到过的事:你写了个支付功能,第一版只支持支付宝。后来产品说「加个微信支付」,你改了一堆 if-else。再后来「加个银联」,又改……每加一种支付,结账代码就得动一次。

这就是「紧耦合」——代码写死了具体类型,没有扩展余地。

接口就是解决这个问题的。它的核心思想一句话:「我不关心你是谁,我只关心你能干什么」

结账函数不写死「用支付宝」,而是说「给我一个会 Pay() 的东西就行」。支付宝、微信、银联,只要都会 Pay(),结账代码一个字都不用改。新增支付方式?写个新类型实现 Pay() 即可。

这就是 Go 接口的威力。而且 Go 的接口比 Java/PHP 更彻底——你连 implements 都不用写,会了就算。我们一步步演示。

6.3.1 一个例子先看懂

// 1. 定义接口:规定「会 Speak() 的就算 Speaker」
type Speaker interface {
    Speak() string
}

// 2. User 实现了 Speak(),自动满足 Speaker
func (u User) Speak() string {
    return "我是用户 " + u.Name
}

// 3. Dog 也实现了 Speak(),也满足 Speaker
type Dog struct{ Name string }
func (d Dog) Speak() string {
    return "汪汪!我是 " + d.Name
}

// 4. 函数参数用接口类型,能接收任何实现者
func introduce(s Speaker) {
    fmt.Println(s.Speak())
}

// 5. 调用:User 和 Dog 都能传进去
u := User{Name: "张三"}
d := Dog{Name: "旺财"}
introduce(u)   // 我是用户 张三
introduce(d)   // 汪汪!我是 旺财

注意第 2、3 步:User 和 Dog 都没写「我是 Speaker」,但因为它们都有 Speak() string 方法,Go 编译器就认定它们满足 Speaker 接口。

6.3.2 隐式实现:Go 的颠覆性设计

对比 PHP / Java:

// PHP:必须显式写 implements
class User implements Speaker {     // ← 这一行必须有
    public function speak() { ... }
}
// Go:完全不写 implements!
type User struct { ... }
func (u User) Speak() string { ... }
// User 自动满足 Speaker 接口,无需任何声明

这就是 Go 的「鸭子类型」——「如果它走起来像鸭子、叫起来像鸭子,那它就是鸭子」。

执行过程可视化——当你写 introduce(d) 时,Go 编译器在背后做什么:

1. introduce 的参数类型是 Speaker(接口)
2. 你传入了 d,类型是 Dog
3. 编译器检查:Dog 有没有 Speak() string 方法?
   └─ Dog 的方法集:{ Speak() string }  ✓ 匹配
4. 把 d 包装成「Speaker 接口值」传入
5. 运行时调用 s.Speak(),实际执行 Dog 的 Speak 方法(动态分派)

好处

  1. 解耦:定义接口的人不需要知道实现者是谁
  2. 能 retrofit:可以给已有的、甚至是标准库的类型赋予新接口
  3. 测试友好:mock 一个接口实现,无需改原代码(第 13 章博客项目就是这么做的)

6.3.3 接口组合

接口可以组合多个接口:

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

// 组合
type ReadWriter interface {
    Reader
    Writer
}

标准库的 io.Readerio.Writer 就是这么定义的——文件、网络连接、缓冲区都实现它们。这就是 Go 强大的抽象能力


6.4 空接口 any 与类型断言

6.4.1 空接口

Go 1.18 起 anyinterface{} 的别名。空接口没有方法,所以所有类型都满足它——类似 JS 的「任意值」。

func printAny(v any) { fmt.Println(v) }
printAny(42)
printAny("hello")

⚠️ 用空接口 = 放弃类型安全。优先用具体接口或泛型,少用 any

6.4.2 类型断言

把接口类型转回具体类型:

var i any = "hello"

s := i.(string)        // 不安全断言,失败 panic
s, ok := i.(string)    // 安全断言(推荐)

6.4.3 类型 switch

func describe(i any) {
    switch v := i.(type) {    // ← 只能在 switch 用 .(type)
    case int:
        fmt.Println("整数:", v)        // v 是 int
    case string:
        fmt.Println("字符串:", v)      // v 是 string
    default:
        fmt.Println("未知:", v)
    }
}

6.5 实战:多态的支付系统(呼应开篇)

开篇说的「结账代码不用改」,现在落地:

type PaymentMethod interface {
    Pay(amount float64) error
    Name() string
}

type Alipay struct{ Account string }
func (a Alipay) Pay(amount float64) error {
    fmt.Printf("支付宝 %s 支付 %.2f\n", a.Account, amount)
    return nil
}
func (a Alipay) Name() string { return "支付宝" }

type WeChat struct{ OpenID string }
func (w WeChat) Pay(amount float64) error {
    fmt.Printf("微信 %s 支付 %.2f\n", w.OpenID, amount)
    return nil
}
func (w WeChat) Name() string { return "微信" }

// 通用结账函数:只依赖接口,不依赖具体类型
func checkout(pm PaymentMethod, amount float64) {
    fmt.Printf("使用 %s 结账\n", pm.Name())
    pm.Pay(amount)
}

// 调用
checkout(Alipay{Account: "zs@xx.com"}, 100)
checkout(WeChat{OpenID: "wx_xxx"}, 200)

// 想加银联?写个新类型实现接口,checkout 一个字都不用改:
type UnionPay struct{}
func (UnionPay) Pay(amt float64) error { ... }
func (UnionPay) Name() string { return "银联" }

这就是接口的威力——开篇的痛点被彻底解决。


6.6 报错诊断小节

报错 1:cannot use xxx (type X) as type Interface in argument

func f(s Speaker) {}
type Cat struct{}
f(Cat{})   // ← 报错

原因:Cat 没有 Speak() 方法,不满足 Speaker 接口。
修复:给 Cat 实现接口要求的方法:func (c Cat) Speak() string { ... }

报错 2:指针方法 vs 值方法导致接口不匹配

type Speaker interface { Speak() string }
type User struct{}
func (u *User) Speak() string { ... }   // ← 指针接收者

var s Speaker
s = User{}        // ← 报错!User 不满足,只有 *User 满足

原因:接口方法是 *User 实现的,User(值)的方法集不含它。
修复s = &User{}(传指针)。

报错 3:panic: interface conversion: ... is not ...

var i any = "hi"
n := i.(int)   // ← panic

原因:不安全断言失败。
修复:用 n, ok := i.(int) 安全形式。

报错 4:接口判 nil 判错(最坑)

var p *User = nil
var i any = p
fmt.Println(i == nil)   // false!明明 p 是 nil

原因:接口内部有「类型+值」两个字段。i 的类型是 *User(非 nil),值是 nil,所以接口本身非 nil。
修复:函数返回 error 接口时,不要返回类型化的 nil,要么返回具体 error 值,要么显式 return nil


6.7 接口设计原则

  1. 小接口:标准库 io.Reader 只有一个方法,却驱动整个 IO 体系。接口越小越强大。
  2. 接口由消费者定义:用的人按需定义接口,而不是实现者预先声明。
  3. 不要为接口而接口:只有一个实现时不需要接口;有多个实现、或要 mock 测试时再抽象。

6.8 Node.js / PHP ↔ Go 对照表

概念 Node.js / PHP Go 备注
类的方法 class 内定义 struct 外用接收者绑定 Go 数据行为分离
this / $this 隐式 显式 receiver Go 必须
继承 extends 嵌套组合 Go 无继承
接口实现 implements 显式 隐式(鸭子类型) Go 特色
多态 通过继承/原型链 通过接口
任意类型 any(TS) any / interface{} Go 1.18+
类型判断 typeof / instanceof 类型断言 .(T)
抽象类 abstract class 接口 + 默认实现 Go 无 abstract