Golang 大小写可见性规范
一、核心规则
在 Go 中,首字母大小写决定标识符能否被包外访问,这是语言内置的可见性规则。
| 首字母 | 含义 | 可见范围 |
|---|---|---|
| 大写 | 导出(exported / public) | 包外可访问 |
| 小写 | 未导出(unexported / private) | 仅本包内可访问 |
Go 没有 public、private、protected 关键字,全靠首字母控制。
go
package user
type User struct { // ✅ 包外可用:user.User
ID int64 // ✅ 包外可访问
name string // ❌ 包外不可访问
}
func GetUser() { } // ✅ 包外可调用
func validate() { } // ❌ 仅包内可调用
二、适用场景
大小写规则适用于以下所有标识符:
1. 变量
go
package config
var AppName = "MyApp" // 导出,其他包可用 config.AppName
var debug = true // 未导出,仅本包可用
go
import "myproject/config"
fmt.Println(config.AppName) // ✅
fmt.Println(config.debug) // ❌ 编译错误
2. 常量
go
const MaxRetry = 3 // 导出
const defaultTimeout = 5 // 未导出
3. 函数 / 方法
go
func NewUser() *User { } // 导出,常作为构造函数
func parseConfig() { } // 未导出,内部辅助函数
func (u *User) GetName() string { } // 导出方法
func (u *User) setName(s string) { } // 未导出方法
4. 结构体及其字段
go
type User struct {
ID int64 // 导出字段
Name string // 导出字段
email string // 未导出字段
}
其他包只能访问导出字段:
go
u := user.User{
ID: 1,
Name: "张三",
// email: "..." // ❌ 不能访问
}
未导出字段只能通过本包提供的导出方法间接访问:
go
// user 包内
func (u *User) Email() string {
return u.email
}
5. 接口
go
type Reader interface { // 导出接口
Read(p []byte) (n int, err error) // 方法也必须导出
}
type validator interface { // 未导出接口,仅包内用
validate() error
}
注意:接口要导出,其中的方法名也必须是大写,否则其他包无法实现该接口。
6. 类型别名、自定义类型
go
type Status int // 导出类型
type statusCode int // 未导出类型
type HTTPClient struct{} // 导出
type httpClient struct{} // 未导出
7. 包名(特殊规则)
包名必须小写,不能使用大写:
go
package user // ✅
package User // ❌ 不规范
package userService // ❌ 不要用驼峰
包是否可被 import,由目录路径决定,而非包名大小写。
三、常见使用场景
1. 对外 API vs 内部实现
go
// internal/service/user.go
package service
// 导出:给 handler 层调用
func CreateUser(req CreateUserReq) (*User, error) {
if err := validate(req); err != nil {
return nil, err
}
return saveUser(req)
}
// 未导出:内部实现细节
func validate(req CreateUserReq) error { ... }
func saveUser(req CreateUserReq) (*User, error) { ... }
设计原则:导出的少而精,未导出的实现细节多。
2. 构造函数:New + 大写类型名
go
type Client struct {
baseURL string
}
// 导出构造函数
func NewClient(url string) *Client {
return &Client{baseURL: url}
}
其他包使用:client := http.NewClient("https://api.example.com")
3. 错误变量:Err 前缀
go
var ErrNotFound = errors.New("not found")
var ErrInvalidInput = errors.New("invalid input")
var errTimeout = errors.New("timeout") // 内部错误,不导出
4. 接口命名:单方法常用 -er 后缀
go
type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
type Closer interface { Close() error }
5. 缩写词:保持一致的大小写
go
var userID int // ✅ ID 大写
var userId int // ❌ 不规范
type HTTPServer struct{} // ✅
type HttpServer struct{} // ❌
var apiURL string // ✅ URL 全大写
常见缩写词:ID、URL、HTTP、JSON、API、SQL、UUID
四、封装机制说明
Go 的封装边界是「包」,不是「类型」
| 语言 | 封装单位 |
|---|---|
| Java/C++ | 类(class) |
| Go | 包(package) |
同一包内:成员互相可访问
go
package service
type user struct {
name string
}
func foo() {
u := user{name: "张三"} // ✅ 同包可直接写字段
u.name = "李四" // ✅ 同包可改
}
跨包:编译器强制限制
go
import "myapp/internal/user"
u := user.User{ID: 1}
u.ID = 2 // ✅ 可以
// u.name = "张三" // ❌ 编译错误:name 未导出
// u.setName("x") // ❌ 编译错误:方法未导出
name := u.GetName() // ✅ 通过导出方法访问
推荐封装写法:字段小写 + 导出方法
go
package user
type User struct {
id int64
name string
email string
}
func NewUser(name, email string) *User {
return &User{name: name, email: email}
}
func (u *User) Name() string { return u.name }
func (u *User) Email() string { return u.email }
func (u *User) SetEmail(email string) error {
if !isValidEmail(email) {
return ErrInvalidEmail
}
u.email = email
return nil
}
只导出接口,不导出实现
go
// 导出接口
type Repository interface {
GetByID(ctx context.Context, id int64) (*User, error)
}
// 实现未导出
type userRepo struct {
db *sql.DB
}
func NewUserRepo(db *sql.DB) Repository {
return &userRepo{db: db}
}
五、与其他语言对比
| 对比项 | Java | Go |
|---|---|---|
| 私有字段 | private |
首字母小写 |
| 公有 API | public |
首字母大写 |
| 封装范围 | 类 | 包 |
| 包内访问私有成员 | 不行 | 整个包都可以 |
| 强制 getter/setter | 靠 private 约束 |
靠设计约定 |
| 语言 | 可见性控制方式 |
|---|---|
| Java | public / private / protected |
| Python | _ 前缀约定(非强制) |
| Go | 首字母大小写(语言强制) |
六、容易忽略的点
1. internal 目录(比大小写更严格)
project/
└── internal/
└── service/ # 只有 project 树内的代码能 import
即使类型是大写导出的,internal 外的包也 无法 import。这是目录级别的封装,与首字母规则叠加使用。
2. 嵌入(embedding)不突破字段可见性
go
type Base struct {
name string // 未导出
}
type User struct {
Base
}
// 其他包
u := User{}
// u.name // ❌ 仍然不能访问
3. JSON 序列化要求字段导出
go
type User struct {
Name string `json:"name"` // 字段导出才能被 encoding/json 序列化
age int `json:"age"` // 未导出 → json 包访问不到
}
json 标签只控制 JSON 字段名,不改变 Go 的可见性规则。要序列化,字段本身必须导出(大写)。
4. 接口方法的可见性
go
type Writer interface {
write([]byte) error // 未导出方法 → 接口实际上只能在本包内使用
}
七、设计建议
应该导出(大写)的场景
- 库的公开 API
- 需要被其他包调用的函数、类型
- 结构体中需要被外部读写的字段
- 对外暴露的错误:
ErrXxx
不应该导出(小写)的场景
- 内部辅助函数
- 实现细节类型
- 不想暴露的字段(密码、token 等)
- 包内私有常量、变量
实践经验
go
// ❌ 过度暴露
type User struct {
ID int64
Name string
Password string // 危险:外部可直接读写
}
// ✅ 推荐写法
type User struct {
id int64
name string
password string
}
func (u *User) ID() int64 { return u.id }
func (u *User) Name() string { return u.name }
// password 不提供 Getter
设计原则
- 默认小写,确认需要对外暴露时再改为大写
- 敏感字段(密码、token)绝不导出
- 业务逻辑放在未导出的方法中
- 使用
internal/目录防止外部模块随意依赖
go
// 默认小写,需要对外再改大写
func process() { } // 内部
func Process() { } // 确认需要对外再导出
type userRepo struct{} // 内部实现
type UserService struct{} // 对外服务
八、快速对照表
| 标识符 | 大写(导出) | 小写(未导出) |
|---|---|---|
变量 Count / count |
包外可读 | 仅本包 |
函数 Get() / get() |
包外可调用 | 仅本包 |
类型 User / user |
包外可用 | 仅本包 |
结构体字段 Name / name |
包外可访问 | 仅本包 |
接口方法 Read() / read() |
可被外包包实现 | 仅本包 |
常量 MaxSize / maxSize |
包外可用 | 仅本包 |
| 包名 | --- | 必须小写 |
九、常见误解纠正
| 误解 | 实际情况 |
|---|---|
| Go 没有封装 | 有,通过大小写 + 包边界实现 |
| 结构体字段都能随便改 | 仅包内可随意访问;包外只能改导出字段 |
| 方法都能随便调用 | 未导出的方法在包外无法调用 |
| 和 Java 一样要全部 private | Go 风格是:该导出的导出,不该导出的用小写 |
| 导出字段没问题 | 能用,但等于公开实现细节,需谨慎设计 |
十、总结
大写 = 对外公开;小写 = 包内私有
- 大小写规则适用于:变量、常量、函数、方法、类型、结构体字段、接口及接口方法
- 包名单独要求小写
internal/目录提供额外的 import 限制- Go 的封装以包为边界,而非以类型为边界
- 编译器会强制执行可见性规则,不是可选的编码风格