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}