ProgrammingBackend Developer

How does garbage collection (GC) work in Go, what are its features, and what should be noted for effective memory management?

Pass interviews with Hintsage AI assistant

Answer

Garbage collection (GC) in Go is an automatic memory management mechanism that first appeared in the early versions of the language. Historically, GC in Go has been one of the sources of criticism due to its impact on performance. However, with the development of the language, especially after version Go 1.5, it has been significantly improved: now it uses a three-color concurrent mark-and-sweep garbage collector with low pauses.

The problem arises when programs create a large number of temporary objects or when they do not remove references to unused structures: this increases the load on GC and can lead to long pauses. Pay special attention to object types, cyclic references, and long chains of references that lie outside the stack.

The solution is to monitor memory allocation, use profiling and tuning of GC through the environment variable GOGC, minimizing the number of allocations in inner loops and critical sections. It is important to remember that garbage collection in Go works only for the heap: everything allocated on the stack will be automatically freed when it goes out of scope, and objects that have "gone" to the heap are controlled by GC.

Code example:

// Profiling alloc and optimizing GC import ( "runtime" "fmt" ) func main() { var memStats runtime.MemStats runtime.ReadMemStats(&memStats) fmt.Printf("Before allocation: %d bytes ", memStats.Alloc) s := make([]int, 1_000_000) for i := range s { s[i] = i } runtime.GC() // manual cleanup runtime.ReadMemStats(&memStats) fmt.Printf("After GC: %d bytes ", memStats.Alloc) }

Key features:

  • The garbage collector in Go is concurrent — it runs parallel to the main program, reducing pause time.
  • GC cleans up only unused objects in the heap, stack allocation does not require GC.
  • GC behavior is controlled via the environment variable GOGC (e.g., GOGC=100 is the default; lowering it speeds up GC but increases CPU usage).

Tricky questions.

How to know which object "goes" to the heap and which remains on the stack?

Answer: This is determined using escape analysis, which can be analyzed using the compiler flag go build -gcflags="-m". Objects that are returned out of a function or used in closures often go to the heap.

Code example:

func escape() *int { v := 42 return &v // v will be on the heap }

Does GC affect all variables, including those on the stack?

No, GC only works with heap-allocated objects. Everything allocated on the stack is cleared automatically upon function completion.

Can a manual call to runtime.GC() significantly improve performance?

On the contrary, manual calls often degrade performance by increasing CPU consumption. It should only be used for testing or debugging cases.

Common mistakes and anti-patterns

  • Unjustified manual call to runtime.GC()
  • Ignoring memory profiling
  • Excessive allocations in loops

Real-life example

Negative case

A developer writes a service that creates new large slices in each request without considering their lifespan. This quickly leads to increased load on the GC, significant pauses, and performance degradation.

Pros:

  • Rapid implementation of functionality

Cons:

  • Response time issues
  • Unpredictable delays
  • High CPU consumption due to GC

Positive case

A developer optimizes the service, profiles allocations, reuses buffers via sync.Pool, reduces GC call frequency, and minimizes memory allocation in hot spots.

Pros:

  • Stable response times
  • Fewer pauses
  • More efficient memory usage

Cons:

  • Requires profiling and understanding the internal mechanisms of the language