从零到一:用Go语言实现单例模式与日志管理器

什么是单例模式?

单例模式是一种创建型设计模式,确保一个类只有一个实例,并提供全局访问点。它适用于需要统一管理资源的场景,例如日志管理、配置管理或数据库连接池。

核心要素

  • 私有构造函数(Go中通过包控制)。
  • 全局唯一实例。
  • 全局访问点。
  • 线程安全(在并发场景中)。

为什么在Go中使用单例模式?

Go语言以简洁著称,但单例模式在以下场景中非常有用:

  • 日志管理:全局共享一个日志实例,避免重复创建。
  • 配置管理:统一加载和访问配置。
  • 资源控制:如数据库连接池,减少资源浪费。

本文将通过一个日志管理器的真实例子,展示如何在Go中实现单例模式。

非线程安全的单例实现

我们先实现一个简单的单例模式,适用于单线程或明确初始化的场景。

代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package logger

import (
    "fmt"
    "time"
)

// Logger 结构体,表示日志管理器
type Logger struct {
    level string // 日志级别,例如 "INFO", "ERROR"
}

// Log 方法,记录一条日志
func (l *Logger) Log(message string) {
    timestamp := time.Now().Format("2006-01-02 15:04:05")
    fmt.Printf("[%s] %s: %s\n", timestamp, l.level, message)
}

// 私有变量,存储唯一实例
var logger *Logger

// GetLogger 返回单例实例
func GetLogger() *Logger {
    if logger == nil {
        logger = &Logger{level: "INFO"}
    }
    return logger
}

使用示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import (
    "yourmodule/logger"
)

func main() {
    log1 := logger.GetLogger()
    log1.Log("Application started")

    log2 := logger.GetLogger()
    log2.Log("Processing data")

    fmt.Printf("log1 == log2: %v\n", log1 == log2) // 输出 true
}

讲解

  • logger是包级私有变量,外部无法直接访问。
  • GetLoggerloggernil时初始化实例。
  • 局限性:在多goroutine场景下,可能会多次初始化。

线程安全的单例实现

Go程序通常是并发的,我们需要确保单例初始化的线程安全。Go的sync.Once是实现线程安全单例的理想工具。

代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package logger

import (
    "fmt"
    "sync"
    "time"
)

// Logger 结构体,表示日志管理器
type Logger struct {
    level string // 日志级别
}

// Log 方法,记录一条日志
func (l *Logger) Log(message string) {
    timestamp := time.Now().Format("2006-01-02 15:04:05")
    fmt.Printf("[%s] %s: %s\n", timestamp, l.level, message)
}

// 私有变量和同步控制
var logger *Logger
var once sync.Once

// GetLogger 返回线程安全的单例实例
func GetLogger() *Logger {
    once.Do(func() {
        logger = &Logger{level: "INFO"}
    })
    return logger
}

使用示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
    "sync"
    "yourmodule/logger"
)

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            log := logger.GetLogger()
            log.Log(fmt.Sprintf("Message from goroutine %d", id))
        }(i)
    }
    wg.Wait()

    log1 := logger.GetLogger()
    log2 := logger.GetLogger()
    fmt.Printf("log1 == log2: %v\n", log1 == log2) // 输出 true
}

讲解

  • sync.OnceDo方法确保logger只初始化一次。
  • 适合高并发场景,例如Web服务中的日志记录。

真实项目:增强版日志管理器

在实际项目中,日志管理器需要更多功能,例如写入文件、支持不同日志级别等。我们扩展Logger并保持单例模式。

代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package logger

import (
    "fmt"
    "os"
    "sync"
    "time"
)

// Logger 结构体,增强版日志管理器
type Logger struct {
    level    string    // 日志级别
    file     *os.File  // 日志文件
    mu       sync.Mutex // 保护文件写入
}

// Log 方法,记录日志到控制台和文件
func (l *Logger) Log(message string) {
    l.mu.Lock()
    defer l.mu.Unlock()

    timestamp := time.Now().Format("2006-01-02 15:04:05")
    logMessage := fmt.Sprintf("[%s] %s: %s\n", timestamp, l.level, message)
    
    fmt.Print(logMessage)
    
    if l.file != nil {
        l.file.WriteString(logMessage)
    }
}

// Close 关闭日志文件
func (l *Logger) Close() {
    l.mu.Lock()
    defer l.mu.Unlock()
    if l.file != nil {
        l.file.Close()
    }
}

var logger *Logger
var once sync.Once

// GetLogger 返回线程安全的单例实例
func GetLogger() *Logger {
    once.Do(func() {
        file, err := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
        if err != nil {
            fmt.Printf("Failed to open log file: %v\n", err)
        }
        logger = &Logger{
            level: "INFO",
            file:  file,
        }
    })
    return logger
}

使用示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import (
    "yourmodule/logger"
)

func main() {
    log := logger.GetLogger()
    log.Log("Application started")
    log.Log("Processing data")

    defer log.Close()
}

讲解

  • 支持日志写入文件(app.log)。
  • 使用sync.Mutex保护文件写入,确保并发安全。
  • 提供Close方法清理资源。

常见问题

  1. 为什么不用全局变量? 全局变量在并发场景下不安全,且容易被外部修改,破坏封装性。

  2. sync.Once 和 Mutex 的区别? sync.Once用于“只执行一次”的场景,代码简洁;Mutex适合多次修改共享资源。

  3. 单例模式在Go中常见吗? 不如Java中常见,但在日志、配置等场景中很有用。

总结

单例模式在Go中通过包、sync.Once等实现,适合全局资源管理。本文通过日志管理器的例子,展示了非线程安全和线程安全的实现方式,以及在实际项目中的应用。希望这篇文章能帮助您更好地理解和使用单例模式!

欢迎留言:分享您在Go项目中使用单例模式的经验!

评论 0