很多 Go 程序员都会写:
type User struct { Name string } func (u *User) GetName() string { return u.Name }但如果问一句:
为什么方法前面要写
(u *User)?为什么方法不是写在 struct 里面?
为什么有时候用
User,有时候用*User?为什么一个方法突然就决定了一个类型是否实现某个接口?
真正理解这些问题,你才算真正理解了 Go 的方法系统。
本文就从一个看起来非常简单的例子开始。
一、先看最简单的例子
假设我们定义一个类型:
type str_name struct {
name string
}
然后给它增加一个方法:
func (h *str_name) Name() string {
return h.name
}
调用:
s := &str_name{name: "Tom"}
fmt.Println(s.Name())
输出:
Tom
初学者通常会把它理解成:
str_name有一个Name()方法。
这个理解没有错。
但还不够。
实际上,这两行代码背后包含了 Go 类型系统中非常重要的几个概念:
type str_name struct {
name string
}
func (h *str_name) Name() string {
return h.name
}
分别涉及:
-
type -
自定义类型
-
struct
-
method
-
receiver
-
pointer receiver
-
method set
-
interface
我们一个一个拆开。
二、type str_name struct 到底是什么?
先看:
type str_name struct {
name string
}
可以把它粗略理解成:
创建了一个叫
str_name的新类型。
例如:
var s str_name
此时:
s
│
└── 类型:str_name
它拥有一个字段:
s.name
类型是:
string
所以:
s := str_name{
name: "Tom",
}
就创建了一个 str_name 类型的值。
三、为什么 Go 要有 type?
这是理解 Go 很关键的一步。
很多人会觉得:
type User struct {
Name string
}
就是"定义一个结构体"。
实际上更准确的说法是:
type是在定义一个新的类型。
struct 只是这个类型的底层结构之一。
例如:
type User struct {
Name string
}
定义了一个 User 类型。
但是 Go 不仅可以基于 struct 创建类型。
还可以:
type UserID int
type Username string
type Money int64
type Status int
例如:
type UserID int64
此时:
var id UserID = 100
UserID 是一个独立的命名类型。
它虽然底层是 int64,但语义上是一个新的类型。
这就是 Go 一个非常重要的设计:
用类型表达业务语义。
四、真正重要的事情来了:方法
现在:
type str_name struct {
name string
}
只有数据。
我们可以:
s := str_name{
name: "Tom",
}
但是如果我们希望:
s.Name()
怎么办?
于是我们定义:
func (h *str_name) Name() string {
return h.name
}
这就是 Go 的 method(方法)。
注意:
Go 的方法并不是这样定义:
type str_name struct {
name string
func Name() string {
...
}
}
这是错误的。
Go 的设计是:
type str_name struct {
name string
}
func (h *str_name) Name() string {
return h.name
}
也就是说:
数据定义和行为定义是分开的。
这和 Java、C#、Kotlin 这种语言的 class 写法有明显区别。
五、func (h *str_name) 是什么鬼?
这是本文真正的核心。
看:
func (h *str_name) Name() string {
return h.name
}
这里:
(h *str_name)
叫做:
receiver(接收者)
所以:
func (h *str_name) Name()
可以理解成:
给
*str_name这个类型定义一个叫Name的方法。
其中:
h
是接收者变量。
而:
*str_name
是接收者类型。
于是:
h.name
实际上就是:
从当前
str_name对象中读取name字段。
六、为什么叫 Receiver?
因为:
s.Name()
实际上可以理解成:
Name(s)
只不过 Go 把调用方式设计成:
s.Name()
而方法内部:
func (h *str_name) Name() string {
return h.name
}
这里的:
h
就是调用这个方法时传进来的那个对象。
所以:
s.Name()
可以脑补成:
s
│
│ 调用 Name()
▼
h
│
▼
h.name
这就是 receiver。
七、那么为什么是 *str_name?
这又是一个非常重要的问题。
我们可以写:
func (h str_name) Name() string {
return h.name
}
也可以写:
func (h *str_name) Name() string {
return h.name
}
两者都合法。
区别在于:
h str_name
是:
值接收者(value receiver)
而:
h *str_name
是:
指针接收者(pointer receiver)
八、值接收者到底意味着什么?
例如:
type User struct {
Name string
}
func (u User) GetName() string {
return u.Name
}
调用:
u := User{
Name: "Tom",
}
fmt.Println(u.GetName())
没有问题。
但是这里的 u 是一个值。
调用方法的时候,相当于把这个值传给方法。
可以粗略理解为:
u
│
│ copy
▼
method receiver
也就是说:
值接收者天然带有"复制"的语义。
九、指针接收者又是什么?
改成:
func (u *User) GetName() string {
return u.Name
}
此时 receiver 是:
*User
也就是:
User 的指针。
例如:
u := User{
Name: "Tom",
}
fmt.Println(u.GetName())
这里看起来非常神奇。
明明 u 是:
User
为什么可以调用:
u.GetName()
而方法要求:
*User
因为 Go 会在适当情况下自动取地址。
可以近似理解成:
(&u).GetName()
所以 Go 允许你写:
u.GetName()
而不是强制:
(&u).GetName()
这就是 Go 很舒服的地方。
十、什么时候必须使用指针接收者?
最重要的场景:
方法需要修改对象本身。
例如:
type Counter struct {
value int
}
func (c *Counter) Increment() {
c.value++
}
使用:
c := Counter{}
c.Increment()
c.Increment()
fmt.Println(c.value)
结果:
2
为什么?
因为:
c *Counter
拿到的是对象地址。
所以:
c.value++
修改的是原来的对象。
十一、如果使用值接收者呢?
如果写成:
func (c Counter) Increment() {
c.value++
}
那么:
c := Counter{}
c.Increment()
fmt.Println(c.value)
结果仍然是:
0
因为:
原对象
│
│ copy
▼
方法里的 c
│
▼
修改副本
原来的 c 没有变化。
所以一个非常重要的经验:
需要修改 receiver 的状态,使用指针接收者。
十二、但是 Name() 明明不修改对象,为什么还可以用 *str_name?
这就是很多 Go 项目里最容易让人困惑的地方。
例如:
type User struct {
name string
}
func (u *User) Name() string {
return u.name
}
这里实际上没有修改:
u
所以从纯技术角度来说:
func (u User) Name() string
也是完全可以的。
那么为什么工程代码里大量使用:
func (u *User) Name()
?
原因主要有三个。
十三、第一个原因:保持 receiver 一致
例如:
type User struct {
ID int64
Name string
}
可能有很多方法:
func (u *User) Name() string {
return u.Name
}
func (u *User) UpdateName(name string) {
u.Name = name
}
func (u *User) Save() error {
...
}
既然这个类型存在大量需要修改状态的方法,那么通常会统一使用:
*User
作为 receiver。
这会让整个类型的 API 更一致。
十四、第二个原因:避免复制大型结构体
假设:
type User struct {
ID int64
Name string
Profile [1024]byte
Settings [4096]byte
}
如果:
func (u User) Name() string
每次调用理论上都存在 receiver 值复制的语义。
而:
func (u *User) Name() string
传递的是指针。
对于大型结构体来说,通常更合理。
当然,不要把它理解成:
"所有方法都必须用指针,否则性能就差。"
不是。
现代 Go 编译器会做大量优化。
应该首先考虑语义和 API 设计 ,而不是看到 struct 就机械地使用指针。
十五、第三个原因:Method Set
这才是最关键的。
Go 的接口系统依赖:
方法集(Method Set)
假设:
type User struct {
name string
}
func (u *User) Name() string {
return u.name
}
那么:
*User
拥有:
Name()
这个方法。
而:
User
的方法集和:
*User
并不是完全一样。
这是 Go 接口系统非常核心的规则。
十六、一个接口就能看懂 Method Set
定义:
type Namer interface {
Name() string
}
然后:
type User struct {
name string
}
func (u *User) Name() string {
return u.name
}
那么:
var n Namer
u := User{name: "Tom"}
n = &u
没问题。
因为:
*User
│
└── Name()
│
▼
Namer
所以:
*User
实现了:
Namer
十七、但是 User 呢?
注意:
n = u
通常会报错。
原因就是:
Name()
定义在:
*User
上。
不是:
User
上。
因此:
User
└── 不满足 Namer
*User
└── 满足 Namer
这就是:
指针接收者会影响接口实现。
这是 Go 开发中非常重要的知识。
十八、如果改成值接收者呢?
改成:
func (u User) Name() string {
return u.name
}
那么情况就不同了:
User
└── Name()
*User
└── Name()
因此:
var n Namer
n = u
可以。
同时:
n = &u
也可以。
所以可以记住一个非常实用的规律:
值接收者方法
│
├── User 可以调用
└── *User 也可以调用
指针接收者方法
│
└── *User 才属于这个方法集
十九、这就是为什么 Go 的接口看起来"没有 implements"
Java:
class User implements Namer
Go:
type Namer interface {
Name() string
}
然后:
type User struct {
name string
}
func (u *User) Name() string {
return u.name
}
没有:
implements
没有:
extends
甚至没有:
implements Namer
但只要方法满足:
Name() string
它就自动实现:
Namer
这叫:
Structural Typing(结构化类型)
也是 Go 接口设计最漂亮的地方之一。
二十、str_name 这个例子真正厉害的地方
回头看最开始:
type str_name struct {
name string
}
func (h *str_name) Name() string {
return h.name
}
它其实已经体现了 Go 的一个非常强大的设计思想:
str_name
│
┌───────┴────────┐
│ │
数据 行为
│ │
name string Name()
Go 并没有把:
数据
行为
强行塞进一个 class 概念里。
而是:
type str_name struct {
name string
}
负责定义数据。
然后:
func (h *str_name) Name() string
负责定义行为。
这让 Go 的类型系统非常轻。
二十一、再往前一步:方法不一定只返回字段
例如:
type User struct {
firstName string
lastName string
}
func (u *User) Name() string {
return u.firstName + " " + u.lastName
}
这里:
Name()
并不是简单 getter。
它实际上表达了一个业务行为:
返回用户的完整名称。
这比:
u.firstName + " " + u.lastName
散落在项目各处要好得多。
因此,在优秀的 Go 代码里:
方法不是为了给字段机械地加一个 getter。
而应该承担类型本身的行为。
二十二、Go 为什么没有强制 getter/setter?
Go 程序员经常看到:
func (u *User) Name() string {
return u.name
}
然后会问:
这不就是 Java 的 getName() 吗?
从形式上看确实类似。
但 Go 的习惯通常不会写:
GetName()
而是:
Name()
例如:
func (u *User) Name() string
func (u *User) ID() int64
func (u *User) Status() Status
因为 Go 更倾向于:
方法名称表达"获取什么",而不是机械表达"get"。
所以:
user.Name()
比:
user.GetName()
更符合 Go 风格。
二十三、再看一个更真实的例子
例如一个订单:
type Order struct {
ID int64
Amount int64
Status string
}
可以定义:
func (o *Order) IsPaid() bool {
return o.Status == "paid"
}
调用:
if order.IsPaid() {
...
}
这比:
if order.Status == "paid" {
...
}
更有价值。
因为:
业务规则
被封装进:
IsPaid()
以后状态判断可能变复杂:
func (o *Order) IsPaid() bool {
return o.Status == "paid" ||
o.Status == "completed"
}
调用方完全不用修改。
这就是方法真正的价值。
二十四、方法本质上是"类型的行为"
所以我们可以建立一个更成熟的认识:
type Order struct {
...
}
定义:
Order 是什么。
而:
func (o *Order) IsPaid() bool
func (o *Order) Cancel() error
func (o *Order) Pay() error
定义:
Order 能做什么。
于是:
Type = 数据 + 行为
虽然 Go 没有传统 class 那种语法,但从工程建模角度:
一个 Go 类型完全可以拥有非常丰富的行为。
二十五、那么 receiver 到底应该用值还是指针?
不要死记。
可以按照下面的原则判断。
第一原则:需要修改对象
使用:
*Type
例如:
func (u *User) Rename(name string) {
u.Name = name
}
第二原则:结构体比较大
通常考虑:
*Type
避免不必要的复制。
第三原则:类型内部包含不能复制的状态
例如:
sync.Mutex
这类类型不能随便复制。
通常应该使用指针接收者。
第四原则:希望整个类型的方法保持一致
如果:
User
的大多数方法都是:
*User
那么新增方法通常也使用:
*User
不要在同一个类型里随意混用。
第五原则:小型、不可变、值语义类型
可以考虑值接收者。
例如:
type Point struct {
X int
Y int
}
func (p Point) Distance() float64 {
...
}
这种类型天然具有值语义。
值接收者非常合理。
二十六、一个非常重要的工程经验
不要把:
*User
理解成:
"性能更好。"
也不要把:
User
理解成:
"更简单。"
真正应该思考的是:
这个类型到底具有什么语义?
如果:
User
代表一个具有身份、状态、生命周期的实体:
type User struct {
ID int64
Name string
Status string
}
通常:
*User
更自然。
因为你实际上操作的是:
同一个 User 对象。
而不是不断复制 User。
二十七、一个更高级的例子:接口 + 指针接收者
现在我们定义:
type Namer interface {
Name() string
}
然后:
type User struct {
name string
}
func (u *User) Name() string {
return u.name
}
我们可以:
func PrintName(n Namer) {
fmt.Println(n.Name())
}
调用:
u := User{name: "Tom"}
PrintName(&u)
这就形成了一个非常典型的 Go 结构:
Namer
│
Name()
▲
│
*User
│
User data
调用方只关心:
Name()
完全不需要知道:
User
内部怎么实现。
这就是 Go 接口的真正价值:
依赖行为,而不是依赖具体类型。
二十八、为什么 Go 项目里大量出现这种代码?
你以后看 Go 项目,很可能看到:
type UserRepository interface {
FindByID(id int64) (*User, error)
Save(user *User) error
}
然后:
type userRepository struct {
db *sql.DB
}
再:
func (r *userRepository) FindByID(id int64) (*User, error) {
...
}
注意:
userRepository
甚至可能是小写的。
但它通过方法:
FindByID()
Save()
实现了:
UserRepository
接口。
这就是 Go 非常典型的:
小类型 + 方法 + 接口组合
而不是:
巨大的 class hierarchy。
二十九、再看 Go 的嵌入
理解 receiver 之后,再看:
type Base struct {
Name string
}
func (b *Base) GetName() string {
return b.Name
}
然后:
type User struct {
Base
}
此时:
u := User{
Base: Base{Name: "Tom"},
}
可以直接:
u.GetName()
虽然:
User
自己没有定义:
GetName()
但是通过嵌入:
Base
方法被提升了。
于是:
User
│
└── Base
│
└── GetName()
这也是 Go 组合优于继承的重要体现。
三十、Go 真正的高级玩法:组合
很多传统面向对象程序员喜欢:
Animal
↓
Mammal
↓
Dog
↓
GoldenRetriever
Go 更倾向于:
Dog
│
├── Name()
├── Move()
├── Bark()
└── ...
再通过:
type Animal interface {
Move()
}
抽象行为。
核心思想是:
不要为了复用代码而建立继承体系。
而是:
通过组合复用实现,通过接口抽象行为。
三十一、回到最开始的 str_name
现在我们重新看:
type str_name struct {
name string
}
func (h *str_name) Name() string {
return h.name
}
如果只是从语法层面理解:
str_name有一个Name()方法。
这是初级理解。
如果理解 receiver:
*str_name定义了Name()方法。
这是中级理解。
如果理解 method set:
*str_name的方法集中包含Name()。
这是更深入的理解。
如果理解 interface:
任何要求
Name() string的接口,都可以由*str_name实现。
这是接口层面的理解。
如果再理解类型设计:
Name()不只是 getter,而是str_name对外暴露的行为契约。
这才是工程层面的理解。
三十二、最后建立一张脑图
以后看到:
func (h *str_name) Name() string
不要再把它当成一行普通函数。
你应该在脑子里自动展开成:
func
│
├── 方法,而不是普通函数
│
└── receiver
│
▼
*str_name
│
├── 指针接收者
│
├── 方法属于 *str_name
│
├── 可以访问 str_name 的字段
│
├── 可以修改原对象
│
└── 参与 *str_name 的 method set
│
▼
interface
这条链路非常重要:
type
↓
自定义类型
↓
method
↓
receiver
↓
value receiver / pointer receiver
↓
method set
↓
interface
↓
组合
↓
Go 的类型设计
三十三、真正值得记住的,不是语法
如果只记住:
func (h *str_name) Name() string
那还不够。
真正应该记住的是下面这几句话。
1. type 是定义类型
type User struct {}
不是简单地"声明一个 struct"。
2. Go 的方法通过 receiver 绑定到类型
func (u *User) Name() string
这里的:
(u *User)
就是 receiver。
3. *User 和 User 的方法集不同
这是理解 Go interface 的关键。
4. 需要修改对象时,通常使用指针接收者
func (u *User) Rename(name string)
5. 小型值类型可以使用值接收者
func (p Point) Distance() float64
6. 接口描述的是行为
type Namer interface {
Name() string
}
它不关心你到底是:
User
Customer
Employee
Admin
只要:
Name() string
满足契约即可。
7. Go 的高级抽象不是继承,而是组合 + 接口
这是理解 Go 工程设计最重要的一步。
结语
Go 的语法其实非常少。
真正难的从来不是:
func (u *User) Name() string
怎么写。
真正难的是理解:
为什么这个方法属于
*User而不是User?
为什么接口不需要implements?
为什么一个 receiver 的选择,会影响接口实现?
什么时候应该使用指针?什么时候应该使用值?
为什么 Go 宁愿让类型通过方法表达行为,也不建立复杂的继承体系?
当你把:
type
+
method
+
receiver
+
method set
+
interface
+
composition
串起来以后,你会发现:
Go 的"面向对象"其实一直都在,只是它把 class、继承、implements 这些传统 OO 语言的复杂外壳全部拆掉了。
最后留下来的只有三个东西:
数据
行为
契约
也就是:
type User struct {
...
}
func (u *User) Name() string {
...
}
type Namer interface {
Name() string
}
这三个东西,基本就是 Go 类型系统的骨架。
而真正优秀的 Go 代码,本质上就是在不断回答一个问题:
这个行为,究竟应该属于哪个类型?