The xpool is a user-friendly, type-safe version of sync.Pool.
Inspired by xpool
This package defines an interface Pool[T any]
// Pool is a type-safe object pool interface.
// for convenience, *sync.Pool is a Pool[any]
type Pool[T any] interface {
// Get fetch one item from object pool
// If needed, will create another object.
Get() T
// Put return the object to the pull.
// It may reset the object before put it back to sync pool.
Put(object T)
}
In such way that *sync.Pool
is a Pool[any]
Imagine you need a pool of io.ReadWrite interfaces implemented by bytes.Buffer. You don't need to cast from interface{}
any
more, just do:
pool := xpool.New(func() io.ReadWriter {
return new(bytes.Buffer)
})
rw := pool.Get()
defer pool.Put(rw)
// now you can use a new io.ReadWrite instance
instead using pure go
pool := &sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
rw, _ := pool.Get().(io.ReadWriter)
defer pool.Put(rw)
// now you can use a new io.ReadWrite instance
Object pools are perfect for that are simple to create, like the ones that have a constructor with no parameters. If we need to specify parameters to create one object, then each combination of parameters may create a different object and they are not easy to use from an object pool.
There are two possible approaches:
- map all possible parameters and create one object pool for combination.
- create monadic object that can be easily created and a particular state can be set via some methods.
The second approach we call "Resettable Objects".
Object pools are perfect for stateless objects, however when dealing with monadic objects we need to be extra careful with the object state. Fortunately, we have some objects that we can easily reset the state before reuse.
Some classes of objects like hash.Hash and bytes.Buffer we can call a method Reset()
to return the object to his initial state. Others such bytes.Reader and gzip.Writer have a special meaning for a Reset(state S)
to be possible reuse the same object instead create a new one.
We define two forms of Reset:
The Niladic interface, where Reset()
receives no arguments (for instance, the hash.Hash
case) to be executed before put the object back to the pool.
// Resetter interface.
type Resetter interface {
Reset()
}
And the Monadic interface, where Reset(S)
receives one single argument (for instance, the gzip.Writer
case) to be executed when we fetch an object from the pool and initialize with a value of type S, and will be resetted back to a zero value of S before put the object back to the pool.
// Resetter interface.
type Resetter[S any] interface {
Reset(state S)
}
Monadic resetters are handling by package xpool/monadic.
Important: you may not want to expose objects with a Reset
method, the xpool will not ensure that the type T
is a Resetter[S]
unless you use the NewWithResetter
constructor.
Calling Reset()
before put it back to the pool of objects, on xpool package:
var pool xpool.Pool[hash.Hash] = xpool.NewWithResetter(func() hash.Hash {
return sha256.New()
})
hasher := pool.Get() // get a new hash.Hash interface
defer pool.Put(hasher) // reset it with nil before put back to sync pool.
_, _ = hasher.Write(p)
value := hasher.Sum(nil)
Calling Reset(v)
with some value when acquire the instance and Reset( <zero value> )
before put it back to the pool of objects, on xpool/monadic package:
// this constructor can't infer type S, so you should be explicit!
var pool monadic.Pool[[]byte,*bytes.Reader] = monadic.New[[]byte](
func() *bytes.Reader {
return bytes.NewReader(nil)
},
)
reader := pool.Get([]byte(`payload`)) // reset the bytes.Reader with payload
defer pool.Put(reader) // reset the bytes.Reader with nil
content, err := io.ReadAll(reader)
It is possible set a custom thread-safe Resetter, instead just call Reset()
or Reset(v)
, via a custom resette, instead use the default one.
on xpool package:
//besides the log, both calls are equivalent
pool:= xpool.NewWithCustomResetter(sha256.New,
func(h hash.Hash) {
h.Reset()
log.Println("just reset the hash.Hash")
},
),
// the default resetter try to call `Reset()` method.
pool:= xpool.NewWithDefaultResetter(sha256.New),
on xpool/monadic package:
// besides the log, both calls are equivalent
// the monadic pool will try to call `Reset([]byte)` method by default.
pool:= monadic.New[[]byte](func() *bytes.Reader {
return bytes.NewReader(nil)
})
// the monadic pool will try to call the specific resetter callback.
pool:= monadic.NewWithCustomResetter(func() *bytes.Reader {
return bytes.NewReader(nil)
}, func(object *bytes.Reader, state []byte) {
object.Reset(state)
log.Println("just reset the *bytes.Buffer")
})
You can use custom resetters to handle more complex types of Reset. For instance, the flate.NewReader returns an io.ReadCloser that also implements flate.Resetter that supports a different kind of Reset()
that expect two arguments and also returns an error.
If we can discard the error and set the second parameter a constant value like nil, we can:
// can infer types from resetter
poolReader := monadic.NewWithCustomResetter(func() io.ReadCloser {
return flate.NewReader(nil)
}, func(object io.ReadCloser, state io.Reader) {
if resetter, ok := any(object).(flate.Resetter); ok {
_ = resetter.Reset(state, nil)
}
})
An alternative can be create an object to hold different arguments like in the example below:
type flateResetterArgs struct {
r io.Reader
dict []byte
}
// can infer type S from resetter
poolReader := monadic.NewWithCustomResetter(func() io.ReadCloser {
return flate.NewReader(nil)
}, func(object io.ReadCloser, state *flateResetterArgs) {
if resetter, ok := any(object).(flate.Resetter); ok {
_ = resetter.Reset(state.r, state.dict)
}
})
Custom resetters can do more than just set the status of the object, they can be used to log, trace and extract metrics.
On xpool the resetter is optional, while on xpool/monadic this is mandatory. If you don't want to have resetters on a monadic xpool, please create a regular xpool.Pool
.