常见设计模式之单例模式(golang)

24 阅读6分钟

单例模式介绍

单例模式是 创建型设计模式 的一种,核心目标是:确保一个类在整个应用中仅有一个实例,并提供一个全局唯一的访问点

它常用于管理全局资源(如配置文件、数据库连接池、日志器、线程池等),避免重复创建实例导致的资源浪费(如频繁连接数据库)或状态不一致(如配置信息冲突)。

单例模式必须满足 3 个关键条件:

  1. 构造方法私有化:禁止外部通过 new 关键字创建实例(杜绝随意创建多个对象);
  2. 自身持有唯一实例:在类内部定义静态成员变量,存储该类的唯一实例;
  3. 提供全局访问点:通过静态方法(如 getInstance())向外部暴露唯一实例。

Go 没有类和构造方法的概念,单例模式通过 包级私有变量 + 公开函数 实现,核心思路:保证实例唯一 + 全局访问,同时需兼顾 Go 特有的 并发安全懒加载 等特性。

Go 实现单例的关键约束:

  1. 实例变量用小写字母开头(instance),保证包级私有(外部无法直接访问);
  2. 提供大写字母开头的公开函数(如 GetInstance())作为全局访问点;
  3. 并发安全:Go 天生支持并发,需通过 sync 包工具避免多协程重复创建实例。

单例模式常见实现

饿汉式(立即初始化)

核心思想:程序启动时(包初始化阶段)直接创建实例,天然并发安全(Go 包初始化是单线程执行的)。

package singleton

var instance *HungrySingleton

type HungrySingleton struct {
    Config string
}

func init() {
    instance = &HungrySingleton{
        Config: "全局配置信息",
    }
}

func GetHungryInstance() *HungrySingleton {
    return instance
}

优缺点:

  • ✅ 优点:实现极简、无并发安全问题、访问效率高(无锁);
  • ❌ 缺点:懒加载失效(实例提前创建),若实例占用资源大且从未被使用,会造成浪费。

适用场景:

实例占用资源少、肯定会被使用的场景(如全局配置类)。

懒汉式(延迟初始化,并发安全版)

核心思想:首次调用 GetInstance() 时才创建实例(懒加载),通过 sync.Mutex 互斥锁保证并发安全。

package singleton

import "sync"

var (
    lazyInstance *LazySingleton
    mutex        sync.Mutex 
)

type LazySingleton struct {
    Data string
}

func GetLazyInstance() *LazySingleton {
    mutex.Lock()        
    defer mutex.Unlock() 

    if lazyInstance == nil {
        lazyInstance = &LazySingleton{
            Data: "延迟初始化的数据",
        }
    }
    return lazyInstance
}

优缺点:

  • ✅ 优点:懒加载(按需创建,节省资源)、并发安全;
  • ❌ 缺点:每次调用 GetInstance() 都要加锁解锁,即使实例已创建,仍有锁开销,高并发场景效率低。

适用场景:

低并发、实例占用资源大且不常用的场景。

双重检查锁定(DCL,Double-Check Locking)

核心思想:优化懒汉式的锁开销,仅在实例未创建时加锁,通过 sync.Once 或 atomic 包避免 “虚假唤醒” 问题(Go 中不推荐直接用 if+mutex 双重检查,推荐用 sync.Once 简化)。

常规实现(DCL)

package main

import (
    "fmt"
    "sync"
)

var lock = &sync.Mutex{}

type single struct {
}

var singleInstance *single

func getInstance() *single {
    if singleInstance == nil {
        lock.Lock()
        defer lock.Unlock()
        if singleInstance == nil {
            fmt.Println("Creating single instance now.")
            singleInstance = &single{}
        } else {
            fmt.Println("Single instance already created.")
        }
    } else {
        fmt.Println("Single instance already created.")
    }

    return singleInstance
}

标准实现(用 sync.Once 简化 DCL):

sync.Once 是 Go 提供的并发原语,保证某个函数仅执行一次,比手动双重检查更简洁、更安全(避免原子操作的复杂性)。

package singleton

import "sync"

var (
    dclInstance *DCLSingleton
    once        sync.Once 
)

type DCLSingleton struct {
    Conn string
}

func GetDCLInstance() *DCLSingleton {
    if dclInstance == nil {
        once.Do(func() {
            dclInstance = &DCLSingleton{
                Conn: "mysql://localhost:3306/test",
            }
        })
    }
    return dclInstance
}

