使用sync.Once实现高效的单例模式

1. 简介

本文介绍使用sync.Once来实现单例模式,包括单例模式的定义,以及使用sync.Once实现单例模式的示例,同时也比较了其他单例模式的实现。最后以一个开源框架中使用sync.Once实现单例模式的例子来作为结尾。

2. 基本实现

2.1 单例模式定义

单例模式是一种创建型设计模式,它保证一个类只有一个实例,并提供一个全局访问点来访问这个实例。在整个应用程序中,所有对于这个类的访问都将返回同一个实例对象。

2.2 sync.Once实现单例模式

下面是一个简单的示例代码,使用 sync.Once 实现单例模式:

 package singleton

 import "sync"

 type singleton struct {
     // 单例对象的状态
 }

 var (
     instance *singleton
     once     sync.Once
 )

 func GetInstance() *singleton {
     once.Do(func() {
         instance = &singleton{}
         // 初始化单例对象的状态
     })
     return instance
 }

在上面的示例代码中,我们定义了一个 singleton 结构体表示单例对象的状态,然后将它的实例作为一个包级别的变量 instance,并使用一个 once 变量来保证 GetInstance 函数只被执行一次。

GetInstance 函数中,我们使用 once.Do 方法来执行一个初始化单例对象。由于 once.Do 方法是基于原子操作实现的,因此可以保证并发安全,即使有多个协程同时调用 GetInstance 函数,最终也只会创建一个对象。

2.3 其他方式实现单例模式

2.3.1 全局变量定义时赋值,实现单例模式

在 Go 语言中,全局变量会在程序启动时自动初始化。因此,如果在定义全局变量时给它赋值,则对象的创建也会在程序启动时完成,可以通过此来实现单例模式,以下是一个示例代码:

type MySingleton struct {
    // 字段定义
}

var mySingletonInstance = &MySingleton{
    // 初始化字段
}

func GetMySingletonInstance() *MySingleton {
    return mySingletonInstance
}

在上面的代码中,我们定义了一个全局变量 mySingletonInstance 并在定义时进行了赋值,从而在程序启动时完成了对象的创建和初始化。在 GetMySingletonInstance 函数中,我们可以直接返回全局变量 mySingletonInstance,从而实现单例模式。

2.3.2 init 函数实现单例模式

在 Go 语言中,我们可以使用 init 函数来实现单例模式。init 函数是在包被加载时自动执行的函数,因此我们可以在其中创建并初始化单例对象,从而保证在程序启动时就完成对象的创建。以下是一个示例代码:

package main

type MySingleton struct {
    // 字段定义
}

var mySingletonInstance *MySingleton

func init() {
    mySingletonInstance = &MySingleton{
        // 初始化字段
    }
}

func GetMySingletonInstance() *MySingleton {
    return mySingletonInstance
}

在上面的代码中,我们定义了一个包级别的全局变量 mySingletonInstance,并在 init 函数中创建并初始化了该对象。在 GetMySingletonInstance 函数中,我们直接返回该全局变量,从而实现单例模式。

2.3.3 使用互斥锁实现单例模式

在 Go 语言中,可以只使用一个互斥锁来实现单例模式。下面是一个简单代码的演示:

var instance *MySingleton
var mu sync.Mutex

func GetMySingletonInstance() *MySingleton {
   mu.Lock()
   defer mu.Unlock()

   if instance == nil {
      instance = &MySingleton{
         // 初始化字段
      }
   }
   return instance
}

在上面的代码中,我们使用了一个全局变量instance来存储单例对象,并使用了一个互斥锁 mu 来保证对象的创建和初始化。具体地,我们在 GetMySingletonInstance 函数中首先加锁,然后判断 instance 是否已经被创建,如果未被创建,则创建并初始化对象。最后,我们释放锁并返回单例对象。

需要注意的是,在并发高的情况下,使用一个互斥锁来实现单例模式可能会导致性能问题。因为在一个 goroutine 获得锁并创建对象时,其他的 goroutine 都需要等待,这可能会导致程序变慢。

2.4 使用sync.Once实现单例模式的优点

相对于init 方法和使用全局变量定义赋值单例模式的实现,sync.Once 实现单例模式可以实现延迟初始化,即在第一次使用单例对象时才进行创建和初始化。这可以避免在程序启动时就进行对象的创建和初始化,以及可能造成的资源的浪费。

而相对于使用互斥锁实现单例模式,使用 sync.Once 实现单例模式的优点在于更为简单和高效。sync.Once提供了一个简单的接口,只需要传递一个初始化函数即可。相比互斥锁实现方式需要手动处理锁、判断等操作,使用起来更加方便。而且使用互斥锁实现单例模式需要在每次访问单例对象时进行加锁和解锁操作,这会增加额外的开销。而使用 sync.Once 实现单例模式则可以避免这些开销,只需要在第一次访问单例对象时进行一次初始化操作即可。

但是也不是说sync.Once便适合所有的场景,这个是需要具体情况具体分析的。下面说明sync.Onceinit方法,在哪些场景下使用init更好,在哪些场景下使用sync.Once更好。

