Singleton pattern and lazy initialization pattern

Original link: https://colobu.com/2023/07/27/go-design-patterns-singleton/

In object-oriented programming languages, the Singleton pattern ensures that a class has only one instance and provides global access to that instance.

Then in the Go language, the singleton mode confirms that there is only one instance of a type, and provides global access to the modified instance, usually by directly accessing global variables.

For example, os.Stdin , os.Stdout , and os.Stderr in the Go standard library represent standard input, standard output, and standard error output, respectively. They are global variables of type *os.File and can be used directly in the program:


1
2
3
4
5

var (
Stdin = NewFile( uintptr (syscall.Stdin), “/dev/stdin” )
Stdout = NewFile( uintptr (syscall.Stdout), “/dev/stdout” )
Stderr = NewFile( uintptr (syscall.Stderr), “/dev/stderr” )
)

Another example is EOF under the io package:


1

var EOF = errors. New( “EOF” )

There are many singleton implementations in the Go standard library, such as http.DefaultClient , http.DefaultServeMux , http.DefaultTransport , net.IPv4zero are all singleton objects.

Sometimes, some people also think that the singleton pattern is also an anti-pattern.

Anti-pattern is a common concept in software engineering, which mainly refers to patterns or practices to be avoided in software design and development.

Some of the main characteristics of antipatterns include:

  • It is often a common mistake or pitfall for beginners.
  • It reflects a solution that appears to work but is actually inefficient or wrong.
  • Using anti-patterns may have similar problem-solving effects in the short-term, but backfire in the long-term.
  • It’s usually a bad or shoddy design that doesn’t follow best practices.
  • A better, alternative solution exists.

Some common anti-pattern examples:

  • Copy-paste programming: To reuse code, copy and paste directly without creating functions or modules.
  • God object: A huge complex object containing all functions.
  • Dependency injection abuse: Even simple objects are injected with dependencies, adding complexity.
  • Self-encapsulation: Adding complexity to classes by encapsulating unnecessary details.
  • Excessive abstraction and design: code lacks readability

Why do you say that, add two goroutines to use http.DefaultClient at the same time, and one of the goroutines modifies some fields of this client, which will also affect the use of the second goroutine.

Moreover, these singletons are modifiable objects, and the third library even secretly modifies the value of this variable without you finding out. For example, if you want to connect to the local port 53 and query some domain names, it may be hijacked by others to its server superior:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twenty one
twenty two
twenty three
twenty four
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

package main
import (
“fmt”
“net”
“github.com/miekg/dns”
)
func main() {
// The singleton object is modified, and the following line may actually be written in the init function of a third package
net.IPv4zero = net.IPv4 (8 , 8 , 8 , 8 )
// Set the DNS server address
dnsServer := net.JoinHostPort(net.IPv4zero.String(), “53” )
// create DNS client
c := new (dns. Client)
// Build DNS request message
msg := new (dns.Msg)
msg.SetQuestion(dns.Fqdn( “rpcx.io” ), dns.TypeA)
// Send DNS request message
resp, _, err := c.Exchange(msg, dnsServer)
if err != nil {
fmt.Println( “Error sending DNS request: “ , err)
return
}
// Parse the DNS response message
ipAddr, err := parseDNSResponse(resp)
if err != nil {
fmt.Println( “Error parsing DNS response: “ , err)
return
}
// output query result
fmt.Println( “IPv4 Address for google.com:” , ipAddr)
}
func parseDNSResponse(resp *dns.Msg) ( string , error) {
if len (resp. Answer) == 0 {
return “” , fmt.Errorf( “No answer in DNS response” )
}
for _, ans := range resp. Answer {
if a, ok := ans.(*dns.A); ok {
return aAString(), nil
}
}
return “” , fmt.Errorf( “No A record found in DNS response” )
}

Originally, I wanted to query the local DNS server, but was hijacked to Google’s 8.8.8.8 DNS server for query.

The lazy initialization mode (Lazy initialization, lazy initialization) postpones the creation of objects, the calculation of data and other operations that require more resources, and is only executed when it is accessed for the first time. Lazy initiation is a delaying tactic. Delay the creation of objects, computed values, or other expensive pieces of code until the first need arises.

In a word, it is lazy initialization.

If you are a Java programmer, there is a high probability that you will be asked about the implementation of the singleton pattern during the interview, just like asking how many ways to write the word fennel in fennel beans. There are probably the following implementations of singletons in Java:

  • Eager Initialization

  • Lazy Initialization

  • Double-Checked Locking

  • Static Inner Class

  • Enumeration Singleton (Enum Singleton)

The latter four are all lazy initialization modes, and the instance will not be initialized until it is used for the first time.

lazy_static macro is often used in the Rust language to implement the lazy initial mode to implement a singleton:


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

lazy_static! {
static ref SINGLETON: Mutex<Singleton> = Mutex::new(Singleton::new());
}
struct Singleton {
// Add fields and methods as needed
}
impl Singleton {
fn new () -> Self {
Singleton {
// Initialize fields
}
}
}

In the Go standard library, you can use sync.Once to implement the lazy initial singleton pattern. For example, when os/user obtains the current user, it only needs to execute a time-consuming system call, and then obtain it directly from the result of the first initialization, even if the first query fails:


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

func Current() (*User, error) {
cache.Do( func () { cache.u, cache.err = current() })
if cache.err != nil {
return nil , cache.err
}
u := *cache.u // copy
return &u, nil
}
// cache of the current user
var cache struct {
sync.Once
u *User
err error
}

In the upcoming Go 1.21, sync.Once has three more siblings:


1
2
3

func OnceFunc(f func ()) func ()
func OnceValue(f func () T) func () T
func OnceValues(f func () (T1, T2)) func () (T1, T2)

They are auxiliary functions based on sync.Once. For example, Current can be rewritten using OnceValues. Interested students can try it.

The explanation of these three new functions can be read in my previous article: The new extension of sync.Once (colobu.com)

This article is transferred from: https://colobu.com/2023/07/27/go-design-patterns-singleton/
This site is only for collection, and the copyright belongs to the original author.