Skip to main content

第 5 章:指针与接口

本章目标​

  • 使用 & 获取变量地址,使用 * 解引用指针
  • 理解 nil 指针和使用前检查
  • 理解 Go 的函数参数始终按值传递
  • 使用指针修改调用者持有的数据
  • 定义接口并让具体类型隐式实现接口
  • 使用接口实现多态
  • 使用 any、类型断言和类型选择
  • 理解接口组合和值类型、指针类型的方法集

指针基础​

指针保存另一个变量的内存地址:

number := 10
pointer := &number

fmt.Println(pointer) // number 的地址
fmt.Println(*pointer) // 地址中保存的值:10

*pointer = 25
fmt.Println(number) // 25

同一个 * 在不同位置有两种含义:

number := 10
var pointer *int = &number // *int 是指向 int 的指针类型
value := *pointer // *pointer 是读取指针指向的值

new 可以为某种类型分配零值并返回指针:

pointer := new(int)
fmt.Println(*pointer) // 0
*pointer = 99

指针的零值是 nil。解引用 nil 会触发运行时错误:

var pointer *int
if pointer != nil {
fmt.Println(*pointer)
}

Go 指针不支持 C 语言式的指针算术,不能通过地址加减来移动到其他元素。

函数参数始终按值传递​

传入普通值时,函数得到数值副本:

func incrementValue(number int) {
number++
}

传入指针时,函数得到地址的副本,但复制后的地址仍指向同一个变量:

func incrementPointer(number *int) {
if number == nil {
return
}
(*number)++
}

value := 10
incrementPointer(&value)
fmt.Println(value) // 11

交换两个变量:

func swap(a, b *int) {
if a == nil || b == nil {
return
}
*a, *b = *b, *a
}

接口与隐式实现​

接口描述类型必须提供的行为:

type Describer interface {
Describe() string
}

类型只要拥有接口要求的全部方法,就自动实现该接口:

type Product struct {
Name string
}

func (p Product) Describe() string {
return p.Name
}

Go 不需要也没有 Java 风格的 implements 声明。调用者可以只依赖接口:

func printDescription(value Describer) {
fmt.Println(value.Describe())
}

使用接口实现多态​

不同类型实现相同接口后,可以交给同一个函数处理:

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

func checkout(method PaymentMethod, amount float64) {
if method.Pay(amount) {
fmt.Println(method.Name(), "成功")
}
}

余额支付和银行卡支付可以拥有完全不同的内部字段,只要都实现 Name 和 Pay,就都能传给 checkout。

any 和类型断言​

any 是 interface{} 的别名,可以保存任意类型:

var value any = "Go"

通过类型断言取回具体类型:

text, ok := value.(string)
if ok {
fmt.Println(text)
}

一定要优先使用带 ok 的形式。直接写 text := value.(string),实际类型不匹配时会发生 panic。

判断多种类型时使用类型选择:

switch current := value.(type) {
case int:
fmt.Println("整数", current)
case string:
fmt.Println("字符串", current)
case bool:
fmt.Println("布尔值", current)
default:
fmt.Printf("其他类型:%T\n", current)
}

any 很灵活,但会丢失编译期的具体类型信息。能够使用明确类型或专用接口时,不要为了方便全部改成 any。

接口组合和方法集​

小接口可以组合成更大的接口:

type Reader interface {
Read() string
}

type Writer interface {
Write(content string)
}

type ReadWriter interface {
Reader
Writer
}

方法接收者会影响哪个类型实现接口:

type Document struct {
content string
}

func (d Document) Read() string {
return d.content
}

func (d *Document) Write(content string) {
d.content = content
}

Document 值只有值接收者方法 Read;*Document 指针同时拥有 Read 和 Write,因此只有 *Document 实现 ReadWriter:

document := &Document{}
var _ ReadWriter = document // 编译期检查接口实现

接口和 nil​

接口值可以理解为“动态类型 + 动态值”。只有两者都为空时,接口才等于 nil:

var value any
fmt.Println(value == nil) // true

var pointer *Document
value = pointer
fmt.Println(value == nil) // false

第二种情况中动态值虽然是 nil 指针,但接口仍记录了动态类型 *Document。调用方法前需要结合具体设计处理这种情况。

练习题​

  1. 编写 swap(a, b *int) 交换两个整数,并处理 nil。
  2. 定义 Counter,用指针接收者实现增加、减少和重置。
  3. 为 Rectangle 添加修改原宽高的 Scale 方法。
  4. 定义 Shape 接口,让矩形和圆实现面积与周长计算。
  5. 定义 Notifier,实现邮件和短信两种通知方式。
  6. 定义 PaymentMethod,实现余额支付和银行卡支付。
  7. 接收 []any,使用类型选择统计其中各种类型的数量。
  8. 组合 Reader 和 Writer,实现可读写的内存文档。

示例代码位于:

E:\Project\Golang-Start\chapter-05-pointers-interfaces\examples

运行第一个示例:

go run ./chapter-05-pointers-interfaces/examples/01-pointer-basics