Go通过首字母大小写控制封装:小写字段(如age)包外不可见,大写(如Name)可导出;工厂函数(如NewPerson)控制实例化并校验;接口定义契约实现解耦。

Go结构体字段大小写决定能否被外部包访问
Go没有private或public关键字,封装全靠首字母大小写。小写字母开头的字段(如age、sal)在包外不可见;大写字母开头的(如Name、ID)自动导出,能被其他包直接读写。
这规则适用于字段、函数、方法、类型——只要名字以小写开头,就锁死在当前包内。比如你在model包里定义type person struct { name string },外部包连p.name都编译不过,报错cannot refer to unexported field 'name' in struct literal。
- 别指望用注释或文档“约定”来替代大小写规则——Go强制执行,不讲情面
- 嵌套结构体里的私有字段同样受保护:即使外层结构体是公有的,内部私有字段仍不可穿透
- 如果想让字段只读(比如
ID初始化后不允许改),就只提供GetID(),不写SetID()
工厂函数是控制实例化入口的唯一可靠方式
把结构体设为小写(如person而非Person),再配一个大写的工厂函数(如NewPerson()),就能堵死外部直接&person{...}的路。这是Go里最常用、也最有效的封装起点。
工厂函数不只是“构造器”,它承担了初始化校验责任。比如年龄不能为负、邮箱格式要验证、ID必须UUID生成——这些逻辑塞进NewPerson()比塞进SetAge()更早、更彻底。
立即学习“go语言免费学习笔记(深入)”;
- 工厂函数返回指针(
*person)更常见,因为后续方法多用指针接收器,避免复制开销 - 不要在工厂里做耗时操作(如网络请求、文件读取),否则调用方无法感知失败;真需要,拆成
NewPerson()+LoadFromDB()两步 - 如果结构体字段全可公开(比如DTO、配置结构体),其实没必要工厂函数——Go不强迫你封装
Set/Get方法不是必须写,但写就要带业务校验
很多人机械套Java习惯,给每个私有字段配一对SetXxx()/GetXxx()。Go里这不是规范,而是权衡:如果字段修改无需约束(比如name只检查非空),那SetName()有意义;但如果只是透传赋值,不如直接暴露字段。
真正该加校验的是有业务含义的字段:年龄范围、金额区间、状态枚举值、长度限制。没校验的Set方法等于开门揖盗。
-
SetAge()里写if age 150是合理的;但SetName()里只写if name == ""就略显单薄,不如合并到工厂函数里一次性校验 - 返回错误比
fmt.Println()有用得多——调用方需要知道失败,而不是只看到控制台打印 - 别为了“封装”而封装:如果
person只在model包内使用,且所有修改都走同一组方法,那字段本身私有已足够,不必额外抽象
接口才是Go封装的高阶形态,不是装饰品
当多个结构体要共享行为(比如Logger、Storer、Validator),就该提取接口。接口定义契约,隐藏实现——这才是Go式封装的核心价值,比字段私有化影响更深。
比如定义type Employee interface { GetSalary() float64; CalculateBonus() float64 },然后让FullTime和Contractor各自实现。上层代码只依赖Employee,完全不知道背后是哪种雇员。
- 接口名用单个名词(
Reader、Writer),方法名用动词(Read()、Write()),这是Go社区共识 - 小接口优先:
io.Reader只有Read()一个方法,却撑起整个I/O生态;别一上来就定义10个方法的大接口 - 接口应在调用方定义(consumer-defined),而不是实现方定义——谁要用,谁定契约,避免实现方过度承诺
Go的封装不是靠语法糖堆出来的,它依赖开发者对“什么该藏、什么该露”的持续判断。字段大小写是底线,工厂函数是入口守门人,校验逻辑是业务护城河,接口才是解耦主干道。漏掉任何一层,都可能让封装变成假象。


