关键说明:

  • sync.Once 内部通过 “互斥锁 + 原子操作” 实现,既保证并发安全,又避免重复初始化;
  • 比 Java 的 DCL 更简洁,无需考虑指令重排(Go 内存模型保证 once.Do() 中的初始化操作对所有协程可见)。

优缺点:

  • ✅ 优点:懒加载、并发安全、高并发效率高(仅首次初始化加锁)

适用场景:

高并发场景下的懒加载单例(如数据库连接池、缓存实例),是 Go 中最常用的工业级实现。

枚举单例(基于 iota,简化版)

Go 没有枚举类型,但可通过 iota 模拟枚举,结合单例思想实现 “唯一实例 + 常量级访问”,适用于全局常量、核心工具类。

package singleton

type EnumSingleton int

const (
    Instance EnumSingleton = iota
)

func (e EnumSingleton) DoSomething() string {
    return "枚举单例执行操作:全局唯一实例"
}

func GetEnumInstance() EnumSingleton {
    return Instance
}

用法:

func main() {
    singleton.Instance.DoSomething()
    
    inst := singleton.GetEnumInstance()
    inst.DoSomething()
}

优缺点:

  • ✅ 优点:实现极简、天然唯一、并发安全、无资源浪费;
  • ❌ 缺点:懒加载失效(程序启动时即定义),仅适用于 “无状态” 单例(如工具类、常量)。

适用场景:

全局常量、无状态工具类(如日志器、格式转换器),是 Go 中最简洁的单例实现。

带参数的单例(支持初始化配置)

核心思想:单例实例需要依赖外部参数(如数据库地址、端口)时,需在首次调用时传入参数初始化,后续调用直接返回实例。

package singleton

import "sync"

var (
    paramInstance *ParamSingleton
    paramOnce     sync.Once
    initParams struct {
        Addr string
        Port int
    }
)

type ParamSingleton struct {
    Addr string 
    Port int
}

func GetParamInstance(addr string, port int) *ParamSingleton {
    once.Do(func() {
        paramInstance = &ParamSingleton{
            Addr: addr,
            Port: port,
        }
        initParams.Addr = addr
        initParams.Port = port
    })

    if addr != initParams.Addr || port != initParams.Port {
        panic("单例已初始化,参数不一致")
    }

    return paramInstance
}

用法:

func main() {
    inst1 := singleton.GetParamInstance("localhost", 8080)
    inst2 := singleton.GetParamInstance("localhost", 8080)
    fmt.Println(inst1 == inst2)
}

适用场景:

实例需要依赖外部配置(如数据库连接池、Redis 客户端)的场景。

关键注意点

并发安全的核心工具

  • sync.Mutex:互斥锁,适用于简单场景,但每次调用都加锁,效率较低;
  • sync.Once:推荐用于懒加载单例,保证初始化函数仅执行一次,兼顾安全和效率;
  • 避免使用 atomic 包手动实现双重检查(复杂且易出错,sync.Once 已封装最优逻辑)。

禁止复制单例实例

Go 中结构体默认支持值复制,若单例被复制,会导致多个实例,破坏单例特性。解决方案:

  • 让单例结构体实现 Unsupported 接口,禁止复制(编译期检查);
  • 仅对外暴露指针(*Singleton),避免值传递。

示例(禁止复制):

type Singleton struct{}

// 编译期检查:若尝试复制 Singleton,会报编译错误
var _ = func(s Singleton) {}(&Singleton{})

单例的生命周期

Go 中单例实例的生命周期与程序一致(直到程序退出),若需手动销毁单例(如释放资源),可添加 Destroy() 函数:

func DestroyInstance() {
    mutex.Lock()
    defer mutex.Unlock()
    if instance != nil {
        instance.Conn.Close() 
        instance = nil   
    }
}

总结

实现方式懒加载并发安全支持参数适用场景
饿汉式实例占用资源少、必被使用
普通懒汉式(Mutex)低并发、需懒加载
双重检查(sync.Once)高并发、需懒加载(推荐)
枚举单例无状态工具类、全局常量

推荐优先级

  • 无参数 + 懒加载 + 高并发:sync.Once 双重检查模式;
  • 无参数 + 必被使用:饿汉式;
  • 无状态工具类:枚举单例;
  • 需传入初始化参数:带参数的 sync.Once 模式。