├── LICENSE ├── README.md ├── assets └── logo.png ├── go.mod ├── item.go ├── no_copy.go ├── pool.go └── pool_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 PJ Engineering and Business Solutions Pty. Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | ⭐   the project to show your appreciation. :arrow_upper_right: 3 |

4 | 5 |

6 | go-pool 7 |

8 | 9 |

10 | 11 | 12 |

13 | 14 | # A Generic sync.Pool ![](https://img.shields.io/static/v1?label=%E2%9C%93&message=Prod%20Ready&labelColor=darkgreen&color=green) 15 | 16 | This package is a **thin** wrapper over the `Pool` provided by the `sync` package. The `Pool` is an essential package to obtain maximum performance. It does so by reducing memory allocations. 17 | 18 | ## Extra Features 19 | 20 | - Invalidate an item from the Pool (so it never gets used again) 21 | - Set a maximum number of items for the Pool (Limit pool growth) 22 | - Return the number of items in the pool (idle and in-use) 23 | - Designed for concurrency 24 | - **Fully Generic** 25 | 26 | ## When should I use a pool? 27 | 28 | > If you frequently allocate many objects of the same type and you want to save some memory allocation and garbage allocation overhead — @jrv 29 | 30 | Read [How did I improve latency by 700% using sync.Pool](https://www.akshaydeo.com/blog/2017/12/23/How-did-I-improve-latency-by-700-percent-using-syncPool) 31 | 32 | ## Example 33 | 34 | ```go 35 | import "github.com/rocketlaunchr/go-pool" 36 | 37 | type X struct {} 38 | 39 | pool := pool.New(func() *X { 40 | return &X{} 41 | }, 5) // maximum of 5 items can be borrowed at a time 42 | 43 | borrowed := pool.Borrow() 44 | defer borrowed.Return() 45 | 46 | // Use item here or mark as invalid 47 | x := borrowed.Item() // Use Item of type: *X 48 | borrowed.MarkAsInvalid() 49 | ``` 50 | 51 | Other useful packages 52 | ------------ 53 | 54 | - [awesome-svelte](https://github.com/rocketlaunchr/awesome-svelte) - Resources for killing react 55 | - [dataframe-go](https://github.com/rocketlaunchr/dataframe-go) - Statistics and data manipulation 56 | - [dbq](https://github.com/rocketlaunchr/dbq) - Zero boilerplate database operations for Go 57 | - [electron-alert](https://github.com/rocketlaunchr/electron-alert) - SweetAlert2 for Electron Applications 58 | - [google-search](https://github.com/rocketlaunchr/google-search) - Scrape google search results 59 | - [igo](https://github.com/rocketlaunchr/igo) - A Go transpiler with cool new syntax such as fordefer (defer for for-loops) 60 | - [mysql-go](https://github.com/rocketlaunchr/mysql-go) - Properly cancel slow MySQL queries 61 | - [react](https://github.com/rocketlaunchr/react) - Build front end applications using Go 62 | - [remember-go](https://github.com/rocketlaunchr/remember-go) - Cache slow database queries 63 | - [testing-go](https://github.com/rocketlaunchr/testing-go) - Testing framework for unit testing 64 | 65 | ### Logo Credits 66 | 67 | 1. Renee French (gopher) 68 | 2. Samuel Jirénius (illustration) -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocketlaunchr/go-pool/12f8d647803e163b2196a6c9c49aca69309898d9/assets/logo.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rocketlaunchr/go-pool 2 | 3 | go 1.23.0 4 | 5 | require golang.org/x/sync v0.13.0 6 | 7 | retract v1.0.0 // Should not have been published 8 | -------------------------------------------------------------------------------- /item.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | // ItemWrap wraps the item returned by the pool's factory. 4 | type ItemWrap[T any] interface { 5 | // Return returns the item back to the pool. 6 | Return() 7 | 8 | // MarkAsInvalid marks the item as invalid (eg. unusable, unstable or broken) so 9 | // that after it gets returned to the pool, it is discarded. It will eventually 10 | // get garbage collected. 11 | MarkAsInvalid() 12 | 13 | // Item represents the unwrapped item borrowed from the pool. 14 | Item() T 15 | } 16 | 17 | // itemWrap wraps the item returned by the pool's factory. 18 | type itemWrap[T any] struct { 19 | item T 20 | 21 | invalid bool 22 | pool interface{ returnItem(*itemWrap[T]) } 23 | } 24 | 25 | // Item represents the unwrapped item borrowed from the pool. 26 | func (iw *itemWrap[T]) Item() T { 27 | return iw.item 28 | } 29 | 30 | // Return returns the item back to the pool. 31 | func (iw *itemWrap[T]) Return() { 32 | iw.pool.returnItem(iw) 33 | } 34 | 35 | // reset restores iw to the zero value. 36 | func (iw *itemWrap[T]) reset() { 37 | iw.item = *new(T) 38 | iw.invalid = false 39 | iw.pool = nil 40 | } 41 | 42 | // MarkAsInvalid marks the item as invalid (eg. unusable, unstable or broken) so 43 | // that after it gets returned to the pool, it is discarded. It will eventually 44 | // get garbage collected. 45 | func (iw *itemWrap[T]) MarkAsInvalid() { 46 | iw.invalid = true 47 | } 48 | -------------------------------------------------------------------------------- /no_copy.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | type noCopy struct{} 4 | 5 | func (*noCopy) Lock() {} 6 | func (*noCopy) Unlock() {} 7 | -------------------------------------------------------------------------------- /pool.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "context" 5 | "runtime" 6 | "sync" 7 | "sync/atomic" 8 | 9 | "golang.org/x/sync/semaphore" 10 | ) 11 | 12 | // Options configures the Pool struct. 13 | type Options struct { 14 | // Initial creates an initial number of ready-to-use items in the pool. 15 | Initial uint32 16 | 17 | // Max represents the maximum number of items you can borrow at a time. This 18 | // prevents unbounded growth in the pool. 19 | // 20 | // Depending on the timing of Returns and Factory calls, the maximum number of 21 | // items in the pool can exceed Max by a small number for a short time. 22 | Max uint32 23 | 24 | // EnableCount, when set, enables the pool's Count function. 25 | // 26 | // NOTE: If you set this AND you need to set your own runtime Finalizer on the item, 27 | // wrap your item in another struct, with the Finalizer added to the inner item. 28 | EnableCount bool 29 | } 30 | 31 | // A Pool is a set of temporary objects that may be individually stored and 32 | // retrieved. 33 | // 34 | // Any item stored in the Pool may be removed automatically at any time without 35 | // notification. If the Pool holds the only reference when this happens, the 36 | // item might be deallocated. 37 | // 38 | // A Pool is safe for use by multiple goroutines simultaneously. 39 | // 40 | // Pool's purpose is to cache allocated but unused items for later reuse, 41 | // relieving pressure on the garbage collector. That is, it makes it easy to 42 | // build efficient, thread-safe free lists. However, it is not suitable for all 43 | // free lists. 44 | // 45 | // An appropriate use of a Pool is to manage a group of temporary items 46 | // silently shared among and potentially reused by concurrent independent 47 | // clients of a package. Pool provides a way to amortize allocation overhead 48 | // across many clients. 49 | // 50 | // The Pool scales under load and shrinks when quiescent. 51 | // 52 | // On the other hand, a free list maintained as part of a short-lived object is 53 | // not a suitable use for a Pool, since the overhead does not amortize well in 54 | // that scenario. It is more efficient to have such objects implement their own 55 | // free list. 56 | // 57 | // A Pool must not be copied after first use. 58 | type Pool[T any] struct { 59 | noCopy noCopy 60 | 61 | initial *uint32 // if nil, then initial items have already been created (or initial option was no set) 62 | 63 | itemWrapPool *sync.Pool 64 | syncPool sync.Pool 65 | semMax *semaphore.Weighted 66 | 67 | enableCount bool 68 | count uint32 // count keeps track of approximately how many items are in existence (in the pool and in-use). 69 | countBorrowedOut uint32 70 | } 71 | 72 | // New creates a new Pool. 73 | // 74 | // factory specifies a function to generate a new item when Borrow is called. opts accepts either an int (representing the max) 75 | // or an Options struct. 76 | // 77 | // NOTE: factory should generally only return pointer types, since a pointer can be put into the return interface 78 | // value without an allocation. 79 | func New[T any](factory func() T, opts ...any) Pool[T] { 80 | pool := Pool[T]{ 81 | itemWrapPool: &sync.Pool{ 82 | New: func() any { return new(itemWrap[T]) }, 83 | }, 84 | } 85 | if len(opts) == 0 { 86 | pool.syncPool.New = func() any { return factory() } 87 | return pool 88 | } 89 | switch o := opts[0].(type) { 90 | case int: 91 | pool.semMax = semaphore.NewWeighted(int64(o)) 92 | case uint32: 93 | pool.semMax = semaphore.NewWeighted(int64(o)) 94 | case int64: 95 | pool.semMax = semaphore.NewWeighted(o) 96 | case Options: 97 | // max 98 | if o.Max != 0 { 99 | if o.Initial > o.Max { 100 | panic("Initial must not exceed Max") 101 | } 102 | pool.semMax = semaphore.NewWeighted(int64(o.Max)) 103 | } 104 | 105 | // initial 106 | if o.Initial > 0 { 107 | pool.initial = &o.Initial 108 | } 109 | 110 | // enableCount 111 | pool.enableCount = o.EnableCount 112 | default: 113 | panic("opts must be an int or Options struct") 114 | } 115 | 116 | pool.syncPool.New = func() any { 117 | newItem := factory() 118 | if pool.enableCount { 119 | atomic.AddUint32(&pool.count, 1) // pool.count++ 120 | runtime.SetFinalizer(newItem, func(newItem any) { 121 | atomic.AddUint32(&pool.count, ^uint32(0)) // pool.count-- 122 | }) 123 | } 124 | return newItem 125 | } 126 | 127 | if pool.initial != nil { 128 | // create initial number of items 129 | items := make([]*itemWrap[T], 0, *pool.initial) 130 | 131 | // create new items 132 | for i := uint32(0); i < *pool.initial; i++ { 133 | items = append(items, pool.borrow()) 134 | } 135 | // return new items 136 | for j := len(items) - 1; j >= 0; j-- { 137 | pool.returnItem(items[j]) 138 | } 139 | pool.initial = nil 140 | } 141 | 142 | return pool 143 | } 144 | 145 | func (p *Pool[T]) borrow() *itemWrap[T] { 146 | if p.semMax != nil { 147 | p.semMax.Acquire(context.Background(), 1) 148 | } 149 | if p.enableCount { 150 | atomic.AddUint32(&p.countBorrowedOut, 1) // p.countBorrowedOut++ 151 | } 152 | 153 | wrap := p.itemWrapPool.Get().(*itemWrap[T]) 154 | item := p.syncPool.Get() 155 | 156 | wrap.item = item.(T) 157 | wrap.pool = p 158 | return wrap 159 | } 160 | 161 | func (p *Pool[T]) returnItem(wrap *itemWrap[T]) { 162 | if !wrap.invalid { 163 | p.syncPool.Put(wrap.item) 164 | } 165 | wrap.reset() 166 | 167 | p.itemWrapPool.Put(wrap) 168 | if p.enableCount { 169 | atomic.AddUint32(&p.countBorrowedOut, ^uint32(0)) // p.countBorrowedOut-- 170 | } 171 | if p.semMax != nil { 172 | p.semMax.Release(1) 173 | } 174 | } 175 | 176 | // Borrow obtains an item from the pool. 177 | // If the Max option is set, then this function will 178 | // block until an item is returned back into the pool. 179 | // 180 | // After the item is no longer required, you must call 181 | // Return on the item. 182 | func (p *Pool[T]) Borrow() ItemWrap[T] { 183 | return p.borrow() 184 | } 185 | 186 | // ReturnItem returns an item back to the pool. However, the 187 | // recommended approach is to call Return on the ItemWrap. 188 | func (p *Pool[T]) ReturnItem(x ItemWrap[T]) { 189 | p.returnItem(x.(*itemWrap[T])) 190 | } 191 | 192 | // Count returns approximately the number of items in the pool (idle). 193 | // If you want an accurate number, call runtime.GC() twice before calling Count (not recommended). 194 | // 195 | // NOTE: Count can exceed both the Initial and Max value by a small number for a short time. 196 | func (p *Pool[T]) Count() uint32 { 197 | if !p.enableCount { 198 | return 0 199 | } 200 | c := atomic.LoadUint32(&p.count) 201 | b := atomic.LoadUint32(&p.countBorrowedOut) 202 | if c > b { 203 | return c - b 204 | } 205 | return 0 206 | } 207 | 208 | // OnLoan returns how many items are in-use. 209 | func (p *Pool[T]) OnLoan() uint32 { 210 | if !p.enableCount { 211 | return 0 212 | } 213 | return atomic.LoadUint32(&p.countBorrowedOut) 214 | } 215 | -------------------------------------------------------------------------------- /pool_test.go: -------------------------------------------------------------------------------- 1 | package pool_test 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | pool "github.com/rocketlaunchr/go-pool" 9 | ) 10 | 11 | func TestPool(t *testing.T) { 12 | type X struct { 13 | ID string 14 | } 15 | 16 | randoms := []string{"AAA", "BBB", "CCC", "DDD", "EEE", "FFF", "GGG", "HHH", "III", "JJJ", "KKK", "LLL", "MMM", "NNN"} 17 | ids := map[string]struct{}{} 18 | for _, v := range randoms { 19 | ids[v] = struct{}{} 20 | } 21 | 22 | i := 0 23 | p := pool.New(func() *X { 24 | // This function will panic if Max field is not able to restrict growth 25 | // of pool and slice goes "index out of range" 26 | defer func() { 27 | i++ 28 | }() 29 | return &X{randoms[i]} 30 | }, pool.Options{Max: 1}) 31 | 32 | for i := 0; i < 1_000_000; i++ { 33 | i := i 34 | t.Run(fmt.Sprintf("test %v", i), func(t *testing.T) { 35 | t.Parallel() 36 | borrowed := p.Borrow() 37 | defer borrowed.Return() 38 | 39 | id := borrowed.Item().ID 40 | 41 | if _, exists := ids[id]; !exists { 42 | t.Errorf("Result was incorrect, got: %s, want: %s.", id, strings.Join(randoms, ",")) 43 | } 44 | }) 45 | } 46 | } 47 | --------------------------------------------------------------------------------