Skip to content(if available)orjump to list(if available)

Go Optimization Guide

Go Optimization Guide

158 comments

·March 31, 2025

nopurpose

Every perf guide recommends to minimize allocations to reduce GC times, but if you look at pprof of a Go app, GC mark phase is what takes time, not GC sweep. GC mark always starts with known live roots (goroutine stacks, globals, etc) and traverse references from there colouring every pointer. To minimize GC time it is best to avoid _long living_ allocations. Short lived allocations, those which GC mark phase will never reach, has almost neglible effect on GC times.

Allocations of any kind have an effect on triggering GC earlier, but in real apps it is almost hopeless to avoid GC, except for very carefully written programs with no dependenciesm, and if GC happens, then reducing GC mark times gives bigger bang for the buck.

liquidgecka

Its worth calling out that abstractions can kill you in unexpected ways with go.

Anytime you use an interface it forces a heap allocation, even if the object is only used read only and within the same scope. That includes calls to things like fmt.Printf() so doing a for loop that prints the value of i forces the integer backing i to be heap allocated, along with every other value that you printed. So if you helpfully make every api in your library use an interface you are forcing the callers to use heap allocations for every single operation.

slashdev

I thought surely an integer could be inlined into the interface, I thought Go used to do that. But I tried it on the playground, and it heap allocates it:

https://go.dev/play/p/zHfnQfJ9OGc

masklinn

Go did use to do that, it was removed years ago, in 1.4: https://go.dev/doc/go1.4#runtime

MarkMarine

Are you including in this analysis the amount of time/resources it takes to allocate? GC isn't the only thing you want to minimize for when you're making a high performance system.

nopurpose

From that perspective it boils down to "do less", which is what any perf guide already includes, allocations is just no different from anything else what app do.

My comment is more about "reduce allocations to reduce GC pressure" advice seen everywhere. It doesn't tell the whole story. Short lived allocation doesn't introduce any GC pressure: you'll be hard pressed to see GC sweep phase on pprof without zooming. People take this advice, spend time and energy hunting down allocations, just to see that total GC time remained the same after all that effort, because they were focusing on wrong type of allocations.

MarkMarine

Yeah I understand what you’re saying, but my point is you’re doing the opposite side of the same coin. Not doing full perf analysis and saying this one method works (yours is to reduce GC mark time, ignoring allocation, others are trying to reduce allocation time, ignoring GC time, or all these other methods listed in this doc.)

kgeist

The runtime also forces GC every 2 minutes. So yeah, a lot of long living allocations can stress the GC, even if you don't allocate often. That's why Discord moved from Go to Rust for their Read States server.

aktau

Side note: see https://tip.golang.org/doc/gc-guide for more on how the Go GC works and what triggers it.

GC frequency is directly driven by allocation rate (in terms of bytes) and live heap size. Some examples:

  - If you halve the allocation rate, you halve the GC frequency.
  - If you double the live heap size, you halve the GC frequency (barring changes away from the default `GOGC=100`).
> ...but if you look at pprof of a Go app, GC mark phase is what takes time, not GC sweep.

It is true that sweeping is a lot cheaper than marking, which makes your next statement:

> Short lived allocations, those which GC mark phase will never reach, has almost neglible effect on GC times.

...technically correct. Usually, this is the best kind of correct, but it omits two important considerations:

  - If you generate a ton of short-lived allocations instead of keeping them around, the GC will trigger more frequently.
  - If you reduce the live heap size (by not keeping anything around), the GC will trigger more frequently.
So now you have cheaper GC cycles, but many more of them. On top of that, you have vastly increased allocation costs.

It is not a priori clear to me this is a win. In my experience, it isn't.

deepsun

Interesting, thank you. But I think those points are not correlated that much. For example if I create unnecessary wrappers in a loop, I might double the allocation rate, but I will not halve the live heap size, because I did not have those wrappers outside the loop before.

Basically, I'm trying to come up with an real world example of a style change (like create wrappers for every error, or use naked integers instead of time.Time) to estimate its impact. And my feeling is that any such example would affect one of your points way more than the other, so we can still argument that e.g. "creating short-lived iterators is totally fine".

nopurpose

I enjoyed your detailed response, it adds value to this discussion, but I feel you missed the point of my comment.

I am against blanket statements "reduce allocations to reduce GC pressure", which lead people wrong way: they compare libraries based on "allocs/op" from go bench, they trust rediculous (who allocates 8KB per iteration in tight loop??) microbenchmarks of sync.Pool like in the article above, hoping to resolve their GC problem. Spend considerabe amount of effort just to find that they barely moved a needle on GC times.

