Cory Hall

Go Construct Patterns

Recently I’ve been working a lot with the AWS CDK in Golang and there isn’t a lot of prior art to reference. When working in TypeScript I could always just look at the aws-cdk repo and find tons of great examples. One thing that has taken me a while to develop a pattern for is creating constructs. In TypeScript it’s pretty simple, you just create a new class that extends some other construct and you’re done. In Go, it’s a little more complicated.

In this post I’m going to talk through some patterns that I’ve discovered when creating Go constructs. If you’ve discovered better ones, please let me know!

Creating a new simple construct

I’ll start off by talking about the basic example that you’ve probably seen elsewhere. For the example, I’ll be creating a construct with an HttpApi as the core resource.

Since Go doesn’t have classes, you can’t just create a MyApi class and extend construct like you would do in TypeScript. Instead you would create a function that looks something like this.

1func NewMyApi(scope constructs.Construct, id string) constructs.Construct {
2    c := constructs.NewConstruct(scope, &id)
3
4    return c
5}

Now I can create my resources within this function and make sure I use the construct that I created as the scope that I pass in.

1func NewMyApi(scope constructs.Construct, id string) constructs.Construct {
2    c := constructs.NewConstruct(scope, &id)
3
4	awscdkapigatewayv2alpha.NewHttpApi(c, jsii.String("MyHttpApi"), &awscdkapigatewayv2alpha.HttpApiProps{})
5
6    return c
7}

That seems pretty straightforward, but I’m only returning a constructs.Construct. What if I want to reference my HttpApi that I created?

Constructs with properties

As soon as I want to reference that HttpApi outside of this construct I need to find a way of returning something other than constructs.Construct. I could take two different approaches. If I wanted this construct to really just be a wrapper around a HttpApi then I could just change it to return the HttpApi.

1func NewMyApi(scope constructs.Construct, id string) awscdkapigatewayv2alpha.HttpApi {
2    c := constructs.NewConstruct(scope, &id)
3
4	return awscdkapigatewayv2alpha.NewHttpApi(c, jsii.String("MyHttpApi"), &awscdkapigatewayv2alpha.HttpApiProps{})
5}

We’ll come back to this approach a little later.

If I want this to be a more general construct or maybe an L3 construct (which represents more than just an HttpApi), then the other approach would be to just create a new struct and return that.

 1type MyApi struct {
 2    HttpApi awscdkapigatewayv2alpha.HttpApi
 3}
 4
 5func NewMyApi(scope constructs.Construct, id string) MyApi {
 6    c := constructs.NewConstruct(scope, &id)
 7    myApi := MyApi{}
 8
 9	myApi.HttpApi := awscdkapigatewayv2alpha.NewHttpApi(c, jsii.String("MyHttpApi"), &awscdkapigatewayv2alpha.HttpApiProps{})
10
11    return myApi
12}

Now we can access the HttpApi resource outside of this function. Pretty simple right? Not quite. In CDK anytime you create groupings of resources you typically wrap them in a Construct. It allows you to more easily access the child constructs, and do things like add a dependency that affects all child constructs.

1app := awscdk.NewApp()
2stack := awscdk.NewStack(app, jsii.String("Stack"), &awscdk.StackProps{})
3myApi := NewMyApi(stack, "MyApi")
4other := NewSomeOtherConstruct(stack, "OtherConstruct")
5
6// MyApi has a dependency on other
7other.Node().AddDependency(myApi)

The way I’ve setup MyApi, this won’t work! myApi is a MyApi struct type, not a construct. I could just add another property to MyApi like this:

 1type MyApi struct {
 2    HttpApi awscdkapigatewayv2alpha.HttpApi
 3    Construct awscdk.Construct
 4}
 5
 6func NewMyApi(scope constructs.Construct, id string) MyApi {
 7    c := constructs.NewConstruct(scope, &id)
 8    myApi := MyApi{
 9        Construct: c,
10    }
11
12	myApi.HttpApi := awscdkapigatewayv2alpha.NewHttpApi(c, jsii.String("MyHttpApi"), &awscdkapigatewayv2alpha.HttpApiProps{})
13
14    return myApi
15}

