单例模式(Singleton Pattern)工程规范与实现(Go / Rust 双语言)

单例模式(Singleton Pattern)工程规范与实现(Go / Rust 双语言)

1 概述

1.1 定义

单例模式属于创建型设计模式,保证一个类在整个进程中仅有唯一实例,对外提供全局统一访问入口。
核心约束:私有化构造能力,阻止外部随意新建实例,统一管控实例生命周期。

工程简化角色:

  • Singleton(单例对象):目标业务结构体
  • Instance Accessor(访问入口):提供全局获取唯一实例的方法,内置延迟初始化逻辑

1.2 适用场景

推荐使用

  1. 资源独占型组件:数据库连接池、HTTP 客户端、日志管理器、配置管理器;
  2. 需要全局统一状态、全局共享一份资源;
  3. 对象创建成本高(IO、网络、大量内存分配),希望只初始化一次。

避免滥用

  1. 无状态轻量对象,可随意新建;
  2. 需要多实例、多套独立配置;
  3. 单元测试难以 Mock(单例天然强耦合,优先依赖注入替代)。

1.3 解决核心痛点

  1. 避免重复创建高开销对象,节约资源;
  2. 全局统一访问点,统一初始化逻辑;
  3. 控制资源竞争,保证独占资源不会被多次抢占。
    重要提醒:单例属于「全局状态」,优先评估依赖注入方案;无法规避全局实例时再使用单例。

2 通用编码规范

  1. 禁止对外暴露构造函数,阻止外部直接实例化;
  2. 支持延迟初始化(懒加载):首次访问才创建实例;
  3. 并发环境保证线程安全,避免重复创建;
  4. 统一提供 GetInstance() 作为唯一访问入口;
  5. 尽量区分:饿汉式(程序启动初始化) / 懒汉式(首次访问初始化);
  6. 不鼓励强制销毁接口;业务进程生命周期内实例唯一。

3 Go 标准实现

Go 业界标准方案:sync.Once 实现线程安全懒汉单例(推荐)
不推荐:裸 sync.Mutex 双重检查、饿汉静态变量(灵活性差)

package main

import (
	"fmt"
	"sync"
)

// Singleton 单例业务对象
type ConfigManager struct {
	AppName string
	Debug   bool
}

var (
	instance *ConfigManager
	once     sync.Once
)

// GetInstance 唯一访问入口,懒加载、并发安全
func GetInstance() *ConfigManager {
	once.Do(func() {
		// 初始化逻辑,仅执行一次
		instance = &ConfigManager{
			AppName: "singleton-demo",
			Debug:   true,
		}
	})
	return instance
}

func main() {
	c1 := GetInstance()
	c2 := GetInstance()
	fmt.Printf("c1 == c2 ? %t\n", c1 == c2) // c1 == c2 ? true
}

Go 两种方案选型说明

1. 懒汉式 sync.Once(工程首选)

首次调用才初始化;适合初始化较重、不一定会被使用的组件。

2. 饿汉式


var instance = &ConfigManager{AppName: "demo"}
func GetInstance() *ConfigManager { return instance }

程序启动直接创建;适合初始化极简、必然使用的组件。

Go 专项约束

  1. 禁止使用 sync.Mutex 手写双重检查锁定,sync.Once 是官方最优原语;
  2. 单例指针全局共享,结构体内部可变字段读写需要自行加锁;
  3. 尽量避免单例内部大量可变状态,降低并发 bug 风险。

4 Rust 标准实现

Rust 版本演进对照:
lazy_static! → once_cell::Lazy → 标准库 LazyLock(Rust ≥1.80)

方案 A:Rust ≥1.80,标准库 LazyLock(新项目首选,零第三方依赖)

use std::sync::LazyLock;

// 单例对象
#[derive(Debug)]
pub struct ConfigManager {
    pub app_name: String,
    pub debug: bool,
}

// 全局唯一实例,懒加载
pub static INSTANCE: LazyLock<ConfigManager> = LazyLock::new(|| ConfigManager {
    app_name: "singleton-demo".to_string(),
    debug: true,
});

fn main() {
    let c1 = &*INSTANCE;
    let c2 = &*INSTANCE;
    println!("{:p}", c1);
    println!("{:p}", c2);
}

方案 B:Rust 1.70 ~ 1.79 使用 OnceLock

适合:需要运行中途手动初始化的场景

use std::sync::OnceLock;

#[derive(Debug)]
pub struct ConfigManager {
    pub app_name: String,
}

pub static INSTANCE: OnceLock<ConfigManager> = OnceLock::new();

// 访问入口
pub fn get_instance() -> &'static ConfigManager {
    INSTANCE.get_or_init(|| ConfigManager {
        app_name: "demo".to_string(),
    })
}

fn main() {
    println!("{:?}", get_instance());
}

方案 C:历史项目 once_cell /lazy_static(存量老代码)

Rust 专项约束

  1. LazyLock 生成 &'static T,不可变引用;
    如果需要内部修改状态,内部搭配 Mutex/RwLock 内部可变性;
  2. 不要尝试 static mut,极易触发 UB;
  3. 单例尽量存放无状态数据;可变数据务必同步原语保护。

示例:带内部可变状态单例

use std::sync::{LazyLock, Mutex};

pub static COUNTER: LazyLock<Mutex<u64>> = LazyLock::new(|| Mutex::new(0));

5 Go / Rust 单例模式核心对比

对比维度 Go Rust
标准懒加载原语 sync.Once LazyLock(≥1.80) / OnceLock(≥1.70)
变量载体 包级 var 指针 static 静态变量
初始化时机 GetInstance()首次调用 首次解引用 &*INSTANCE
可变状态处理 结构体字段自行加锁 依赖 Mutex/RwLock 内部可变性
第三方依赖 ≥1.80 无依赖;旧版本可选用 once_cell
典型写法 封装 GetInstance() 函数 直接使用static INSTANCE

6 最佳实践与常见误区

6.1 常见误区

  1. 盲目到处使用单例;优先依赖注入,单例是兜底方案;
  2. Go 手写双重检查锁替代 sync.Once;
  3. Rust 使用 static mut 实现可变单例,产生未定义行为;
  4. 在单例中大量存放可变全局状态,导致并发难以调试;
  5. 将单例作为万能方案,忽略单元测试 Mock 困难问题。

6.2 最佳实践

  1. 懒加载优先:组件不一定启用时,使用延迟初始化;
  2. 高并发场景严格使用语言官方同步原语,不要自己造同步逻辑;
  3. 单例内部尽量无状态;必须可变时,配套锁保护;
  4. 存量代码:
  • Go:统一收敛到 sync.Once;
  • Rust:新版本逐步从 lazy_static/once_cell 迁移至标准库 LazyLock;
  1. 边界场景:需要动态延迟初始化、等待配置文件加载,Rust 使用 OnceLock。
posted @ 2026-07-17 15:39  等你下课啊  阅读(8)  评论(0)    收藏  举报