If we generalize then my "avoid long-lived allocations" or yours "reduce allocation rate in terms of bytes" are much more useful in practice, than what this and many other articles preach.

zmj

Pretty similar story in .NET. Make sure your inner loops are allocation-free, then ensure allocations are short-lived, then clean up the long tail of large allocations.

neonsunset

.NET is far more tolerant to high allocation traffic since its GC is generational and overall more sophisticated (even if at the cost of tail latency, although that is workload-dependent).

Doing huge allocations which go to LOH is quite punishing, but even substantial inter-generational traffic won't kill it.

ncruces

The point is not to avoid GC entirely, but to reduce allocation pressure.

If you can avoid allocs in a hot loop, it definitely pays to do so. If you can't for some reason, and can use sync.Pool there, measure it.

Cutting allocs in half may not matter much, but if you can cut them by 99% because you were allocating in every iteration of a 1 million loop, and now aren't, it will make a difference, even if all those allocs die instantly.

I've gotten better than two fold performance increases on real code with both techniques.

zbobet2012

E.h., kind of. If you are allocating in a hot loop it's going to suck regardless. Object pools are really key if you want high perf because the general purpose allocator is way less efficient in comparison.

bboreham

Agree that mark phase is the expensive bit. Disagree that it’s not worth reducing short-lived allocations. I spend a lot of time analyzing Go program performance, and reducing bytes allocated per second is always beneficial.

felixge

+1. In particular []byte slice allocations are often a significant driver of GC pace while also being relatively easy to optimize (e.g. via sync.Pool reuse).

stouset

Checking out the first example—object pools—I was initially blown away that this is not only possible but it produces no warnings of any kind:

    pool := sync.Pool{
        New: func() any { return 42 }
    }

    a := pool.Get()

    pool.Put("hello")
    pool.Put(struct{}{})

    b := pool.Get()
    c := pool.Get()
    d := pool.Get()

    fmt.Println(a, b, c, d)
Of course, the answer is that this API existed before generics so it just takes and returns `any` (née `interface{}`). It just feels as though golang might be strongly typed in principle, but in practice there are APIs left and rigth that escape out of the type system and lose all of the actual benefits of having it in the first place.

Is a type system all that helpful if you have to keep turning it off any time you want to do something even slightly interesting?

Also I can't help but notice that there's no API to reset values to some initialized default. Shouldn't there be some sort of (perhaps optional) `Clear` callback that resets values back to a sane default, rather than forcing every caller to remember to do so themselves?

ncruces

This is still strong typing, even it it's not static typing.

It's static vs. dynamic and strong vs. weak.

https://stackoverflow.com/a/11889763

9rx

It is strong, static, and structural. But structural typing is effectively compile-time duck typing, so it is understandable that some might confuse it with dynamic typing.

masklinn

Ggp is not talking about structural typing, but about sync.Pool type erasing (it takes `any` values, and returns `any` values). So you can put (and will retrieve) random garbage from it.

zaphodias

While I think you're right (generics might be useful there), it's fairly easy to wrap the `sync` primitives such as `sync.Pool` and `sync.Map` into your specific use case.

Go is pretty strict about breaking changes, so they probably won't change the current implementations; maybe we'll see a v2 version, or maybe not. The more code you have, the more code you have to maintain, and given Go's backward-compatibility promises, that's a lot of work.

aktau

Upstream thinks a type-safer `sync.Pool` is a good idea too. It's being discussed in https://go.dev/issue/71076.

Someone

> While I think you're right (generics might be useful there), it's fairly easy to wrap the `sync` primitives such as `sync.Pool` and `sync.Map` into your specific use case.

That’s not a strong argument. You can easily (but sometimes tediously) wrap any API with one that (further) restricts what types you can use with it. Generics make it possible to avoid doing that work, and code you don’t write won’t have errors.

zaphodias

Don't get me wrong, I agree! Especially performance-wise, I'd love to have the best primitives that let me build whatever I want and not some very generic primitives that perform a bit worse and I have to tune myself so I don't shoot myself in the foot.

strangelove026

Sync.map is meant to have poor performance I believe

https://github.com/golang/go/issues/21031

PhilippGille

It depends on the use case.

From the Godoc:

> The Map type is optimized for two common use cases: (1) when the entry for a given key is only ever written once but read many times, as in caches that only grow, or (2) when multiple goroutines read, write, and overwrite entries for disjoint sets of keys. In these two cases, use of a Map may significantly reduce lock contention compared to a Go map paired with a separate Mutex or RWMutex.

Source: https://pkg.go.dev/sync#Map

And regarding slow writes, those were recently improved in Go 1.24:

> The implementation of sync.Map has been changed, improving performance, particularly for map modifications. For instance, modifications of disjoint sets of keys are much less likely to contend on larger maps, and there is no longer any ramp-up time required to achieve low-contention loads from the map.

Source: https://go.dev/doc/go1.24#minor_library_changes ("sync" section)

jlouis

It is fairly common your type system ends up with escape hatches allowing you to violate the type rules in practice. See e.g., OCaml and the function "magic" in the Obj module.

It serves as a way around a limitation in the type system which you don't want to deal with.

You can still have the rest of the code base be safe, as long as you create a wrapper which is.

The same can be said about having imperative implementations with functional interfaces wrapping said implementation. From the outside, you have a view of a system which is functionally sound. Internally, it might break the rules and use imperative code (usually for the case of efficiency).

stouset

Obviously every type system in practice has escape hatches. But I’ve never seen another staticly-typed language where you need to break out of the type system so regularly.

Go’s type system has your back when you’re writing easy stuff.

But it throws up its hands and leaves you to fend for yourself when you need to do nearly anything interesting or complex, which is precisely when I want the type system to have my back.

I should not have to worry (or worse, not worry and be caught off guard) that my pool of database connections suddenly starts handing back strings.

int_19h

> But I’ve never seen another staticly-typed language where you need to break out of the type system so regularly.

It's about the same as Java and C# prior to their adoption of generics, and largely for the same reasons.

jfwwwfasdfs

A lot of languages have top types

tgv

You never programmed in Go, I assume? Then you have to understand that the type of `pool.Get()` is `any`, the wildcard type in Go. It is a type, and if you want the underlying value, you have to get it out by asserting the correct type. This cannot be solved with generics. There's no way in Java, Rust or C++ to express this either, unless it is a pool for a single type, in which case Go generics indeed could handle that as well. But since Go is backwards compatible, this particular construct has to stay.

> Also I can't help but notice that there's no API to reset values to some initialized default.

That's what the New function does, isn't it?

BTW, the code you posted isn't syntactically correct. It needs a comma on the second line.

gwd

> That's what the New function does, isn't it?

But that's only run when the pool needs to allocate more space. What GP seems to expect is that sync.Pool() would always return a zeroed structure, just as Golang allocation does.

I think Golang's implementation does make sense, as sync.Pool() is clearly an optimization you use when performance is an issue; and in that case you almost certainly want to only initialize parts of the struct that are needed. But I can see why it would be surprising.

> [any] is a type

It's typed the way Python is typed, not the way Rust or C are typed; so loses the "if it compiles there's a good chance it's correct" property that people want from statically typed languages.

I don't use sync.Pool, but it does seem like now that we have generics, having a typed pool would be better.

9rx

> so loses the "if it compiles there's a good chance it's correct" property that people want from statically typed languages.

If that's what people actually wanted, Coq and friends would be household names, not the obscure oddities that they are. All the languages that people actually use on any kind of regular basis require you to write tests in order to gain that sense of correctness, which also ends up validating type-correctness as a natural byproduct.

"A machine can help me refactor my code" is the property that most attracts people to the statically typed languages that are normally used. With "I can write posts about it on the internet" being the secondary property of interest.

stouset

> But that's only run when the pool needs to allocate more space. What GP seems to expect is that sync.Pool() would always return a zeroed structure, just as Golang allocation does.

Not quite that. Imagine I have a pool of buffers with a length and capacity, say when writing code to handle receiving data from the network.

When I put one of those buffers back, I would like the next user of that buffer to get it back emptied. The capacity should stay the same, but the length should be zero.

I think it’s reasonable to have a callback to do this. One, it doesn’t force every consumer of the pool to have to remember themselves; it’s now a guarantee of the system itself. Two, it’s not much work but it does prevent me from re-emptying freshly-allocated items (in this case reinitialzing is fast, but in some cases it may not be).

This also should be an optional callback since there are many cases where you don’t want any form of object reset.

tgv

> What GP seems to expect is that sync.Pool() would always return a zeroed structure

Might be, but that's a design decision that has nothing to do with type or generics, isn't it? You seem to refer to a function to drain the pool, which is not needed, and frankly, rather unusual.

> It's typed the way Python is typed

Not in the slightest.

> "if it compiles there's a good chance it's correct"

If you want to compare it to something, it's more like Rust's unwrap(), which will panic if you apply it to the wrong result.

ignoramous

> What GP seems to expect is that sync.Pool() would always return a zeroed structure, just as Golang allocation does.

One could define a new "Pool[T]" type (extending sync.Pool) to get these guarantees:

  type Pool[T any] sync.Pool  // typed def

  func (p *Pool[T]) Get() T { // typed Get
      pp := (*sync.Pool)(p)
      return pp.Get().(T)
  }

  func (p *Pool[T]) Put(v T) { // typed Put
      pp := (*sync.Pool)(p)
      pp.Put(v)
  }

  intpool := Pool[int]{        // alias New
      New: func() any { var zz int; return zz },
  }

  boolpool := Pool[bool]{      // alias New
      New: func() any { var zz bool; return zz },
  }
https://go.dev/play/p/-WG7E-CVXHR

zaphodias

I assume they're referring to the fact that a Pool can hold different types instead of being a collection of items of only one homogeneous type.

eptcyka

Is there a time in your career where an object pool absolutely had to contain an unbounded set of types? Any time when you would try know at compile time the total set of types a pool should contain?

pyrale

> There's no way in Java, Rust or C++ to express this either

You make it look like it's a good thing to be able to express it.

There's no way in Java, Rust or C++ to express this, praised be the language designers.

As for expressing a pool value that may be multiple things without a horrible any type and an horrible cast, you could make an union type in Rust, or an interface in Java implemented by multiple concrete objects. Both ways would force the consumer to explicitly check the value without requiring unchecked duck typing.

int_19h

> There's no way in Java, Rust or C++ to express this, praised be the language designers.

That's not even the case. In Java, you'd just use Object, which is for all practical purposes equivalent to `interface{}` aka `any` in Go. And then you downcast. Indeed, code exactly like this was necessary in Java to work with collections before generics were added to the language.

In C++, there's no common supertype, but there std::any, which can contain a value of any type and be downcast if you know what the actual type is.

tgv

> You make it look like it's a good thing to be able to express it.

No, just that this pre-generics Go, and backwards compatibility is taken seriously.

sophacles

Rust has an Any type. It's rarely useful, but there are occasionally situations where a heterogeneous collection is the right thing to do. Casting the any type back to actual type is fairly nice though, as the operation returns an Option<T> and you're forced to deal with the case where your cast is wrong.

gf000

How is it different than pre-generic Java?

Map/List<T> etc are erased to basically an array of Objects (or a more specific supertype) at compile-time, but you can still use the non-generic version (with a warning) if you want and put any object into a map/list, and get it out as any other type, you having to cast it as the correct type.

sapiogram

> You never programmed in Go, I assume?

You might want to step off that extremely high horse for a second, buddy. It's extremely reasonable to expect a type-safe pool that only holds a single type, since that's the most common use case.

__turbobrew__

Calling mmap “zero copy” is generous. I guess we glaze over the whole page fault thing, or the fact that performance is heavily dependent on how much memory pressure the process is under.

This is the same n00b trap that derailed the llama.cpp project last year because people don’t understand how memory maps and paging works, and the tradeoffs.

kevmo314

Zero-copy is totally underrated. Like the site alludes to, Go's interfaces make it reasonably accessible to write zero-copy code but it still needs some careful crafting. The payoff is great though, I've often been surprised by how much time is spent allocating and shuffling memory around.

jasonthorsness

I once built a proxy that translated protocol A to protocol B in Go. In many cases, protocol A and B were just wrappers around long UTF-8 or raw bytes content. For large messages, reading the content into a slice then writing that same slice into the outgoing socket (preceded and followed by slices containing the translated bits from A to B) made a significant improvement in performance vs. copying everything over into a new buffer.

Go's network interfaces and slices makes this kind of thing particularly simple - I had to do the same thing in Java and it was a lot more awkward.

roundup

Additionally...

- https://go101.org/optimizations/101.html

- https://github.com/uber-go/guide

I wish this content existed as a model context protocol (MCP) tool to connect to my IDE along w/ local LLM.

After 6 months or switching between different language projects, it's challenging to remember all the important things.

jigneshdarji91

Additionally... - https://www.uber.com/en-AU/blog/how-we-saved-70k-cores-acros...

This has saved Uber a lot of money on compute (I'm one of the devs). If your compute fleet is large and has memory to spare (stateless), performing dynamic GOGC tuning to tradeoff higher memory utilization for fewer GC events will save quite a lot of compute.

TechDebtDevin

Embedding those docs in your MCP server takes about 5 seconds with mcp-go's AddResource method

https://github.com/mark3labs/mcp-go/blob/main/examples/every...

donatj

Unpopular opinion maybe, but sync.Pool is so sharp, dangerous and leaky that I'd avoid using it unless it's your absolute last option. And even then, maybe consider a second server first.

infogulch

A new sync/v2 NewPool() is being discussed that eliminates the sharp edges by making it generic: https://github.com/golang/go/issues/71076

I haven't personally found it to be problematic; just keep it private, give it a default new func, and be cautious about only putting things in it that you got out.

nasretdinov

I think in general people understand that sync.Pool introduces essentially an equivalent of unitialised memory (since objects aren't required to be cleaned up before returning them to the pool), and mostly use it for something like []byte, slicing it like buf[0:0] to avoid accidentally reading someone else's memory.

But the instrument itself is really sharp and is indeed kind of last resort

jrockway

GOMEMLIMIT has saved me a number of times. In containerized production, it's nice, because sometimes jobs are ephemeral and don't even do enough allocations to hit the memory limit, so you don't spend any time in GC. But it's saved me the most times in CI where golangci-lint or govulncheck can't complete without running out of memory on a kind-of-large CI machine. Set GOMEMLIMIT and it eventually completes. (I switched to nogo, though, so at least golangci-lint isn't a problem anymore.)

dennis-tra

Can someone explain to me why the compiler can’t do struct-field-alignment? This feels like something that can easily be automated.

masklinn

It can. Rust does.

That requires a way to opt out tho, because there are situations where you need a specific field ordering, so now the langage needs to provide way to tune struct compilation behaviour.

CamouflagedKiwi

Because the order of fields can be significant. It's very relevant for syscalls, and is observable via the reflect package; it'd be strange if the field order was arbitrarily changed (and might change further between releases).

I assume the thinking was that this is pretty easy to optimise if you care, and if it's on by default there'd then have to be some opt-out which there isn't a good mechanism for.

9rx

> and if it's on by default there'd then have to be some opt-out which there isn't a good mechanism for.

Good is subjective, but the mechanism is something already implemented: https://pkg.go.dev/structs#HostLayout

kbolino

In particular, struct field alignment matches C (even without cgo) and so any change to the default would break a lot of code.

9rx

> struct field alignment matches C (even without cgo)

The spec defines alignment for numeric types, but that's about it. There is nothing in the spec about struct layout. That is implementation dependent. If you are relying on a particular implementation, you are decidedly in unsafe territory.

> so any change to the default would break a lot of code.

The compiler can disable optimization on cgo calls automatically and most other places where it matters are via the standard library, so it might not be as much as you think. And if you still have a case where it matters, that is what this is for: https://pkg.go.dev/structs#HostLayout

int_19h

This is very unfortunate, since most structs are never going to be passed to C, yet end up paying the tax anyway. They really should have made it opt-in.

9rx

Like the answer to all "Why doesn't Go have X?" questions: Lack of manpower. There has been some work done to support it, but is far from complete. Open source doesn't mean open willingness to contribute, unfortunately. Especially when you're not the cool kid on the block.

parhamn

Noticed the object pooling doc, had me wondering: are there any plans to make packages like `sync` generic?

neillyons

Curious to know what people are building where you need to optimise like this? eg Struct Field Alignment https://goperf.dev/01-common-patterns/fields-alignment/#avoi...

dundarious

False sharing is an absolutely classic Concurrency 101 lesson, nothing remarkable about it.

kubb

Something that shouldn’t be written in a GC language.

Cthulhu_

GC is not relevant in this case, it's about whether you can make structs fit in cache lines and CPU registers. Mechanical sympathy is the googleable phrase. GC is a few layers further away.

piokoch

I don't think GC has anything to do here, doing manual memory allocation we might hit the same problem.

EdwardDiego

Huh, this surprises me about Golang, didn't realise it was so similar to C with struct alignment. https://goperf.dev/01-common-patterns/fields-alignment/#why-...

Cthulhu_

Yup, it's a fairly low-level language intended as a replacement to C/C++ but for modern day systems (networked, concurrent, etc). You don't have manual memory management per se but you still need to decide on heap vs stack and consider the hardware.

jerf

"you still need to decide on heap vs stack"

No, you can't decide on heap vs stack. Go's compiler decides that. You can get feedback about the decision if you pass the right debug flags, and then based on that you may be able to tickle the optimizer into changing its mind based on code changes you make, but it'll always be an optimization decision subject to change without notice in any future versions of Go, just like any other language where you program to the optimizer.

If you need that level of control, Go is generally not the right language. However, I would encourage developers to be sure they need that level of control before taking it, and that's not special pleading for Go but special pleading for the entire class of "languages that are pretty fast but don't offer quite that level of control". There's still a lot of programmers running around with very 200x ideas of performance, even programmers who weren't programmers at the time, who must have picked it up by osmosis.

(My favorite example to show 200x perf ideas is paginated APIs where the "pages" are generally chosen from the set {25, 50, 100} for "performance reasons". In 2025, those are terribly, terribly small numbers. Presenting that many results to humans makes sense, but my default size for paginating API calls nowadays is closer to 1000, and that's the bottom end, for relatively expensive things. If I have no reason to think it's expensive, tack another order of magnitude on to my minimum.)

fmstephe

Just an anecdote from work to back this up. I wrote a system that was taking requests, making another request to a service (that basically wrapped elasticsearch) and then processed the results and returned to the results to the caller.

By default the elastic-search results were paginated and defaulted to some small number in the order of 25..100. I increased this steadily upwards beyond 100,000 to the point where every request always returned the entire result in the first page. And it _transformed_ the performance of the service. From one that was unbearably slow for human users to one that _felt_ instantaneous. I had real perf numbers at the time, but now all I have are the impressions.

But the lesson on the impact of the overhead of those paginated calls was important. Obviously everything is specific and YMMV, but this something worth having in the back of your mind.

jensneuse

You can often fool yourself by using sync.Pool. pprof looks great because no allocs in benchmarks but memory usage goes through the roof. It's important to measure real world benefits, if any, and not just synthetic benchmarks.

makeworld

Why would Pool increase memory usage?

jensneuse

Let's say you have constantly 1k requests per second and for each request, you need one buffer, each 1 MiB. That means you have 1 GiB in the pool. Without a pool, there's a high likelihood that you're using less. Why? Because in reality, most requests need a 1 MiB buffer but SOME require a 5 MiB buffer. As such, your pool grows over time as you don't have control over the distribution of the size of the pool items.

So, if you have predictable object sizes, the pool will stay flat. If the workloads are random, you have a new problem because, like in this scenario, your pool grows 5x more.

You can solve this problem. E.g. you can only give back items into the pool that are small enough. Alternatively, you could have a small pool and a big pool, but now you're playing cat and mouse.

In such a scenario, it could also work to simply allocate and use GC to clean up. Then you don't have to worry about memory and the lifetime of objects, which makes your code much simpler to read and reason about.

jerf

Long before sync.Pool was a thing, I wrote a pool for []bytes: https://github.com/thejerf/gomempool I haven't taken it down because it isn't obsoleted by sync.Pool because the pool is aware of the size of the []bytes. Though it may be somewhat obsoleted by the fact the GC has gotten a lot better since I wrote it, somewhere in the 1.3 time frame. But it solve exactly that problem I had; relatively infrequent messages from the computer's point of view (e.g., a system that is probably getting messages every 50ms or so), but that had to be pulled into buffers completely to process, and had highly irregular sizes. The GC was doing a ton of work when I was allocating them all the time but it was easy to reuse buffers in my situation.

theThree

>That means you have 1 GiB in the pool.

This only happen when every request last 1 second.

xyproto

I guess if you allocate more than you need upfront that it could increase memory usage.

throwaway127482

I don't get it. The pool uses weak pointers under the hood right? If you allocate too much up front, the stuff you don't need will get garbage collected. It's no worse than doing the same without a pool, right?

nopurpose

also no one GCs sync.Pool. After a spike in utilization, live with increased memory usage until program restart.

ncruces

That's just not true. Pool contents are GCed after two cycles if unused.

nopurpose

What do you mean? Pool content can't be GCed , because there are references to it: pool itself.

What people do is what this article suggested, pool.Get/pool.Put, which makes it only grow in size even if load profile changes. App literally accumulated now unwanted garbage in pool and no app I have seen made and attempt to GC it.