But then I have to make sure to always reference the Construct property, which is not very intuitive.

1other.Node().AddDependency(myApi.Construct)

Construct Override

What I really want is for MyApi to be a construct. In other languages this might be done using extends or implements. In Go I can do this by using type embedding, but as this article points out, this is not true inheritance. Since all objects in CDK are interfaces what you might end up doing is trying to embed an interface in a struct, which as the article points out, “is allowed but is almost never what you want.” In CDK apps this is especially true. If we try to embed Construct in MyApi:

1type MyApi struct {
2    awscdk.Construct
3    HttpApi awscdkapigatewayv2alpha.HttpApi
4}

We will eventually end up getting errors like this:

1panic: @jsii/kernel.SerializationError: Passed to parameter construct of static method aws-cdk-lib.Stack.of: Unable to deserialize value as constructs.IConstruct
2├── 🛑 Failing value is an object
3{ Construct: {} }
4╰── 🔍 Failure reason(s):
5    ╰─ Value does not have the "$jsii.byref" key

Instead we need to turn the MyApi struct into an interface. This means that our struct properties also need to become interface methods.

1type MyApi interface {
2    constructs.Construct
3    HttpApi() awscdkapigatewayv2alpha.HttpApi
4}

We can then create a private struct that implements the MyApi construct.

1type myApi struct {
2    constructs.Construct
3    httpApi awscdkapigatewayv2alpha.HttpApi
4}
5
6func (a *myApi) HttpApi() awscdkapigatewayv2alpha.HttpApi {
7    return a.httpApi
8}

The last piece is to use constructs.NewConstruct_Override(). This ensures that it will use your myApi struct as the struct that internally implements the Construct interface.

1func NewMyApi(scope constructs.Construct, id string) MyApi {
2    myApi := myApi{}
3    constructs.NewConstruct_Override(myApi, scope, &id)
4
5	myApi.httpApi := awscdkapigatewayv2alpha.NewHttpApi(myApi, jsii.String("MyHttpApi"), &awscdkapigatewayv2alpha.HttpApiProps{})
6
7    return myApi
8}

Now you can use MyApi as a Construct and still have access to your custom properties. The full example would look something like this.

 1type MyApi interface {
 2    constructs.Construct
 3    HttpApi() awscdkapigatewayv2alpha.HttpApi
 4}
 5
 6type myApi struct {
 7    constructs.Construct
 8    httpApi awscdkapigatewayv2alpha.HttpApi
 9}
10
11func NewMyApi(scope constructs.Construct, id string) MyApi {
12    myApi := myApi{}
13    constructs.NewConstruct_Override(myApi, scope, &id)
14
15	myApi.httpApi := awscdkapigatewayv2alpha.NewHttpApi(myApi, jsii.String("MyHttpApi"), &awscdkapigatewayv2alpha.HttpApiProps{})
16
17    return myApi
18}
19
20func (a *myApi) HttpApi() awscdkapigatewayv2alpha.HttpApi {
21    return a.httpApi
22}

Wrapping a L2 Construct (L2.5)

Coming back to the example earlier where I want my construct to represent my own version of an HttpApi I can do something very similar to what I did with the Construct example in the previous section.

 1type MyApi interface {
 2    awscdkapigatewayv2alpha.HttpApi
 3}
 4
 5type myApi struct {
 6    awscdkapigatewayv2alpha.HttpApi
 7}
 8
 9func NewMyApi(scope constructs.Construct, id string) MyApi {
10    myApi := myApi{}
11
12	awscdkapigatewayv2alpha.NewHttpApi_Override(myApi, scope, jsii.String("MyHttpApi"), &awscdkapigatewayv2alpha.HttpApiProps{})
13
14    return myApi
15}