
方法
前面我们已经学习了 Go 的变量、常量、数据类型、输入输出、条件控制、切片、字符串、映射表、指针、结构体和函数。接下来开始学习 Go 语言中把行为绑定到类型上的机制:方法(method)。
很多初学者会把方法直接理解成"类里面的函数"。这个理解可以帮助入门,但 Go 的方法和传统面向对象语言中的成员函数有一个关键区别:Go 没有 class,方法通过接收者(receiver)绑定到一个已定义的类型上,类型是否实现接口,取决于它的方法集。
按照 Go 官方语言规范:Method declarations 的定义,方法就是带有接收者的函数。简单理解:
- 普通函数说明"这段逻辑接收什么参数";
- 方法额外说明"这段逻辑属于哪个类型";
- 接收者是值还是指针,决定了方法能否修改原对象,也会影响接口实现。【这是需要注意的!】
本章按照"官方定义 → 基本方法 → 值接收者和指针接收者 → 方法集 → 接口实现 → 方法值与方法表达式 → 嵌入和提升 → 泛型类型方法 → 自定义数据结构实现"的顺序展开,重点学习:
- 方法声明和接收者;
- 值接收者与指针接收者;
- 可寻址值的自动取地址;
- T 和 *T 的方法集;
- 方法集与接口的隐式实现;
- nil 指针接收者;
- 方法值和方法表达式;
- 结构体嵌入后的方法提升;
- 给切片、map 和泛型类型定义方法;
- 使用方法实现一个泛型栈。
本文代码在 go1.27.0 darwin/arm64 环境中实际编译运行。地址、内存大小和系统相关输出可能因环境不同而变化。
方法的基本声明
方法的声明形式和函数很像,只是函数名之前多了接收者:
Go
func (接收者 接收者类型) 方法名(参数列表) 返回值列表 {
// 方法体
}
例如,定义一个矩形类型,再给它添加计算面积的方法:
Go
package main
import "fmt"
type Rectangle struct {
Width float64
Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func main() {
rectangle := Rectangle{Width: 3, Height: 4}
fmt.Println("面积:", rectangle.Area())
}
运行结果:
面积: 12
这里的 r 是方法接收者,类型是 Rectangle。调用 rectangle.Area() 时,Go 会把 rectangle 作为接收者传给 Area 方法。
可以把它近似理解成下面这个普通函数:
Go
func area(r Rectangle) float64 {
return r.Width * r.Height
}
但方法比普通函数多了一个明确的类型归属。调用者看到 rectangle.Area(),马上就能知道这个行为属于 Rectangle。
方法只能绑定到已定义的本地类型
Go 允许给当前包中定义的类型添加方法,但不能给其他包已经定义的类型直接添加方法:
Go
type Celsius float64
func (c Celsius) Fahrenheit() float64 {
return float64(c)*9/5 + 32
}
Celsius 是当前代码定义的新类型,所以可以声明 Fahrenheit 方法。下面这种写法不允许:
Go
// func (s string) First() byte { ... } // 编译错误:不能给非本地类型 string 添加方法
同样,不能给来自其他包的类型添加方法:
Go
// func (t time.Time) YearText() string { ... } // 编译错误:不能给非本地类型添加方法
如果确实需要为外部类型增加行为,可以定义一个新的本地类型或使用组合:
Go
type MyTime time.Time
不过新类型不会自动继承原类型的方法。Go 官方规范明确指出:定义一个新类型只会得到新的类型身份,不会把原类型的方法直接复制过来。
给非结构体类型定义方法
方法接收者不要求必须是结构体。只要接收者是当前包定义的非指针、非接口类型,就可以绑定方法:
Go
package main
import "fmt"
type Celsius float64
func (c Celsius) Fahrenheit() float64 {
return float64(c)*9/5 + 32
}
func (c Celsius) String() string {
return fmt.Sprintf("%.1f°C", c)
}
func main() {
temperature := Celsius(25)
fmt.Println(temperature)
fmt.Println("华氏温度:", temperature.Fahrenheit())
}
运行结果:
25.0°C
华氏温度: 77
这里的 Celsius 底层类型是 float64,但它仍然是一个独立的已定义类型,可以拥有自己的方法。这个能力常用于给数字、字符串、切片等类型补充符合业务语义的行为。
值接收者
值接收者的写法是:
Go
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
调用方法时,接收者会以值的形式传入,方法拿到的是一份副本。如果方法只读取数据,值接收者通常很合适:
Go
package main
import "fmt"
type Counter struct {
Value int
}
func (c Counter) Add(value int) {
c.Value += value
}
func main() {
counter := Counter{Value: 10}
counter.Add(5)
fmt.Println(counter.Value)
}
运行结果:
10
Add 方法内部确实执行了 c.Value += value,但 c 是 Counter 的副本,所以原来的 counter.Value 没有改变。
值接收者并不是"不能修改任何东西"。它可以修改副本中的字段;只是这些修改会随着方法返回而丢失。如果结构体中包含切片、map 或指针字段,值接收者复制的是这些字段的描述符或指针,底层数据仍然可能被修改:
Go
type Report struct {
Tags []string
}
func (r Report) RenameFirstTag(name string) {
r.Tags[0] = name
}
这里 r 本身是副本,但 r.Tags 可能和原对象共享底层数组,因此修改元素仍然可能影响调用者。这种行为要特别小心。
指针接收者
指针接收者的写法是:
Go
func (c *Counter) Add(value int) {
c.Value += value
}
指针接收者拿到的是对象地址,可以修改原对象,也可以避免复制较大的结构体:
Go
package main
import "fmt"
type Counter struct {
Value int
}
func (c *Counter) Add(value int) {
c.Value += value
}
func main() {
counter := Counter{Value: 10}
counter.Add(5)
fmt.Println(counter.Value)
}
运行结果:
15
调用 counter.Add(5) 时,counter 是一个可寻址变量。Go 会自动把这次调用转换成:
(&counter).Add(5)
因此,下面两种调用等价:
Go
counter.Add(5)
(&counter).Add(5)
这只是方法调用时的便利规则。普通函数不会自动取地址:
Go
func add(counter *Counter, value int) {
counter.Value += value
}
add(&counter, 5) // 正确
// add(counter, 5) // 编译错误
指针接收者的选择理由
下面几种情况通常应该使用指针接收者:
- 方法需要修改接收者;
- 结构体较大,不希望每次调用都复制;
- 类型中包含互斥锁、不能复制的状态或其他不适合复制的字段;
- 这个类型的大部分方法已经使用指针接收者,希望保持方法集和调用方式一致。
如果一个类型的所有方法都只读取数据,并且类型很小,值接收者会更直观。实际项目中,接收者选择应该保持一致,不要让同一个类型的方法一半使用值、一半使用指针,却没有明确理由。
接收者为 nil 的方法
指针接收者可能是 nil。方法调用本身不一定立即 panic,是否 panic 取决于方法体有没有访问 nil 指针指向的字段:
Go
package main
import "fmt"
type Logger struct {
Prefix string
}
func (l *Logger) Name() string {
if l == nil {
return "<nil logger>"
}
return l.Prefix
}
func (l *Logger) Print(message string) {
fmt.Println(l.Prefix + message)
}
func main() {
var logger *Logger
fmt.Println(logger.Name())
// logger.Print("hello") // panic:方法体访问了 nil 接收者的字段
}
运行结果:
<nil logger>
允许 nil 接收者有时很有用,例如链表节点、树节点或者"空对象"语义。但如果方法不支持 nil,就应该在文档中明确说明,或者在方法内部主动检查并返回错误。
方法集
方法集是学习 Go 方法时最容易混淆、也最重要的概念之一。按照 Go 官方语言规范:Method sets:
- 已定义类型 T 的方法集包含所有接收者为 T 的方法;
- 类型 *T 的方法集包含接收者为 T 和 *T 的方法;
- 接口的方法集由接口声明的方法组成,并由满足接口类型集合的交集决定。
可以用下面的表格记忆:
| 类型 | 方法集包含 |
|---|---|
| T | 接收者为 T 的方法 |
| *T | 接收者为 T 和 *T 的方法 |
| interface | 接口声明的方法 |
例如:
cpp
type Counter struct {
Value int
}
func (c Counter) ValueOf() int {
return c.Value
}
func (c *Counter) Add(value int) {
c.Value += value
}
那么:
- Counter 的方法集只有 ValueOf;
- *Counter 的方法集包含 ValueOf 和 Add。
**这也是为什么指针类型比值类型"拥有更多方法"。**方法调用时,编译器可以在可寻址值上自动取地址;但把一个值放进接口时,不会为了满足接口而偷偷创建一个新的指针。
方法集与接口实现
Go 的接口是隐式实现的。一个类型只要拥有接口要求的全部方法,就实现了这个接口,不需要显式写 implements。
cpp
package main
import "fmt"
type Speaker interface {
Speak() string
}
type Dog struct {
Name string
}
func (d Dog) Speak() string {
return d.Name + ": wang"
}
type Robot struct {
Name string
}
func (r *Robot) Speak() string {
return r.Name + ": beep"
}
func announce(speaker Speaker) {
fmt.Println(speaker.Speak())
}
func main() {
dog := Dog{Name: "旺财"}
robot := Robot{Name: "R2"}
announce(dog)
announce(&robot)
}
运行结果:
旺财: wang
R2: beep
Dog 的 Speak 使用值接收者,所以 Dog 和 *Dog 都实现 Speaker。Robot 的 Speak 使用指针接收者,所以只有 *Robot 实现 Speaker,Robot 本身不能直接传给 announce。
可以用编译期断言把这种关系写得更明确:
Go
var _ Speaker = Dog{}
var _ Speaker = (*Robot)(nil)
如果以后修改了方法签名,编译器会在这里立刻报错,而不是等到某个业务调用路径才发现问题。
值和指针赋给接口的差异
下面的代码不能编译:
Go
var robot Robot
// var speaker Speaker = robot // 编译错误
var speaker Speaker = &robot // 正确
原因不是 Robot 没有 Speak 方法,而是 Robot 的方法集不包含指针接收者方法。对于一个可寻址的普通变量,robot.Speak() 这样的调用可以自动取地址;但接口保存的是一个已经确定的值,编译器不能再替你改变它的动态类型。【核心注意点】
接口中的值接收者副本
值接收者方法会接收接口中保存值的副本:
Go
package main
import "fmt"
type Counter struct {
Value int
}
type Incrementer interface {
Increment()
}
func (c Counter) Increment() {
c.Value++
}
func main() {
counter := Counter{Value: 10}
var incrementer Incrementer = counter
incrementer.Increment()
fmt.Println(counter.Value)
}
运行结果:
10
如果希望通过接口修改原对象,接口中保存的应该是指针,并且方法使用指针接收者:
Go
func (c *Counter) Increment() {
c.Value++
}
counter := Counter{Value: 10}
var incrementer Incrementer = &counter
incrementer.Increment()
fmt.Println(counter.Value) // 11
方法值和方法表达式
方法还可以像函数一样被保存和传递。
方法值
如果写成 object.Method,得到的是方法值。接收者已经绑定在这个函数值里,后续调用时不需要再次传入接收者:
Go
package main
import "fmt"
type Counter struct {
Value int
}
func (c *Counter) Add(value int) {
c.Value += value
}
func main() {
counter := Counter{Value: 10}
add := counter.Add
add(5)
fmt.Println(counter.Value)
}
运行结果:
15
add 保存了 counter 这个接收者。由于 Add 是指针接收者方法,add 调用时仍然会修改原来的 counter。
值接收者的方法值会保存接收者的一份副本:
Go
package main
import "fmt"
type Counter struct {
Value int
}
func (c Counter) ValueOf() int {
return c.Value
}
func main() {
counter := Counter{Value: 10}
valueOf := counter.ValueOf
counter.Value = 20
fmt.Println(valueOf())
}
运行结果:
10
方法值创建时,值接收者已经被求值并保存,所以之后修改 counter 不会影响 valueOf 保存的副本。
方法表达式
方法表达式把接收者变成普通函数的第一个参数:
Go
package main
import "fmt"
type Counter struct {
Value int
}
func (c *Counter) Add(value int) {
c.Value += value
}
func main() {
counter := Counter{Value: 10}
add := (*Counter).Add
add(&counter, 5)
fmt.Println(counter.Value)
}
运行结果:
15
(*Counter).Add 的函数类型相当于 func(*Counter, int)。方法表达式适合把方法传给只接收普通函数的 API,也能帮助我们理解方法的本质:方法只是带有接收者参数的函数。
结构体嵌入和方法提升
结构体可以嵌入另一个类型。嵌入后,内部类型的方法可能会被外层类型提升:
Go
package main
import "fmt"
type Logger struct {
Prefix string
}
func (l Logger) Print(message string) {
fmt.Println(l.Prefix + message)
}
type Service struct {
Logger
Name string
}
func main() {
service := Service{
Logger: Logger{Prefix: "[API] "},
Name: "user",
}
service.Print("启动")
service.Logger.Print("直接调用")
}
运行结果:
[API] 启动
[API] 直接调用
service.Print("启动") 实际上使用了嵌入字段 Logger 的 Print 方法。这个方法提升让组合对象可以复用内部类型的行为。
但嵌入不是继承:
- Service 不是 Logger 的子类型;
- Service 不会自动拥有 Logger 的类型身份;
- 提升方法的接收者仍然是内部 Logger;
- 如果外层定义了同名方法,外层方法会优先。
Go
type Service struct {
Logger
}
func (s Service) Print(message string) {
fmt.Println("[Service]", message)
}
这时调用 service.Print() 会选择 Service 自己的 Print。若需要调用内部方法,可以显式写 service.Logger.Print()。
给切片和 map 定义方法
方法不只适用于结构体。为切片定义一个命名类型,就可以给它添加面向业务的操作:
Go
package main
import "fmt"
type Scores []int
func (scores Scores) Total() int {
total := 0
for _, score := range scores {
total += score
}
return total
}
func (scores Scores) Average() float64 {
if len(scores) == 0 {
return 0
}
return float64(scores.Total()) / float64(len(scores))
}
func (scores *Scores) Add(score int) {
*scores = append(*scores, score)
}
func main() {
scores := Scores{80, 90}
scores.Add(100)
fmt.Println("成绩:", scores)
fmt.Println("总分:", scores.Total())
fmt.Println("平均分:", scores.Average())
}
运行结果:
成绩: [80 90 100]
总分: 270
平均分: 90
Scores 的 Total 和 Average 使用值接收者,因为它们只读取数据;Add 使用指针接收者,因为 append 可能改变切片的长度和底层数组,需要把新的切片描述符写回原变量。
给 map 定义方法也很常见:
Go
type Headers map[string]string
func (headers Headers) Set(key, value string) {
headers[key] = value
}
func (headers Headers) Get(key string) (string, bool) {
value, ok := headers[key]
return value, ok
}
这里 Set 可以使用值接收者,因为 map 值本身包含对底层哈希表的引用。修改 headerskey 会影响同一个底层 map。只有在方法需要让 map 变量本身指向另一个 map 时,才需要返回新的 map 或使用指针。
泛型类型的方法
Go 支持给泛型类型定义方法。方法的接收者需要声明对应的类型参数:
Go
package main
import "fmt"
type Box[T any] struct {
value T
}
func (box Box[T]) Get() T {
return box.value
}
func (box *Box[T]) Set(value T) {
box.value = value
}
func main() {
box := Box[int]{}
box.Set(42)
fmt.Println(box.Get())
textBox := Box[string]{value: "Go"}
fmt.Println(textBox.Get())
}
运行结果:
42
Go
Boxint 和 Boxstring 是两个不同的实例化类型,但它们共享同一套方法定义。Get 使用值接收者读取值,Set 使用指针接收者修改值。
方法上的类型参数来自接收者的泛型类型定义。方法不能像普通泛型函数那样随意增加一组独立的类型参数;需要跨类型转换时,通常把逻辑写成普通泛型函数,或者重新设计泛型类型。
使用方法实现一个泛型栈
接下来实现一个完整的数据结构:泛型栈(Stack)。
栈遵循后进先出(LIFO)规则:
Go
Push(10) → Push(20) → Push(30)
栈顶
│
▼
+----+
| 30 |
+----+
| 20 |
+----+
| 10 |
+----+
我们用切片保存元素,再通过方法限制外部操作。栈的零值应该直接可用,因此不需要额外的构造函数。
定义栈类型和 Push 方法
Go
type Stack[T any] struct {
items []T
}
func (stack *Stack[T]) Push(value T) {
stack.items = append(stack.items, value)
}
items 使用未导出字段,外部代码不能直接改变栈内部顺序。Push 使用指针接收者,因为 append 可能改变切片长度和容量。
实现 Pop、Peek 和 Len
Go
func (stack *Stack[T]) Pop() (T, bool) {
if len(stack.items) == 0 {
var zero T
return zero, false
}
last := len(stack.items) - 1
value := stack.items[last]
var zero T
stack.items[last] = zero
stack.items = stack.items[:last]
return value, true
}
func (stack *Stack[T]) Peek() (T, bool) {
if len(stack.items) == 0 {
var zero T
return zero, false
}
return stack.items[len(stack.items)-1], true
}
func (stack Stack[T]) Len() int {
return len(stack.items)
}
func (stack Stack[T]) IsEmpty() bool {
return len(stack.items) == 0
}
Pop 删除元素后把最后一个位置写成 T 的零值。对于元素是指针、切片或 map 的情况,这一步可以及时清理引用,减少底层数组继续持有对象的时间。
完整测试代码
Go
package main
import "fmt"
type Stack[T any] struct {
items []T
}
func (stack *Stack[T]) Push(value T) {
stack.items = append(stack.items, value)
}
func (stack *Stack[T]) Pop() (T, bool) {
if len(stack.items) == 0 {
var zero T
return zero, false
}
last := len(stack.items) - 1
value := stack.items[last]
var zero T
stack.items[last] = zero
stack.items = stack.items[:last]
return value, true
}
func (stack *Stack[T]) Peek() (T, bool) {
if len(stack.items) == 0 {
var zero T
return zero, false
}
return stack.items[len(stack.items)-1], true
}
func (stack Stack[T]) Len() int {
return len(stack.items)
}
func (stack Stack[T]) IsEmpty() bool {
return len(stack.items) == 0
}
func main() {
var stack Stack[int]
fmt.Println("初始为空:", stack.IsEmpty())
stack.Push(10)
stack.Push(20)
stack.Push(30)
top, _ := stack.Peek()
fmt.Println("栈顶:", top)
fmt.Println("长度:", stack.Len())
value, ok := stack.Pop()
fmt.Println("弹出:", value, ok)
fmt.Println("弹出后栈顶:", mustPeek(&stack))
fmt.Println("长度:", stack.Len())
for !stack.IsEmpty() {
value, _ := stack.Pop()
fmt.Println("继续弹出:", value)
}
fmt.Println("最终为空:", stack.IsEmpty())
}
func mustPeek(stack *Stack[int]) int {
value, ok := stack.Peek()
if !ok {
panic("stack is empty")
}
return value
}
运行结果:
Go
初始为空: true
栈顶: 30
长度: 3
弹出: 30 true
弹出后栈顶: 20
长度: 2
继续弹出: 20
继续弹出: 10
最终为空: true
这个实现体现了方法的几个关键作用:
- Push、Pop、Peek 和 Len 都围绕 Stack 类型组织;
- 外部代码只能通过方法操作 items;
- 指针接收者负责修改栈,值接收者负责只读查询;
- 空栈通过零值和第二个 bool 返回值表达;
- Stackint 可以直接替换成 Stackstring 或其他类型。
栈的时间复杂度
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| Push | 摊销 O(1) | 切片尾部追加,扩容时会复制 |
| Pop | O(1) | 读取并删除最后一个元素 |
| Peek | O(1) | 读取最后一个元素 |
| Len | O(1) | 直接读取切片长度 |
| IsEmpty | O(1) | 判断长度是否为零 |
栈的实现并不复杂,真正值得理解的是:数据结构的状态放在类型内部,修改状态的操作使用指针接收者,查询状态的操作使用值接收者。这样可以让方法名直接表达业务动作,同时把内部表示隐藏起来。
方法命名和标准接口
Go 的方法名通常使用简洁的动词或名词,不使用 Get、Do 这类没有额外含义的前缀。例如 Effective Go 建议,如果类型有一个 owner 字段,对外读取它的方法可以命名为 Owner,而不是 GetOwner。
标准库中有一些方法名已经形成约定:
- String() string:实现 fmt.Stringer;
- Error() string:实现 error;
- Read、Write、Close:通常表示 io.Reader、io.Writer、io.Closer 的行为;
- Len、Less、Swap:用于实现 sort.Interface。
下面实现 String 方法:
Go
package main
import "fmt"
type Point struct {
X int
Y int
}
func (point Point) String() string {
return fmt.Sprintf("(%d, %d)", point.X, point.Y)
}
func main() {
point := Point{X: 3, Y: 4}
fmt.Println(point)
}
运行结果:
(3, 4)
fmt.Println 发现 Point 实现了 String() string,就会调用这个方法得到文本表示。方法名和签名需要遵循标准接口约定,不能随意写成 ToString,否则标准库不会自动识别。
常见错误和避坑提醒
误区一:把方法当成继承
方法只绑定到接收者类型,结构体嵌入带来的是组合和方法提升,不会产生父子类型关系。需要抽象行为时,应该使用接口,而不是依赖嵌入模拟继承。
误区二:值接收者修改了副本
如果方法要修改原对象,应使用指针接收者。看到方法体中写了字段,并不代表调用者的对象会被修改。
误区三:认为值能自动满足指针方法接口
可寻址变量调用 pointer method 时,Go 可以自动取地址;但值赋给接口时不会自动转成指针。接口赋值前必须检查 T 和 *T 的方法集。
误区四:给非本地类型添加方法
不能给 string、time.Time 或其他包里的类型添加方法。定义一个本地新类型后,原类型的方法也不会自动继承。
误区五:复制包含锁的结构体
如果结构体内部包含 sync.Mutex 等不能复制的状态,值接收者可能产生隐蔽问题。此类类型通常应该使用指针接收者,并避免复制整个结构体。
误区六:忽略 nil 接收者
指针方法可能在 nil 指针上被调用。方法要么主动支持 nil,要么在契约中明确不允许 nil,并避免访问 nil 指针的字段。
误区七:用指针接收者包装每一个切片方法
切片元素修改通常不需要 *\[\]T。只有在需要改变切片长度、容量或让切片变量指向新底层数组时,才需要指针接收者或返回新切片。
误区八:方法名不符合标准接口
如果想实现 fmt.Stringer、error、io.Writer 等接口,方法名和签名必须完全匹配。String、Error、Write 的大小写和返回值都不能随意修改。
一份可以直接复制的综合示例
下面的例子把方法、接口、指针接收者和方法值放在一起,模拟一个可统计的计数器:
Go
package main
import "fmt"
type Counter struct {
value int
}
type Metric interface {
Add(int)
Value() int
}
func (counter *Counter) Add(delta int) {
counter.value += delta
}
func (counter Counter) Value() int {
return counter.value
}
func printMetric(metric Metric) {
fmt.Println("当前值:", metric.Value())
}
func main() {
counter := Counter{}
var metric Metric = &counter
metric.Add(10)
metric.Add(5)
printMetric(metric)
add := counter.Add
add(20)
fmt.Println("最终值:", counter.Value())
}
运行结果:
当前值: 15
最终值: 35
Counter 的 Add 使用指针接收者,所以接口中保存的是 *Counter;Value 使用值接收者,因此 *Counter 同时拥有 Value 方法。最后把 counter.Add 保存成方法值 add,调用 add(20) 仍然修改原来的计数器。
总结
本章我们学习了 Go 方法的完整基础:
- 方法是带接收者的函数;
- 方法只能绑定到当前包定义的非指针、非接口类型;
- 值接收者拿到副本,适合只读操作;
- 指针接收者拿到地址,可以修改原对象并避免复制;
- 可寻址值调用指针方法时,Go 会自动取地址;
- T 的方法集只包含 T 接收者方法;
- *T 的方法集包含 T 和 *T 接收者方法;
- 接口实现由方法集决定,不需要显式声明;
- 值可以调用指针方法,不代表这个值一定满足接口;
- 方法值会保存接收者,方法表达式会把接收者变成第一个参数;
- 结构体嵌入可以提升方法,但不等同于继承;
- 切片、map 和泛型类型同样可以拥有方法;
- 方法可以把数据结构的内部状态和操作封装在一起;
- 方法名和签名遵循标准约定,才能正确实现 fmt.Stringer、error、io.Writer 等接口。
真正理解 Go 方法之后,就不会把它简单看成"结构体里的函数"。更准确的理解是:方法把行为绑定到类型,接收者决定状态如何传递,方法集决定接口是否成立,而清晰的方法边界又让复杂数据结构能够被安全地使用。
官方资料: