Background

Ever lost count of how many times you’ve seen Java developers scratching their heads and asking, “How on earth do I design an abstract class with Go interfaces?” Well, newsflash: Inheritance packed its bags and left Go-land a long time ago. But fear not! Go has its own quirky ways to keep polymorphism and code reuse alive and kicking through the dynamic duo of composition and interfaces.

Interface

In Go, if a struct declares all the methods that an interface defines, then that struct implicitly implements that interface.

For example, imagine you’re writing code to deploy an application to an AWS EC2 instance:

type EC2Deployer interface {
DeployComponent()
}

type EC2 struct {
}

func (e *EC2) DeployComponent() {
// implementation
}

In the above example, EC2 implements EC2Deployer, allowing it to call the DeployComponent function.

However, if you’re asked to provide EKS deployment (and potentially deployment to a GCP service as well), you might face code duplication and maintenance challenges. This approach wouldn’t allow you to pass a base interface to a function, resulting in a solution that is neither straightforward nor easy to maintain.

func doSomethingThatAllTheDeployDo(deployer *AWSDeployer) {
  deployer.DeployComponent()
}

Composition

The Go Team encourages the use of composition over inheritance. In Go, composition is achieved by embedding one struct into another (or an interface into another).

Continuing with the example, your app can deploy something to EKS, EC2, Lambda, etc. We can use Go interfaces and composition to reduce code duplication as much as possible, making it more flexible to changes and easier to maintain.

// AWSDeployer represents a common behavior for deploying components to AWS.
type AWSDeployer interface {
	DeployComponent()
}

type AWS struct {
}

func (a *AWS) DeployComponent() {
	fmt.Println("default deploy to AWS")
}

In the above, the ‘base’ DeployComponent function can be used as default behavior in case a concrete implementation doesn’t need to do anything special.

Using composition, we can now create multiple concrete AWS services and embed the AWS struct into other structs.


type EC2 struct {
  AWS
  svc *ec2.Client
}

type EKS struct {
  AWS
  svc *eks.Client
}

You could also have a GCPAppDeployer if you decide to add support for Google Cloud later.

Ashley McNamara has some very good tips for naming conventions and Go best practices. In the above example, if we strictly follow the Go ‘best practices,’ when an interface has only one method, it usually ends with -er. I’m not a fan of this, but in my example, AWSDeployer actually makes sense.

More often than not, interfaces will end up a little more complex, and I personally think it’s fine to name them something as descriptive as possible.

Full example

Let’s say you now need to extend your application to also deploy to EKS. We can create a new struct EKS which will embed AWS, meaning the EKS struct will also be able to call any methods defined by (a *AWS).

aws_deployer.go

package deployer

type AWSDeployer interface {
  DeployComponent()
}

type AWS struct {
  somethingCommon string
}

func (a *AWS) DeployComponent() {
  fmt.Println("base AWS Deployer")
}

func GetDeployer(serviceType AWSServiceType) (*AWS, error) {
  var deployer *AWS
  swich serviceType {
  case: EC2:
    return NewEC2Deployer(aws.Config{}), nil
  case: EKS:
    return  NewEKSDeployer(aws.Config{}), nil
  default:
    return nil, errors.New("unknown aws service type"), nil
  }
}

ec2_deployer.go

package deployer

type EC2Deployer interface {
  DeployComponent()
}

type EC2 struct {
  AWS
  svc *ec2.Client
}

func NewEC2Deployer(cfg aws.Config) *EC2 {
  return &EC2{
    svc: ec2.NewFromConfig(cfg),
  }
}

func (b *EC2) Deploy() {
  fmt.Println("deploying to EC2")
}

eks_deployer.go

package deployer

// EKSDeployer represents a common behavior for deploying components to the cloud.
type EKSDeployer interface {
  DeployComponent()
}

type EKS struct {
  AWS
  svc *eks.Client
}

func NewEKSDeployer(cfg aws.Config) *EKS {
  return &EKS{
    svc: eks.NewFromConfig(cfg),
  }
}

func (b *EKS) Deploy() {
  fmt.Println("deploying to EKS")
}

Notice that in the base AWSDeployer, I have added some kind of factory (though in Go, the Factory pattern doesn’t make a lot of sense).

This is how you can call each function:

func main() {
  &AWS{}.DeployComponent()
  deployer.GetDeployer(EKS).DeployComponent()
  deployer.GetDeployer(EC2).DeployComponent()
}

Will print

base AWS Deployer
deploying to EKS
deploying to EC2

Notice as well how I defined each struct.

type EC2 struct {
  AWS
  svc *ec2.Client
}

Typically, this is where you add the services required by your deploy. By passing these services through the struct, you simplify the process of injecting mocks for unit testing. I’ll provide a more detailed explanation of this in a future post.