2.5 sync.Once和init方法适用场景

对于init实现单例,比较适用于在程序启动时就需要初始化变量的场景。因为init函数是在程序运行前执行的,可以确保变量在程序运行时已经被初始化。

对于需要延迟初始化某些对象,对象被创建出来并不会被马上使用,或者可能用不到,例如创建数据库连接池等。这时候使用sync.Once就非常合适。它可以保证对象只被初始化一次,并且在需要使用时才会被创建,避免不必要的资源浪费。

3. gin中单例模式的使用

3.1 背景

这里首先需要介绍下gin.Engine, gin.Engine是Gin框架的核心组件,负责处理HTTP请求,路由请求到对应的处理器,处理器可以是中间件、控制器或处理HTTP响应等。每个gin.Engine实例都拥有自己的路由表、中间件栈和其他配置项,通过调用其方法可以注册路由、中间件、处理函数等。

一个HTTP服务器,只会存在一个对应的gin.Engine实例,其保存了路由映射规则等内容。

为了简化开发者Gin框架的使用,不需要用户创建gin.Engine实例,便能够完成路由的注册等操作,提高代码的可读性和可维护性,避免重复代码的出现。这里对于一些常用的功能,抽取出一些函数来使用,函数签名如下:

// ginS/gins.go
// 加载HTML模版文件
func LoadHTMLGlob(pattern string) {}
// 注册POST请求处理器
func POST(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {}
// 注册GET请求处理器
func GET(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {}
// 启动一个HTTP服务器
func Run(addr ...string) (err error) {}
// 等等...

接下来需要对这些函数来进行实现。

3.2 具体实现

首先从使用出发,这里使用POST方法/GET方法注册请求处理器,然后使用Run方法启动服务器:

func main() {
   // 注册url对应的处理器
   POST("/login", func(c *gin.Context) {})
   // 注册url对应的处理器
   GET("/hello", func(c *gin.Context) {})
   // 启动服务
   Run(":8080")
}

这里我们想要的效果,应该是调用Run方法启动服务后,往/login路径发送请求,此时应该执行我们注册的对应处理器,往/hello路径发送请求也是同理。

所以,这里POST方法,GET方法,Run方法应该都是对同一个gin.Engine 进行操作的,而不是各自使用各自的gin.Engine实例,亦或者每次调用就创建一个gin.Engine实例。这样子才能达到我们预想的效果。

所以,我们需要实现一个方法,获取gin.Engine实例,每次调用该方法都是获取到同一个实例,这个其实也就是单例的定义。然后POST方法,GET方法又或者是Run方法,调用该方法获取到gin.Engine实例,然后调用实例去调用对应的方法,完成url处理器的注册或者是服务的启动。这样子就能够保证是使用同一个gin.Engine实例了。具体实现如下:

// ginS/gins.go
import (
   "github.com/gin-gonic/gin"
)
var once sync.Once
var internalEngine *gin.Engine

func engine() *gin.Engine {
   once.Do(func() {
      internalEngine = gin.Default()
   })
   return internalEngine
}
// POST is a shortcut for router.Handle("POST", path, handle)
func POST(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
   return engine().POST(relativePath, handlers...)
}

// GET is a shortcut for router.Handle("GET", path, handle)
func GET(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
   return engine().GET(relativePath, handlers...)
}

这里engine() 方法使用了 sync.Once 实现单例模式,确保每次调用该方法返回的都是同一个 gin.Engine 实例。然后POST/GET/Run方法通过该方法获取到gin.Engine实例,然后调用实例中对应的方法来完成对应的功能,从而达到POST/GET/Run等方法都是使用同一个实例操作的效果。

3.3 sync.Once实现单例的好处

这里想要达到的目的,其实是GET/POST/Run等抽取出来的函数,使用同一个gin.Engine实例。

为了达到这个目的,我们其实可以在定义internalEngine 变量时,便对其进行赋值;或者是通init函数完成对internalEngine变量的赋值,其实都可以。

但是我们抽取出来的函数,用户并不一定使用,定义时便初始化或者在init方法中便完成了对变量的赋值,用户没使用的话,创建出来的gin.Engine实例没有实际用途,造成了不必要的资源的浪费。

而engine方法使用sync.Once实现了internalEngin的延迟初始化,只有在真正使用到internalEngine时,才会对其进行初始化,避免了不必要的资源的浪费。

这里其实也印证了上面我们所说的sync.Once的适用场景,对于不会马上使用的单例对象,此时可以使用sync.Once来实现。

4.总结

单例模式是一种常用的设计模式,用于保证一个类仅有一个实例。在单例模式中,常常使用互斥锁或者变量赋值的方式来实现单例。然而,使用sync.Once可以更方便地实现单例,同时也能够避免了不必要的资源浪费。当然,没有任何一种实现是适合所有场景的,我们需要根据具体场景具体分析。

posted @ 2023-03-16 20:02  菜鸟额  阅读(170)  评论(0编辑  收藏  举报