第 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。调用方法前需要结合具体设计处理这种情况。
练习题
- 编写
swap(a, b *int)交换两个整数,并处理nil。 - 定义
Counter,用指针接收者实现增加、减少和重置。 - 为
Rectangle添加修改原宽高的Scale方法。 - 定义
Shape接口,让矩形和圆实现面积与周长计算。 - 定义
Notifier,实现邮件和短信两种通知方式。 - 定义
PaymentMethod,实现余额支付和银行卡支付。 - 接收
[]any,使用类型选择统计其中各种类型的数量。 - 组合
Reader和Writer,实现可读写的内存文档。
示例代码位于:
E:\Project\Golang-Start\chapter-05-pointers-interfaces\examples
运行第一个示例:
go run ./chapter-05-pointers-interfaces/examples/01-pointer-basics