DEV Community

Orlando Vargas
Orlando Vargas

Posted on

Implementing factory pattern GO

I decided to write this article after spending too much time looking to implement things in GO similarly to Java with Spring Boot. I still consider myself a newbie in GO, but I hope my findings help other users look for their way in this language.

The best way to start with a new language is to read enough official documentation and then jump in, take an existing code you have in another language and migrate it.


In my project, we have a service in Java that provides an interface that we use from other services to store files using one of our supported storage providers like GCP, AWS, or Azure. I wanted to migrate a part of this service to understand GO.

I started creating an interface similar to what we have in Java:

package storage

type Storage interface {
   Create(filename string, content []byte) error
   Download(filename string) ([]byte, error)
   Delete(filename string) error
}
Enter fullscreen mode Exit fullscreen mode

The implemented provider for GCP looked like the following:

package gcp

type storage struct {

}

func NewGCP(cfg map[string]string) *storage {
   return &storage {
       // initialize fields
   }
}
func (s *storage) Create(filename string, content []byte) error {
   panic("implement gcp create file")
}

func (s *storage) Download(filename string) ([]byte, error) {
   panic("implement gcp download file")
}

func (s *storage) Delete(filename string) error {
   panic("implement gcp delete file")
}
Enter fullscreen mode Exit fullscreen mode

And for AWS:

package aws

type storage struct {

}

func NewAWS(cfg map[string]string) *storage {
   return &storage {
       // initialize fields
   }
}

func (s *storage) Create(filename string, content []byte) error {
   panic("implement aws create file")
}

func (s *storage) Download(filename string) ([]byte, error) {
   panic("implement aws download file")
}

func (s *storage) Delete(filename string) error {
   panic("implement aws delete file")
}
Enter fullscreen mode Exit fullscreen mode

In GO, there isn’t such thing as the Constructor, at least not in the same way it exists in Java or .Net; instead, you initialize a struct directly by providing a value for each one of its fields but, what if the struct is non-exported or it has non-exported fields?

In the previous code, the NewGCP and NewAWS functions act as the factory of their storage struct, allowing developers to initialize it out of its package.

These functions are the most common factory implementation found in GO code, and even for exported structs, you might need to implement a factory function like these to initialize its non-exported fields.

The current JAVA service allows our customers to select which provider to use, so I needed an additional factory to return the proper implementation for each customer.

My first thought was to use dependency injection the same way the JAVA service does with Spring Boot and the Qualifier annotation.

After searching for a DI library, I found recommendations about not using or creating such libraries because they can obscure your code with a sort of magic, making it hard to read and maintain.

With that in mind, my first implementation was like the following:

func New(providerName string, cfg map[string]string) (Storage, error) {
   switch providerName {
   case "gcp":
      return gcp.NewGCP(cfg), nil
   case "aws":
      return aws.NewAWS(cfg), nil
   default:
      return nil, fmt.Errorf("unsupported provider")
   }
}
Enter fullscreen mode Exit fullscreen mode

The previous code worked fine; it is easy to read and maintain but still isn’t too good; if we need to support new providers, we have to change it every time; this violates the Open-Closed principle that states that; classes, modules, functions, etc., should be open for extension but closed for modification.

While migrating the code, I started working with the database integration; I noticed the Import for side effects case mentioned in the “effective go” documentation, so reading the code of the SQL and MySql packages, I realized that the pattern to create the factory and fixing the Open-Closed violation in the above code was already in these packages.

I changed my code to this approach by making the following refactoring:

First, I moved and renamed the Storage interface to its package.

package provider
type Provider interface {
   Create(filename string, content []byte) error
   Download(filename string) ([]byte, error)
   Delete(filename string) error
}

type Factory func(cfg map[string]string) (Provider, error)
Enter fullscreen mode Exit fullscreen mode

Notice the two types; Provider is the interface to create a new storage integration, and Factory is the function’s signature to initialize the storage provider.

Then, I refactor my factory method to make it extensible, changing the switch/case with a map and creating a function to register the providers.

package storage

import (
   "fmt"
   "storage/provider"
   "sync"
)

var (
   providersMu sync.RWMutex
   providers   = make(map[string]provider.Factory)
)

func Register(providerName string, provider provider.Factory) {
   providersMu.Lock()
   defer providersMu.Unlock()
   if provider == nil {
      panic("storage: Register provider is nil")
   }
   if _, dup := providers[providerName]; dup {
      panic("storage: Register called twice for provider " + providerName)
   }
   providers[providerName] = provider
}

type Storage struct {
   provider provider.Provider
}

func New(providerName string, cfg map[string]string) (*Storage, error) {
   providersMu.RLock()
   factory, ok := providers[providerName]
   providersMu.RUnlock()

   if !ok {
      return nil, fmt.Errorf("storage: unknown provider %q (forgotten import?)", providerName)
   }

   p, err := factory(cfg)

   if err != nil {
      return nil, err
   }

   return &Storage{
      provider: p,
   }, nil
}

func (s *Storage) Create(filename string, content []byte) error {
   // Put here any additional logic
   return s.provider.Create(filename, content)
}

func (s *Storage) Download(filename string) ([]byte, error) {
   // Put here any additional logic
   return s.provider.Download(filename)
}

func (s *Storage) Delete(filename string) error {
   // Put here any additional logic
   return s.provider.Delete(filename)
}

Enter fullscreen mode Exit fullscreen mode

Finally, I added the init function to each provider package to auto-register them during the package initialization.

package gcp

import (
   "storage"
   "storage/provider"
)

type gcpStorage struct {
}

func init() {
   storage.Register("gcp", newGcp)
}

func newGcp(cfg map[string]string) (provider.Provider, error) {
   // check if the configuration is ok

   return &gcpStorage{

   }, nil
}

func (s *gcpStorage) Create(filename string, content []byte) error {
   panic("implement gcp create file")
}

func (s *gcpStorage) Download(filename string) ([]byte, error) {
   panic("implement gcp download file")
}

func (s *gcpStorage) Delete(filename string) error {
   panic("implement gcp delete file")
}
Enter fullscreen mode Exit fullscreen mode

The code to use the Storage library looks like the following:

package main

import (
   "storage"
   _ "storage/aws"
   _ "storage/gcp"
)

func main() {
   s, err := storage.New("gcp", map[string]string{})
   if err != nil {
      log.Fatal(err)
   }

   err = s.Create("hw.txt", []byte("Hello World"))
   if err != nil {
      log.Fatal(err)
    }
}
Enter fullscreen mode Exit fullscreen mode

In this article, I showed part of my journey learning GO by implementing a factory method based on code written by the community.

I recommend you to read more of the official documentation, look for examples reading the official repositories, not only the Hello World examples, try not reinventing the wheel, and finally, ensure to understand the patterns and the implementation before adopting a library that could make your code less maintainable and readable for yourself and your team.

Top comments (0)