Go 语言单例设计模式
🏗 什么是单例模式?
单例(Singleton)是一种创建型设计模式, 它的目标是:一个类只有一个实例,并且提供全局访问点。
用白话说,就是:
整个程序中,这个对象只能创建一次,多次调用都用同一个。
典型场景:配置管理、日志记录、数据库连接池等。
🏹 Go 中实现单例的特点
因为 Go 没有像 Java 那样的类和构造器, 它主要用 包级变量 + sync.Once + init 函数 来实现单例。
Go 关键点: ✅ 包级变量 ✅ sync.Once 保证只初始化一次(线程安全) ✅ 提供对外公开的获取实例方法
🌟 单例模式完整代码讲解
我们一步步来看,最后给你拼成完整例子。
① 定义结构体(要单例的对象)
假设我们要实现一个 Logger
日志记录器。
type Logger struct { logLevel string }
这个 Logger
是我们只希望存在一个的对象。
② 定义包级变量
var instance *Logger
注意:
-
用
var
定义全局变量instance
-
最开始它是
nil
③ 使用 sync.Once 保证只初始化一次
Go 提供了 sync.Once
,它的 Do
方法能保证:
无论多少 goroutine 调用,函数只执行一次。
import "sync" var once sync.Once
④ 提供获取单例实例的方法
我们写一个 GetInstance()
方法, 外部代码都通过它来获取单例。
func GetInstance() *Logger { once.Do(func() { instance = &Logger{ logLevel: "INFO", // 默认日志级别 } }) return instance }
这里的关键:
-
第一次调用时,
once.Do
中的函数会运行,创建Logger
实例。 -
后面再调用
GetInstance()
,once.Do
不会再跑,直接返回之前创建的instance
。
⑤ 测试一下单例
写个 main 函数测试看看:
package main import ( "fmt" "singleton" ) func main() { logger1 := singleton.GetInstance() logger2 := singleton.GetInstance() if logger1 == logger2 { fmt.Println("是同一个实例") } else { fmt.Println("不是同一个实例") } logger1.logLevel = "DEBUG" fmt.Println("logger2 的 logLevel:", logger2.logLevel) // 应该输出 DEBUG }
输出:
是同一个实例 logger2 的 logLevel: DEBUG
这证明:
-
logger1 和 logger2 指向同一个内存对象。
-
改变一个的属性,另一个也能看到。
📦 完整代码(带 package)
文件结构:
/singleton singleton.go main.go
singleton/singleton.go
package singleton import "sync" type Logger struct { logLevel string } var instance *Logger var once sync.Once func GetInstance() *Logger { once.Do(func() { instance = &Logger{ logLevel: "INFO", } }) return instance }
main.go
package main import ( "fmt" "singleton" ) func main() { logger1 := singleton.GetInstance() logger2 := singleton.GetInstance() if logger1 == logger2 { fmt.Println("是同一个实例") } else { fmt.Println("不是同一个实例") } logger1.logLevel = "DEBUG" fmt.Println("logger2 的 logLevel:", logger2.logLevel) }
🛡 为什么要用 sync.Once?
很多人会问:
为什么不用 init() 直接初始化?
因为:
-
init() 是包加载时调用的,不是懒加载,可能浪费资源。
-
多线程(goroutine)环境下,如果手动加锁实现,容易写错。
-
sync.Once 是 Go 专门为「只执行一次」设计的,简单可靠、线程安全。
📌 单例模式的优缺点
✅ 优点 | ⚠️ 缺点 |
---|---|
只有一个实例,节省资源 | 不容易扩展为多实例 |
提供全局访问点,方便共享 | 全局状态可能导致隐藏的依赖关系 |
延迟加载(懒汉式)节省初始化开销 | 单例隐藏在包里,测试时不好 mock |
👉 立即点击链接,开启你的全栈开发之路:Golang全栈开发完整课程