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 官方和社区强烈不推荐用 this 或 self。官方惯例是使用类型首字母的小写缩写。如果方法体内用不到这个实例,你甚至可以省略变量名,只写类型:
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 方法(动态分派)
好处:
- 解耦:定义接口的人不需要知道实现者是谁
- 能 retrofit:可以给已有的、甚至是标准库的类型赋予新接口
- 测试友好: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.Reader、io.Writer 就是这么定义的——文件、网络连接、缓冲区都实现它们。这就是 Go 强大的抽象能力。
6.4 空接口 any 与类型断言
6.4.1 空接口
Go 1.18 起 any 是 interface{} 的别名。空接口没有方法,所以所有类型都满足它——类似 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 接口设计原则
- 小接口:标准库
io.Reader只有一个方法,却驱动整个 IO 体系。接口越小越强大。 - 接口由消费者定义:用的人按需定义接口,而不是实现者预先声明。
- 不要为接口而接口:只有一个实现时不需要接口;有多个实现、或要 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 |