Golang: Functional Options Pattern
2024-06-26
What is the Functional Options Pattern?
In essence, the functional options pattern in Go is a design approach that enhances the way you create and configure objects. It offers a flexible and readable way to set optional parameters when constructing objects, especially when dealing with numerous configurations.
Key Advantages:
- Readability: The code becomes more self-explanatory as each option function clearly denotes its purpose.
- Flexibility: You can easily add, remove, or reorder configuration options without disrupting the existing codebase.
- Conciseness: It avoids the need for numerous constructors or overloaded functions with varying parameter lists.
- Defaults: Options can have sensible default values, making the code more user-friendly.
Core Concept:
- Option Functions: You define functions that take a pointer to the object being configured and modify its fields accordingly.
- Variadic Parameter: The object constructor accepts a variadic parameter (e.g.,
...Option
) to collect any number of these option functions. - Application: Inside the constructor, you iterate through the provided option functions and apply each one to the object.
Example 1: Configuring a Server
package main
import (
"fmt"
"net/http"
)
type Server struct {
host string
port int
readTimeout int
writeTimeout int
handler http.Handler // Use http.Handler interface
}
// Option type (function that modifies Server)
type Option func(*Server)
func NewServer(options ...Option) *Server {
s := &Server{
host: "localhost",
port: 8080,
readTimeout: 5, // Default timeouts in seconds
writeTimeout: 10,
handler: http.NotFoundHandler(), // Default 404 handler
}
// Apply options
for _, option := range options {
option(s)
}
return s
}
// Option functions
func WithHost(host string) Option {
return func(s *Server) {
s.host = host
}
}
func WithPort(port int) Option {
return func(s *Server) {
s.port = port
}
}
func WithTimeouts(readTimeout, writeTimeout int) Option {
return func(s *Server) {
s.readTimeout = readTimeout
s.writeTimeout = writeTimeout
}
}
func WithHandler(handler http.Handler) Option {
return func(s *Server) {
s.handler = handler
}
}
// Example handler
func myHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from %s!\n", r.URL.Path)
}
func main() {
server := NewServer(
WithHost("127.0.0.1"),
WithPort(9090),
WithTimeouts(10, 15), // Set read and write timeouts
WithHandler(http.HandlerFunc(myHandler)), // Set custom handler
)
// Start the server (not shown for brevity)
// ... http.ListenAndServe(fmt.Sprintf("%s:%d", server.host, server.port), server.handler) ...
}
Example 2: Configuring a Worker Pool
package main
import (
"fmt"
"time"
)
type WorkerPool struct {
numWorkers int
jobChannel chan func()
resultChannel chan interface{}
}
type Option func(*WorkerPool)
func NewWorkerPool(numWorkers int, options ...Option) *WorkerPool {
wp := &WorkerPool{
numWorkers: numWorkers,
// Default channels with specific types and sizes
jobChannel: make(chan func(), 10), // Buffered job queue
resultChannel: make(chan interface{}, 5), // Buffered result queue
}
for _, option := range options {
option(wp)
}
// Start the workers
for i := 0; i < numWorkers; i++ {
go worker(wp)
}
return wp
}
// Option functions to customize channel types and buffer sizes
func WithJobChannel(ch chan func()) Option {
return func(wp *WorkerPool) {
wp.jobChannel = ch
}
}
func WithResultChannel(ch chan interface{}) Option {
return func(wp *WorkerPool) {
wp.resultChannel = ch
}
}
// Worker function
func worker(wp *WorkerPool) {
for job := range wp.jobChannel {
result := job()
wp.resultChannel <- result
}
}
func main() {
unbufferedJobChannel := make(chan func())
bufferedResultChannel := make(chan interface{}, 20)
pool := NewWorkerPool(5,
WithJobChannel(unbufferedJobChannel),
WithResultChannel(bufferedResultChannel),
)
// Submit jobs
pool.jobChannel <- func() {
time.Sleep(100 * time.Millisecond)
return "Job 1 result"
}
pool.jobChannel <- func() {
time.Sleep(200 * time.Millisecond)
return "Job 2 result"
}
// Collect results
for i := 0; i < 2; i++ {
result := <-pool.resultChannel
fmt.Println(result)
}
}