0% found this document useful (0 votes)
2 views5 pages

Golang_generics

The blog post discusses the Functional Options Pattern in Go, which addresses issues related to creating and configuring objects with default values. It explains how to implement this pattern using a Factory Method and Generic Helper Functions, allowing for flexible, scalable, and maintainable code. The post also highlights the advantages of using this pattern over traditional parameter passing methods, emphasizing improved readability and customization.

Uploaded by

Shyam Kumar
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2 views5 pages

Golang_generics

The blog post discusses the Functional Options Pattern in Go, which addresses issues related to creating and configuring objects with default values. It explains how to implement this pattern using a Factory Method and Generic Helper Functions, allowing for flexible, scalable, and maintainable code. The post also highlights the advantages of using this pattern over traditional parameter passing methods, emphasizing improved readability and customization.

Uploaded by

Shyam Kumar
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
You are on page 1/ 5

In this blog post, we’ll dive into

What Problems Does the Functional Options Pattern Solve?


Functional Options Pattern Concept
How to Write Generic Helper Functions
Implementing Into a Factory Method.
What Problems Does the Functional Options Pattern Solve?
In Go, we have the flexibility to create new types based on structs, allowing us to
specify only the fields we need. For example

package main

import (
"log"
"time"
)

type Server struct {


host string
port int
timeout time.Duration
}

