包级封装
本课属于 Coddy GO 学习旅程的“面向对象编程”单元。第 48 课,共 107 课。
虽然上一课介绍了字段级别的可见性,但 Go 的封装实际上是在包级别进行的。同一个包中的所有内容,无论位于哪个文件,都可以访问未导出的字段和函数。
考虑一个包含多个文件的包:
// account/account.go
package account
type Account struct {
balance int // 未导出
}
func (a *Account) Deposit(amount int) {
a.balance += amount
}
// account/helper.go
package account
func ResetAccount(a *Account) {
a.balance = 0 // 可以访问,因为在同一个包中
}
这两个文件都属于 account 包,因此 helper.go 可以直接访问未导出的 balance 字段。这是有意这样设计的。这样,你可以将包的逻辑拆分到多个文件中,同时保留内部访问权限。
但是,包外的代码无法访问未导出的成员:
// main.go
package main
import "account"
func main() {
acc := &account.Account{}
acc.Deposit(100) // 可以使用:导出的方法
// acc.balance = 50 // 错误:未导出的字段
// account.ResetAccount // 如果 ResetAccount 已导出,就可以使用
}
这种包级边界是 Go 的主要封装机制。你可以将包设计成内聚的单元,让内部代码自由协作,同时只允许外部代码通过导出的 API 交互。可以把每个包看作一个自包含的模块,具有清晰的公共接口。
挑战
简单让我们构建一个库存管理系统,演示同一包中的多个文件如何通过直接访问未导出字段来协作,同时让外部代码无法访问这些数据。
你将创建 3 个文件,让它们在 main 包中协同工作:
product.go:定义一个Product结构体,其中包含一个已导出的Name字段和一个未导出的quantity字段(int)。添加一个已导出的方法Info() string,返回以下格式的字符串:[Name]: [quantity] in stockinventory.go:创建一些辅助函数,通过直接访问未导出的quantity字段来处理商品(因为这些函数与字段位于同一个包中)。实现以下函数:Restock(p *Product, amount int):将指定数量添加到商品的库存中Sell(p *Product, amount int) bool:如果库存充足,就从库存中减去指定数量;如果售出成功,返回true,如果库存不足,则返回falseNewProduct(name string, initialQty int) *Product:构造函数,用给定的名称和初始数量创建商品
main.go:从输入中读取商品详情,使用构造函数创建商品,执行库存操作并显示结果。每次操作后,都打印商品信息以显示更新后的状态。
输入如下:
- 第 1 行:商品名称
- 第 2 行:初始数量(整数)
- 第 3 行:补货数量(整数)
- 第 4 行:售出数量(整数)
创建商品后,打印其初始信息。然后补货,再次打印信息。最后尝试售出商品,并打印更新后的信息(如果售出成功)或 Sale failed: insufficient stock(如果库存不足)。
例如,输入 Laptop、10、5 和 8 时,输出应为:
Laptop: 10 in stock
Laptop: 15 in stock
Laptop: 7 in stock输入 Phone、3、2 和 10 时,输出应为:
Phone: 3 in stock
Phone: 5 in stock
Sale failed: insufficient stock这里的关键在于,inventory.go 与 product.go 位于同一个包中,因此可以直接读取和修改未导出的 quantity 字段。这体现了包级别的封装:内部可以自由协作,同时数据仍受到保护,不会被外部包访问。
亲手试试
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// 读取商品名称
name, _ := reader.ReadString('\n')
name = strings.TrimSpace(name)
// 读取初始数量
initialQtyStr, _ := reader.ReadString('\n')
initialQty, _ := strconv.Atoi(strings.TrimSpace(initialQtyStr))
// 读取补货数量
restockStr, _ := reader.ReadString('\n')
restockAmount, _ := strconv.Atoi(strings.TrimSpace(restockStr))
// 读取销售数量
sellStr, _ := reader.ReadString('\n')
sellAmount, _ := strconv.Atoi(strings.TrimSpace(sellStr))
// TODO:使用 NewProduct 构造函数创建新商品
// TODO:打印商品的初始信息
// TODO:为商品补货并打印信息
// TODO:尝试销售并打印结果
// 如果销售成功,打印更新后的信息
// 如果销售失败,打印 "Sale failed: insufficient stock"
fmt.Println() // 替换为实际输出
}
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
面向对象编程中的所有课
自己练习: Go 在线编译器