SoFunction
Updated on 2025-03-05

Deeply understand the use of Go

1. Introduction

This article will introduce the Go languageConcurrent primitives, includingBasic usage methods, precautions, etc. Better to useTo reduce the repetitive creation of objects, maximize the reuse of objects, reduce the pressure on program GC, and improve the performance of programs.

2. Problem introduction

2.1 Problem Description

Here we implement a simple JSON serializer, which can implement amap[string]intSerialize to a JSON string, and implement it as follows:

func IntToStringMap(m map[string]int) (string, error) {
   // Define one for cache data   var buf 
   ([]byte("{"))
   for k, v := range m {
      ((`"%s":%d,`, k, v))
   }
   if len(m) > 0 {
      (() - 1) // Remove the last comma   }
   ([]byte("}"))
   return (), nil
}

Used hereto cache the data, and then followkey:valueIn the form of , the data is generated by a string and then returned, the implementation is relatively simple.

Each callIntToStringMapWhen a method is created, ato cache intermediate results, andIn fact, it can be reused because the serialization rules have little to do with them, they are just used as a cache area.

But the current implementation is every callIntToStringMapWhen a,If in an application, the request concurrency is very high, it is frequently created and destroyedIt will bring great performance overhead, which will lead to frequent allocation of objects and garbage collection, increasing the pressure of memory usage and garbage collection.

So what's the way to makeCan it be reused to the greatest extent and avoid repeated creation and recycling?

2.2 Solution

In fact, we can find that in order toCan be reused to avoid repeated creation and recycling, we only need toCache, and when needed, remove it from the cache; when used up, put it back into the cache pool. This way, there is no need for every callIntToStringMapWhen a method is created,

Here we can implement a cache pool ourselves. When objects are needed, they can be retrieved from the cache pool. When objects are not needed, they can be put back into the cache pool.IntToStringMapMethods requireWhen , it is taken from the cache pool. When used up, it is put back into the cache pool and wait for the next acquisition. Here is a slicing implementationCache pool.

type BytesBufferPool struct {
   mu   
   pool []*
}

func (p *BytesBufferPool) Get() * {
   ()
   defer ()
   n := len()
   if n == 0 {
      // When there is no object in the cache pool, create a      return &{}
   }
   // When there is an object, take out the last element of the slice and return   v := [n-1]
   [n-1] = nil
    = [:n-1]
   return v
}

func (p *BytesBufferPool) Put(buffer *) {
   if buffer == nil {
      return
   }
   // Put it into slices   ()
   defer ()
   ()
    = append(, buffer)
}

aboveBytesBufferPoolAchievedcache pool, whereGetMethods are used to retrieve objects from the cache pool. If there is no object, create a new object to return;PutMethod is used to replace objectsBytesBufferPoolAmong them, useBytesBufferPoolTo optimizeIntToStringMap

// First define a BytesBufferPoolvar buffers BytesBufferPool

func IntToStringMap(m map[string]int) (string, error) {
   // No longer create it yourself, but remove it from BytesBufferPool   buf := ()
   // After the function is finished, it will be put back into the cache pool   defer (buf)
   ([]byte("{"))
   for k, v := range m {
      ((`"%s":%d,`, k, v))
   }
   if len(m) > 0 {
      (() - 1) // Remove the last comma   }
   ([]byte("}"))
   return (), nil
}

At this point, we implemented a cache pool by ourselves, and successfullyInitToStringMapFunctions have been optimized, reducingFrequent creation and recycling of objects has improved the frequent creation and recycling of objects to a certain extent.

but,BytesBufferPoolThere are actually several problems with the implementation of this cache pool. First, it can only be used for cacheObject; Second, the number of cached objects in the object pool cannot be dynamically adjusted according to the actual situation of the system. If the concurrency is high during a certain period of time,Objects are created in large quantities and put back after useBytesBufferPoolAfter that, it will never be recycled, which may lead to memory waste, and a little more severely, it will also lead to memory leakage.

Since there are these problems with custom cache pools, we can't help but ask, is there a more convenient way in the Go language standard library to help us cache objects?

Not to mention, there is really, the Go standard library provides, can be used to cache objects that need to be created and destroyed frequently, and it supports cache of any type of objects, and at the same timeIt is possible to adjust the number of objects in the cache pool according to the actual situation of the system. If an object has not been used for a long time, it will be recycled at this time.

Compared to the buffer pool that you implement,It has higher performance, makes full use of the capabilities of multi-core CPUs, and can also dynamically adjust the number of objects in the buffer pool according to the load of the currently used objects of the system. It is also relatively simple to use. It can be said that it is the best choice for realizing a stateless object cache pool.

Let's take a look belowBasic usage of theIntToStringMapThe method is being implemented.

3. Basic use

3.1 How to use

Basic definition of 3.1.1

The definition is as follows:Get,PutTwo methods:

type Pool struct {
  noCopy noCopy

  local      // local fixed-size per-P pool, actual type is [P]poolLocal
  localSize uintptr        // size of the local array

  victim      // local from previous cycle
  victimSize uintptr        // size of victims array

  New func() any
}
func (p *Pool) Put(x any) {}
func (p *Pool) Get() any {}
  • GetMethod: FromRemove cached objects
  • PutMethod: Put the cache object intoamong
  • NewFunction: CreatingWhen aNewfunction, whenGetWhen the method cannot obtain the object, it will be called at this timeNewThe function creates a new object and returns it.

3.1.2 How to use

When usingWhen you are in the process of , the following steps are usually required:

  • Use firstDefine an object buffer pool
  • When using the object, remove it from the buffer pool
  • After use, put the object back into the buffer pool again

Here is a simple code example showing how to use itProbably the code structure:

type struct data{
    // Define some properties}
//1. Create a cache pool for data objectsvar dataPool = {New: func() interface{} {
   return &data{}
}}

func Operation_A(){
    // 2. Where data objects are needed, take them out from the cache pool    d := ().(*data)
    // Perform subsequent operations    // 3. Replace the object into the cache pool    (d)
}

3.2 Use examples

Let's useCome rightIntToStringMapCarry out transformation to achieveThe reuse of objects can also automatically adjust the number of objects in the buffer pool according to the current status of the system.

// 1. Define an object buffer poolvar buffers  = {
   New: func() interface{} {
      return &{}
   },
}
func IntToStringMap(m map[string]int) (string, error) {
   // 2. When needed, take an object out of the buffer pool   buf := ().(*)
   ()
   // 3. After using it, put it back into the buffer pool   defer (buf)
   ([]byte("{"))
   for k, v := range m {
      ((`"%s":%d,`, k, v))
   }
   if len(m) > 0 {
      (() - 1) // Remove the last comma   }
   ([]byte("}"))
   return (), nil
}

We useAchievedThe buffer pool, inIntToStringMapIn the function, webuffersGet one fromObject and put it back into the pool at the end of the function, avoiding frequent creation and destructionObject overhead.

At the same time,existIntToStringMapIf calls are not frequent, they can be automatically recycledIn-houseObjects can reduce memory pressure without user concern. Moreover, its underlying implementation also takes into account the concurrent execution of multi-core CPUs. Each processor will have its corresponding local cache, which also reduces the overhead of multi-threaded locking to a certain extent.

As can be seen from the above,It is very simple to use, but there are still some precautions. If used improperly, it may still lead to memory leakage and other problems. Let's introduce it belowThings to note when using it.

4. Precautions for use

4.1 Pay attention to the size of the object you put in

If you don't pay attention to put it inThe size of the object in the buffer pool may appearThere are only a few objects in it, but it occupies a large amount of memory, resulting in memory leaks.

Here, for objects of fixed size, you don't need to pay too much attention to putting them in.The size of the object in this scenario is very small. However, if put inThere is a mechanism for automatic expansion of objects in it. If you do not pay attention to it,The size of the object in this case will likely cause memory leakage. Let’s see an example below:

func Sprintf(format string, a ...any) string {
   p := newPrinter()
   (format, a)
   s := string()
   ()
   return s
}

SprintfThe method completes assembly based on the passed format and the corresponding parameters and returns the corresponding string result. According to the normal idea, you only need to apply for onebyteArray, and then according to certain rules,formatandparameterPut the contents inbyteIn the array, thebyteConvert the array to a string and return it.

According to the above idea, we found that in fact, every time we use itbyteArrays are reusable and do not require repeated construction.

In factSprintfThe same is true for the implementation of the method.byteArrays do not actually create a new one at a time, but reuse them. It achieves appStructure,formatandparameterThe responsibility of assembling into strings according to certain rules is delivered toppStructure, at the same timebyteArray asppMember variables of the structure.

ThenppPut an instance ofAmong them, realizeppReuse purpose, thus introducing the avoidance of duplicate creationbyteArrays lead to frequent GCs, and also improve performance. Below isnewPrinterThe logic of the method, obtainppStructures, all fromGet it in:

var ppFree = {
   New: func() any { return new(pp) },
}

// newPrinter allocates a new pp struct or grabs a cached one.
func newPrinter() *pp {
    // Get pp from ppFree   p := ().(*pp)
   // Execute some initialization logic    = false
    = false
    = false
   (&)
   return p
}

Return to the abovebytearray, at this time it acts asppA member variable of the structure, used for intermediate results of string formatting, is defined as follows:

// Use simple []byte instead of  to avoid large dependency.
type buffer []byte

type pp struct {
   buf buffer
   // Omit other unrelated fields}

There seems to be no problem here, but in fact, there may be problems such as memory waste or even memory leaks. If this happens, there is a very long string that needs to be formatted, and then it is calledSprintfto implement formatting, at this timeppIn the structurebufferIt also needs to be continuously expanded until the entire string length can be stored.ppIn the structurebufferIt will occupy a relatively large memory.

whenSprintfAfter the method is completed, re-set theppStructure placementAmong them, at this timeppIn the structurebufferThe occupied memory will not be released.

However, if called next timeSprintfMethod to format the string, the length is not that long, but at this timeTaken outppIn the structurebyte arrayThe length is after the last expansionbyte array, at this time, memory waste will be caused, and in serious cases, it may even lead to memory leakage.

Therefore, becauseppIn the objectbufferThe memory occupied by the field will be automatically expanded, and the size of the object is not fixed, soppReplace the objectWhen in the middle, you need to pay attention to the size of the object. If it is too large, it may lead to memory leakage or memory waste. You can directly discard it and not re-place it.among. In fact,ppReplace the structureIt is also based on this logic, and it will first judgeppIn structurebufferIf the memory occupied by the field is too large, it will not be re-placed at this time.Among them, they are discarded directly, as follows:

func (p *pp) free() {
   // If the size of the byte array exceeds a certain limit, it will be returned directly   if cap() > 64<<10 {
      return
   }

    = [:0]
    = nil
    = {}
    = nil
   
   // Otherwise, it will be put back in   (p)
}

Based on the above summary, ifIf the memory size occupied by the stored objects is not fixed, you need to pay attention to the size of the object being placed to prevent memory leakage or memory waste.

4.2 Do not put database connection/TCP connection into it

The acquisition and release of resources such as TCP connections and database connections usually require certain specifications, such as explicitly closing the connection after the connection is completed. These specifications are formulated based on network protocols, database protocols and other specifications. If these specifications are not correctly followed, it may lead to problems such as connection leakage and exhaustion of connection pool resources.

When usingWhen storing connection objects, if these connection objects are not explicitly closed, they will remain in memory until the process ends. If there are too many connection objects, these unclosed connection objects will occupy too much memory resources, resulting in memory leaks and other problems.

For example, suppose there is an objectConnIndicates a database connection, itsCloseMethod is used to close the connection. IfConnPut the object inand after being removed from the pool and used it, no manual call is calledCloseIf the method returns the object, these connections will remain open until the program exits or reaches the connection limit. This can lead to resource exhaustion or some other problem.

Here is a simple example code usingStore TCP connection objects, demonstrating the leakage of connection objects:

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

var pool = &{
   New: func() interface{} {
      conn, err := ("tcp", "localhost:8000")
      if err != nil {
         panic(err)
      }
      return conn
   },
}

func main() {

   // Simulate the connection   for i := 0; i < 100; i++ {
      conn := ().()
      (100 * )
      (conn, "GET / HTTP/1.0\r\n\r\n")
      // Don't close the connection      // When you are not using a connection, just release the connection object to the pool      (conn)
   }

}

In the above code, we useCreated a TCP connection and stored it tomiddle. When emulating a connection using, we get the connection object from the pool, send a simple HTTP request to the server, and then release the connection object into the pool. However, we do not explicitly close the connection object. If the number of connected objects is large, these unclosed connected objects will occupy a large amount of memory resources, resulting in memory leaks and other problems.

Therefore, certain specifications should be followed for the release of resources such as database connections or TCP connections, and should not be used at this time.To reuse, you can implement database connection pooling and other methods to achieve connection multiplexing.

5. Summary

This article introduces the Go languagePrimitive, it is a very good tool to realize object reuse, reduce program GC frequency, and improve program performance.

We first introduced a scenario that requires reusing objects through a simple JSON serializer implementation, and then implemented a buffer pool ourselves. From the problems existing in the buffer pool, we then led to the. Next, we introduceBasic use of and application of it to the implementation of JSON serializer.

Next, the introductionCommon precautions, if you need to pay attention to putting them inThe size of the object is analyzed, thus telling theSome possible precautions can help you better use them.

The above is a detailed understanding of the use of Go. For more information about Go, please follow my other related articles!