Go 中 type、方法与指针接收者:从 str_name.Name() 看懂 Go 的类型系统 很多 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自定义类型structmethodreceiverpointer receivermethod setinterface我们一个一个拆开。二、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 100UserID是一个独立的命名类型。它虽然底层是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”Javaclass User implements NamerGotype 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/setterGo 程序员经常看到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 ↓ GoldenRetrieverGo 更倾向于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() float646. 接口描述的是行为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 代码本质上就是在不断回答一个问题这个行为究竟应该属于哪个类型