使用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能够更便利地完结单例,同时也能够防止了不必要的资源糟蹋。当然,没有任何一种完结是合适一切场景的,咱们需求根据详细场景详细分析。