func (s *Server) Run() {


log.Printf("Server running %s:%d", s.host, s.port)

func main() {
localHostServer := &Server{
host: "127.0.0.1",
port: 8080,
timeout: 3 * time.Second,
}
localHostServer.Run()
}
This Go code snippet outlines a Server struct, equipped with host, port, and
timeout configurations. It includes methods to kickstart the server, coupled with
logging functionalities to track its actions. The main function showcases how to
initiate the server, providing a practical example of its startup process

Creating default values

Now, consider a scenario where we frequently create a Server instance with the same
attribute values. Unfortunately, we can’t directly encode these default values into
the type itself. This is where the Factory Method comes into play. In essence, the
Factory Method is a design pattern that involves a method dedicated to creating and
returning an instance of an object, pre-populated with default values. For example

func NewLocalHost() *Server {


return &Server{
host: "127.0.0.1",
port: 8080,
timeout: 3 * time.Second,
}

func main() {
localHostServer := NewLocalHost()
localHostServer.Run()
}
Notice that we now have a method to generate a Server object with predefined
values, which is incredibly useful given that these values are common across my
application. Thus, we’ve introduced a function, NewLocalHost, which provides us
with an object that’s ready to use

Now How Can We Modify Default Values?

The dilemma often boils down to ‘can’t’ versus ‘shouldn’t’. Despite the
constraints, a practical workaround involves passing arguments to a Factory Method

It’s advisable to STEER CLEAR of implementing it in that manner.

// NewLocalHost creates a new Server instance with optional port and timeout
parameters.
// If port or timeout are not provided (nil), default values are used.
func NewLocalHost(port interface{}, timeout interface{}) *Server {
defaultPort := 8080
defaultTimeout := 3 * time.Second

// Check and set port if provided


actualPort := defaultPort
if p, ok := port.(int); ok {
actualPort = p
}

// Check and set timeout if provided


actualTimeout := defaultTimeout
if t, ok := timeout.(time.Duration); ok {
actualTimeout = t
}

return &Server{
host: "127.0.0.1",
port: actualPort,
timeout: actualTimeout,
}
}

func main() {
// Example usage of NewLocalHost without parameters, using default values
localHostServer := NewLocalHost(9090, nil)
localHostServer.Run()

// After some operations, stop the server


// localHostServer.Stop()
}
Directly passing field values to a factory method, rather than leveraging the
Functional Options pattern, can lead to several challenges, especially as your
codebase expands and evolves. Here are some potential issues associated with this
approach:

Limited Flexibility for Defaults: Managing default values becomes cumbersome. With
direct parameter passing, you either force callers to specify all values
explicitly, including those that should often just be defaults, or you create
multiple constructors for different scenarios, which leads to cluttered and less
maintainable code.\
Long Parameter List: As the number of server parameters grows, the factory method
signature become unwildy.
Compromised Readability: When a function is called with multiple parameters,
especially if they are of the same type, it's hard to tell what each parameter
represents without looking up the function definition. This makes the code less
readable and more error-prone.
Reduced Encapsilation and Flexibility: Directly passing parameters requires
exposing the internal structure and implementation details of your objects. This
can reduce the dlexibility to change the internal implementation.
Inconsistent Object State: Without a clear mechanism to enforce the setting of
necessary fields or validate the configuration, it's easy to end up with objects in
an inconsistent or invalid state.
How Function Options Pattern solve all of those issues?
This pattern are functions that you can agregate to a Factory Method an Example

func main() {
localHostServer, err := NewLocalHost(WithPort(9090))
if err != nil {
log.Fatal(err)
}
localHostServer.Run()
}
The code snipped above has used Function Option Pattern, now it’s possible
personalise the values through Generic Helper Functions WithTimeOut or WithPort.

Notice that we just have showed the main using the Options Pattern not the over all
implementation. I've wanted point out how easy is use this methods instead pass the
value directly. Couples advantages of use that Pattern:

Flexible: It allows for easy addition of new options without breaking existing
code.
Scalable: The pattern scales well with complex configurations and evolving software
requirements.
Readable: Code using functional options is often more readable than alternatives,
making it easier to understand what options are being set.
Intuitive: The pattern leverages Go's first-class functions and closures, making it
intuitive for those familiar with these concepts.
Customizable: Offers a high degree of customization, allowing developers to define
options that can precisely control the behavior of their objects.
Maintainable: The pattern promotes maintainability by keeping configuration logic
centralized and decoupled from the object's core functionality.
But is missing the full implementation, how create a Generic Helper Function and
how implement them into a Factory Method?

How create a Generic Helper


A import point about Generic Helper Functions, they dont execute any change to
value that process is going just on the next step. They return a function that
receive as argument own object and return an error.

Then let’s create a type return by Generic Helper Function

type OptionsServerFunc func(c *Server) error


Now that we have own type let’s recreate the WithPort Generic Helper with an
improvement. Some patterns and

Prefix name as With and field changed


Receive only one argument per function
Argument must has same type of the field who will receive
Must return a function with the assign operation
To show as error mechanism can be used we will add extra validation to check if the
port range of 5000 to 9999,

Let’s code

func WithPort(port int) OptionsServerFunc {


return func(s *Server) error {
// Check if the port is within the valid range.
if port >= 5000 && port < 10000 {
s.port = port
return nil
}
// Return an error if the port is out of the valid range, using formatted
error string for clarity.
return fmt.Errorf("port %d is out of the valid range (5000-9999)", port)
}
}
The code snipped has a Generic Helper Fuction ready to be used even thow if we use
as argument won’t work into NewLocalHost if mechanism to receive not exist.

How add Generic Helper Fuction into a factory method?


We need deal with multiples Genreic Helper who are optional and then read one by
one passing into them the object and check if return an error value.

Fist to the function receive multiples arguments we must use a functionality is


called “variadic functions”. A variadic function can take an arbitrary number of
argument of the same type. This achieved by using the ellipsis (‘…’) prefix before
the parameter type in the function definition.

Variadic Function exemple

func sum(nums ...int) int {


total := 0
for _, num := range nums {
total += num
}
return total
}

func main() {
fmt.Println(sum(1, 2))
fmt.Println(sum(1, 2, 3))

// You can also pass a slice of ints by using the ellipsis suffix
numbers := []int{1, 2, 3, 4}
fmt.Println(sum(numbers...))
}
Now we’ve already know how use the variadic mechanism we need iterate with the list
of Generic Help Function. We can use a range to do it, ignore the index and pass
the objected created as argument.

Let’s code

func NewLocalHost(opts ...OptionsServerFunc) (*Server, error) {


server := &Server{
host: "127.0.0.1",
port: 8080,
timeout: 3 * time.Second,
}
for _, opt := range opts {
if err := opt(server); err != nil {
return nil, err
}
}
return server, nil

}
The code snippet features a loop within the NewLocalHost function that iterates
over a slice of OptionsServerFunc. Each opt within the slice is a function
accepting a pointer to a Server as its argument, and it returns an error. As the
loop progresses, each opt is invoked with server as its parameter. Should any opt
return an error, the loop halts prematurely, and the NewLocalHost function returns
both nil and the encountered error.

You might also like