Go包封装依赖标识符首字母大小写:大写导出供外部使用,小写未导出仅包内可见;通过接口+未导出类型+工厂函数实现安全抽象,避免直接导出结构体;文件组织宜按职责分离,核心逻辑放internal.go等,API放api.go等。

Go 语言没有传统意义上的“访问修饰符”(如 public/private),包级封装完全依赖**标识符首字母大小写**这一条规则。记牢这点,就抓住了 Go 包封装的本质。
为什么小写字母开头的函数/变量无法被外部包调用
Go 规定:只有首字母大写的标识符(Exported)才能被其他包导入并使用;小写字母开头的(unexported)仅在定义它的包内可见。
- 这不是编译器“限制”,而是链接期不可见 ——
go build甚至不会把未导出符号写进目标文件 - 即使你用反射(
reflect)尝试读取,也无法跨包访问未导出字段(会 panic 或返回零值) - 别指望通过嵌入结构体“绕过”:若嵌入的是未导出类型,其字段仍不可见
如何安全暴露接口而不泄露实现细节
典型做法是定义导出的接口类型 + 未导出的具体类型,再通过导出的工厂函数返回接口实例。
// package cache
type Cache interface {
Get(key string) (string, bool)
Set(key, value string)
}
type cacheImpl struct { // 小写,不可导出
data map[string]string
}
func NewCache() Cache { // 导出工厂函数
return &cacheImpl{data: make(map[string]string)}
}
- 调用方只能看到
Cache接口行为,无法依赖cacheImpl内部字段或方法 - 后续可自由替换
cacheImpl实现(比如加锁、换 LRU 等),只要接口不变,使用者完全无感 - 避免直接导出结构体指针(如
func New() *MyStruct),否则使用者可能误操作内部字段
包内私有逻辑该放在哪个文件里
Go 不强制文件组织方式,但约定俗成的做法能显著降低误导出风险:
立即学习“go语言免费学习笔记(深入)”;
- 把核心逻辑和未导出类型集中放在
internal.go或impl.go这类命名的文件中 - 导出的 API(接口、工厂函数、常量)统一放在
api.go或export.go中 - 如果包很小(仅一个文件),直接靠首字母控制即可;但一旦出现
func init()或大量辅助函数,拆分文件能快速定位作用域边界 - 注意:
internal/目录是另一层机制(仅允许父目录及子目录导入),和首字母规则无关,别混用
真正容易被忽略的是:包封装不是“防人”,而是“防自己” —— 它迫使你在设计初期就明确哪些是稳定契约,哪些是临时实现。一旦导出,就等于承诺长期兼容。










