Go没有构造函数,仅靠NewXXX函数约定实现:返回结构体指针以支持方法修改状态,显式初始化slice/map/channel等字段,封装私有字段,并统一校验参数、设默认值。

Go 没有构造函数,但可以用 NewXXX 函数约定替代
Go 语言本身不支持类和构造函数,所谓“构造函数”只是社区约定的 NewXXX(或 New)命名函数,返回结构体指针。它不是语法特性,而是人为约定——编译器不检查、不强制,全靠团队自觉。
常见错误是直接写 MyStruct{...} 初始化后传值,导致后续方法无法修改原实例(值拷贝),或忘记初始化内部字段(如切片未 make、map 未 make)。
- 必须返回指针(
*MyStruct),否则方法调用时 receiver 是副本,无法持久化状态 - 所有需要预设的字段(尤其是 slice、map、channel)必须在函数内显式初始化,不能依赖零值
- 如果结构体含私有字段(小写开头),只能通过
NewXXX构造,这是封装的关键手段
带参数校验和默认值的 New 函数怎么写
真实项目中,New 函数常承担参数合法性检查和默认值填充职责。比如一个连接池结构体,端口不能为 0,超时时间不应过短。
不要把校验逻辑散落在业务代码里;统一收口到 NewPool 中,失败时返回 error,而不是 panic —— 这样调用方能明确处理失败路径。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 校验失败立即 return,避免后续逻辑执行(比如别在
make后再 check) - 默认值应写死在函数内(如
timeout: 30 * time.Second),而非依赖调用方传 0 值 - 若参数过多,考虑用选项模式(Option Pattern),但简单场景直接参数列表更清晰
func NewPool(addr string, port int) (*Pool, error) {
if port <= 0 {
return nil, errors.New("port must be > 0")
}
return &Pool{
addr: addr,
port: port,
timeout: 30 * time.Second,
conns: make([]net.Conn, 0),
}, nil
}为什么不能用 init() 或包级变量模拟构造函数
有人试图用 init() 函数或全局变量(如 defaultPool = NewPool(...))来“预构造”实例,这会导致两个严重问题:一是并发不安全(多个 goroutine 同时 init 可能竞争),二是无法按需定制参数(比如不同测试用例需要不同 timeout)。
更隐蔽的问题是:这类全局实例会让单元测试难以 mock 或重置,尤其当结构体内部含状态(如计数器、缓存 map)时,测试间会相互污染。
-
init()只能做无参、无副作用的初始化,绝不该用于构造可变状态对象 - 包级变量应仅用于真正全局、只读、无状态的配置(如
DefaultClient = &http.Client{...}),且需确保线程安全 - 每个测试用例应独立调用
NewXXX,保证隔离性
嵌入结构体时 New 函数的组合陷阱
当结构体嵌入另一个结构体(如 type Server struct { Logger }),容易误以为调用 NewLogger() 就能自动初始化嵌入字段。实际上 Go 不会自动调用嵌入类型的构造函数,必须手动调用并赋值。
常见错误是漏掉嵌入字段初始化,导致运行时报 panic(如访问未初始化的 s.Logger.Log)。
- 嵌入字段必须显式初始化:
logger: NewLogger(...),不能留空 - 如果嵌入的是接口(如
logger Logger),则需传入具体实现,此时NewServer应接受该接口参数 - 避免在
NewServer内部 new 多个子对象再组合——这会增加耦合,不利于替换实现(比如换日志库)
最易被忽略的一点:嵌入结构体字段名与类型名相同时,Go 允许省略字段名(匿名嵌入),但初始化时仍需显式调用其构造函数,且不能省略字段名写法(除非用字面量匿名嵌入,但那样无法单独初始化)。

















