Documentation
¶
Overview ¶
Package rill is a Go toolkit designed for efficient and straightforward streaming, parallel processing, and pipeline construction. It abstracts away the complexities of concurrency management, enabling developers to focus on core logic. With features like lightweight integration, batch processing, error handling, and support for functional programming paradigms, rill enhances productivity in building concurrent applications. It offers type-safe operations, and minimizes memory usage even for large data sets.
Example (Basic) ¶
A basic example demonstrating how ForEach can be used to process a list of items concurrently.
package main
import (
"fmt"
"math/rand"
"strings"
"time"
"github.com/destel/rill"
)
func main() {
items := rill.FromSlice([]string{"item1", "item2", "item3", "item4", "item5", "item6", "item7", "item8", "item9", "item10"}, nil)
err := rill.ForEach(items, 3, func(item string) error {
randomSleep(1000 * time.Millisecond) // simulate some additional work
res := strings.ToUpper(item)
fmt.Println(res)
return nil
})
if err != nil {
fmt.Println("Error:", err)
}
}
func randomSleep(max time.Duration) {
time.Sleep(time.Duration(rand.Intn(int(max))))
}
Output:
Example (Batching) ¶
This example fetches keys from a list of URLs, retrieves their values from a key-value database, and prints them. The pipeline leverages concurrency for fetching and processing and uses batching to reduce the number of database calls.
package main
import (
"fmt"
"math/rand"
"path/filepath"
"strings"
"time"
"github.com/destel/rill"
)
type KV struct {
Key string
Value string
}
func main() {
startedAt := time.Now()
defer func() { fmt.Println("Elapsed:", time.Since(startedAt)) }()
urls := rill.FromSlice([]string{
"https://example.com/file1.txt",
"https://example.com/file2.txt",
"https://example.com/file3.txt",
"https://example.com/file4.txt",
}, nil)
// Fetch keys from each URL and flatten them into a single stream
keys := rill.FlatMap(urls, 3, func(url string) <-chan rill.Try[string] {
return streamFileLines(url)
})
// Exclude any empty keys from the stream
keys = rill.Filter(keys, 3, func(key string) (bool, error) {
return key != "", nil
})
// Organize keys into manageable batches of 10 for bulk operations
keyBatches := rill.Batch(keys, 10, 1*time.Second)
// Fetch values from DB for each batch of keys
resultBatches := rill.Map(keyBatches, 3, func(keys []string) ([]KV, error) {
values, err := kvMultiGet(keys...)
if err != nil {
return nil, err
}
results := make([]KV, len(keys))
for i, key := range keys {
results[i] = KV{Key: key, Value: values[i]}
}
return results, nil
})
// Convert batches back to a single items for final processing
results := rill.Unbatch(resultBatches)
// Exclude any empty values from the stream
results = rill.Filter(results, 3, func(kv KV) (bool, error) {
return kv.Value != "<nil>", nil
})
// Iterate over each key-value pair and print
cnt := 0
err := rill.ForEach(results, 1, func(kv KV) error {
fmt.Println(kv.Key, "=>", kv.Value)
cnt++
return nil
})
if err != nil {
fmt.Println("Error:", err)
}
fmt.Println("Total keys:", cnt)
}
// streamFileLines simulates line-by-line streaming of a file from a URL,
// introducing a randomized delay to simulate network latency.
// It's a simplified placeholder for actual network-based file streaming.
func streamFileLines(url string) <-chan rill.Try[string] {
out := make(chan rill.Try[string])
go func() {
defer close(out)
base := filepath.Base(url)
base = strings.TrimSuffix(base, filepath.Ext(base))
for i := 0; i < 10; i++ {
randomSleep(20 * time.Millisecond)
out <- rill.Wrap(fmt.Sprintf("%s:key:%d", base, i), nil)
}
}()
return out
}
// kvMultiGet simulates a batch read from a key-value database,
// introducing a randomized delay to simulate network latency.
// It's a simplified placeholder for actual database operation.
func kvMultiGet(keys ...string) ([]string, error) {
randomSleep(1000 * time.Millisecond)
values := make([]string, len(keys))
for i, key := range keys {
if strings.HasSuffix(key, "2") || strings.HasSuffix(key, "3") {
values[i] = "<nil>"
continue
}
values[i] = strings.Replace(key, "key:", "val:", 1)
}
return values, nil
}
func randomSleep(max time.Duration) {
time.Sleep(time.Duration(rand.Intn(int(max))))
}
Output:
Example (Ordering) ¶
This example demonstrates how OrderedMap can be used to enforce ordering of processing results. Pipeline below fetches temperature measurements for a city and calculates daily temperature changes. Measurements are fetched concurrently, but ordered processing is used to calculate the changes.
package main
import (
"fmt"
"math"
"math/rand"
"time"
"github.com/destel/rill"
)
type Measurement struct {
Date time.Time
Temp float64
}
func main() {
startedAt := time.Now()
defer func() { fmt.Println("Elapsed:", time.Since(startedAt)) }()
city := "New York"
endDate := time.Now()
startDate := endDate.AddDate(0, 0, -30)
// Make a channel that emits all the days between startDate and endDate
days := make(chan rill.Try[time.Time])
go func() {
defer close(days)
for date := startDate; date.Before(endDate); date = date.AddDate(0, 0, 1) {
days <- rill.Wrap(date, nil)
}
}()
// Download the temperature for each day concurrently
measurements := rill.OrderedMap(days, 10, func(date time.Time) (Measurement, error) {
temp, err := getTemperature(city, date)
return Measurement{Date: date, Temp: temp}, err
})
// Iterate over the measurements, calculate and print changes. Use a single goroutine
prev := Measurement{Temp: math.NaN()}
err := rill.ForEach(measurements, 1, func(m Measurement) error {
change := m.Temp - prev.Temp
prev = m
fmt.Printf("%s: %.1f°C (change %+.1f°C)\n", m.Date.Format("2006-01-02"), m.Temp, change)
return nil
})
if err != nil {
fmt.Println("Error:", err)
}
}
// getTemperature simulates fetching a temperature reading for a city and date,
func getTemperature(city string, date time.Time) (float64, error) {
randomSleep(1000 * time.Millisecond)
var h float64
for _, c := range city {
h += float64(c)
}
temp := 15 - 10*math.Sin(h+float64(date.Unix()))
return temp, nil
}
func randomSleep(max time.Duration) {
time.Sleep(time.Duration(rand.Intn(int(max))))
}
Output:
Example (Slices) ¶
Rill is designed for channel based workflows, but it can also be used with slices, thanks to its ability to do ordered processing. Example below demonstrates how you can create a **mapSLice** generic helper function that does parallel slice processing. That helper is then used to fetch users from an API concurrently.
package main
import (
"fmt"
"math/rand"
"time"
"github.com/destel/rill"
)
type User struct {
ID int
Username string
}
func main() {
startedAt := time.Now()
defer func() { fmt.Println("Elapsed:", time.Since(startedAt)) }()
ids := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
users, err := mapSLice(ids, 3, getUser)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("%+v\n", users)
}
// mapSLice is a helper function that does a parallel map operation on a slice of items
func mapSLice[A, B any](in []A, n int, f func(A) (B, error)) ([]B, error) {
inChan := rill.FromSlice(in, nil)
outChan := rill.OrderedMap(inChan, n, f)
return rill.ToSlice(outChan)
}
func randomSleep(max time.Duration) {
time.Sleep(time.Duration(rand.Intn(int(max))))
}
// getUser simulates fetching a user from an API, introducing a randomized delay to simulate network latency.
func getUser(id int) (User, error) {
randomSleep(1000 * time.Millisecond)
adj := []string{"big", "small", "fast", "slow", "smart", "happy", "sad", "funny", "serious"}
noun := []string{"dog", "cat", "bird", "fish", "mouse", "elephant", "lion", "tiger", "bear", "wolf"}
username := fmt.Sprintf("%s_%s", adj[rand.Intn(len(adj))], noun[rand.Intn(len(noun))])
return User{ID: id, Username: username}, nil
}
Output:
Index ¶
- func Batch[A any](in <-chan Try[A], n int, timeout time.Duration) <-chan Try[[]A]
- func Buffer[A any](in <-chan A, n int) <-chan A
- func Catch[A any](in <-chan Try[A], n int, f func(error) error) <-chan Try[A]
- func Drain[A any](in <-chan A)
- func DrainNB[A any](in <-chan A)
- func Filter[A any](in <-chan Try[A], n int, f func(A) (bool, error)) <-chan Try[A]
- func FlatMap[A, B any](in <-chan Try[A], n int, f func(A) <-chan Try[B]) <-chan Try[B]
- func ForEach[A any](in <-chan Try[A], n int, f func(A) error) error
- func FromChan[A any](values <-chan A, err error) <-chan Try[A]
- func FromChans[A any](values <-chan A, errs <-chan error) <-chan Try[A]
- func FromSlice[A any](slice []A, err error) <-chan Try[A]
- func Map[A, B any](in <-chan Try[A], n int, f func(A) (B, error)) <-chan Try[B]
- func Merge[A any](ins ...<-chan A) <-chan A
- func OrderedCatch[A any](in <-chan Try[A], n int, f func(error) error) <-chan Try[A]
- func OrderedFilter[A any](in <-chan Try[A], n int, f func(A) (bool, error)) <-chan Try[A]
- func OrderedFlatMap[A, B any](in <-chan Try[A], n int, f func(A) <-chan Try[B]) <-chan Try[B]
- func OrderedMap[A, B any](in <-chan Try[A], n int, f func(A) (B, error)) <-chan Try[B]
- func OrderedSplit2[A any](in <-chan Try[A], n int, f func(A) (bool, error)) (outTrue <-chan Try[A], outFalse <-chan Try[A])
- func Split2[A any](in <-chan Try[A], n int, f func(A) (bool, error)) (outTrue <-chan Try[A], outFalse <-chan Try[A])
- func ToChans[A any](in <-chan Try[A]) (<-chan A, <-chan error)
- func ToSlice[A any](in <-chan Try[A]) ([]A, error)
- func Unbatch[A any](in <-chan Try[[]A]) <-chan Try[A]
- type Try
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Batch ¶
Batch groups items from an input channel into batches based on a maximum size and a timeout. A batch is emitted when it reaches the maximum size, the timeout expires, or the input channel closes. To emit batches only when full, set the timeout to -1. This function never emits empty batches. The timeout countdown starts when the first item is added to a new batch. Zero timeout is not supported and will panic.
func Buffer ¶
Buffer takes a channel of items and returns a buffered channel of exact same items in the same order. This is useful when you want to write to the input channel without blocking the writer.
Typical use case would look like
ids = Buffer(ids, 100) // Now up to 100 ids can be buffered if subsequent stages of the pipeline are slow
func Catch ¶
Catch allows handling errors from the input channel using n goroutines for concurrency. When f returns nil, error is considered handled and filtered out; otherwise it is replaced by the result of f. The output order is not guaranteed: results are written to the output as soon as they're ready. Use OrderedCatch to preserve the input order.
func Drain ¶
func Drain[A any](in <-chan A)
Drain consumes and discards all items from an input channel, blocking until the channel is closed
func Filter ¶
Filter removes items that do not meet a specified condition, using n goroutines for concurrency. If an error is encountered, either from the function f itself or from upstream it is forwarded to the output for further handling. The output order is not guaranteed: results are written to the output as soon as they're ready. Use OrderedFilter to preserve the input order.
func FlatMap ¶
FlatMap applies a function to each item in an input channel, where the function returns a channel of items. These items are then flattened into a single output channel using n goroutines for concurrency. The output order is not guaranteed: results are written to the output as soon as they're ready. Use OrderedFlatMap to preserve the input order.
func ForEach ¶
ForEach applies a function f to each item in an input channel using n goroutines for parallel processing. The function blocks until all items are processed or an error is encountered, either from the function f itself or from upstream. In case of an error leading to early termination, ForEach ensures the input channel is drained to avoid goroutine leaks, making it safe for use in environments where cleanup is crucial. The function returns the first encountered error, or nil if all items were processed successfully. While this function does not guarantee the order of item processing due to its concurrent nature, using n = 1 results in sequential processing, as in a simple for-range loop.
func FromChan ¶
FromChan converts a regular channel into a channel of values wrapped in a Try container. Additionally, this function can take an error, that will be added to the output channel alongside the values. If both values and error are nil, the function returns nil.
func FromChans ¶
FromChans converts a regular channel into a channel of values wrapped in a Try container. Additionally, this function can take a channel of errors, which will be added to the output channel alongside the values. If both values and errors are nil, the function returns nil.
func FromSlice ¶
FromSlice converts a slice into a channel of Try containers. If err is not nil function returns a single Try container with the error.
func Map ¶
Map applies a transformation function to each item in an input channel, using n goroutines for concurrency. If an error is encountered, either from the function f itself or from upstream it is forwarded to the output for further handling. The output order is not guaranteed: results are written to the output as soon as they're ready. Use OrderedMap to preserve the input order.
func Merge ¶
func Merge[A any](ins ...<-chan A) <-chan A
Merge combines multiple input channels into a single output channel. Items are emitted as soon as they're available, so the output order is not defined.
func OrderedCatch ¶
OrderedCatch is similar to Catch, but it guarantees that the output order is the same as the input order.
func OrderedFilter ¶
OrderedFilter is similar to Filter, but it guarantees that the output order is the same as the input order.
func OrderedFlatMap ¶
OrderedFlatMap is similar to FlatMap, but it guarantees that the output order is the same as the input order.
func OrderedMap ¶
OrderedMap is similar to Map, but it guarantees that the output order is the same as the input order.
func OrderedSplit2 ¶
func OrderedSplit2[A any](in <-chan Try[A], n int, f func(A) (bool, error)) (outTrue <-chan Try[A], outFalse <-chan Try[A])
OrderedSplit2 is similar to Split2, but it guarantees that the order of the outputs matches the order of the input.
func Split2 ¶
func Split2[A any](in <-chan Try[A], n int, f func(A) (bool, error)) (outTrue <-chan Try[A], outFalse <-chan Try[A])
Split2 divides the input channel into two output channels based on the discriminator function f, using n goroutines for concurrency. The function f takes an item from the input and decides which output channel (outTrue or outFalse) it should go to by returning a boolean. If an error is encountered, either from the function f itself or from upstream it is intentionally sent to one of the output channels in a non-deterministic manner. The output order is not guaranteed: results are written to the outputs as soon as they're ready. Use OrderedSplit2 to preserve the input order.
func ToChans ¶
ToChans splits a channel of Try containers into a channel of values and a channel of errors. It's an inverse of FromChans. Returns two nil channels if the input is nil.
func ToSlice ¶
ToSlice converts a channel of Try containers into a slice of values and an error. It's an inverse of FromSlice. The function blocks until the whole channel is processed or an error is encountered. In case of an error leading to early termination, ToSlice ensures the input channel is drained to avoid goroutine leaks.